From 330bfac76f52948da7e38c00ec6b101f42f6bf39 Mon Sep 17 00:00:00 2001 From: Eduard Marbach Date: Mon, 1 Apr 2024 20:06:26 +0200 Subject: [PATCH] feat: initial feature implementation (#2) Co-authored-by: Vincent Link --- .dockerignore | 5 + .env.template | 9 +- .github/workflows/release.yml | 79 + .gitignore | 159 +- .prettierrc.js | 4 + .vscode/launch.json | 42 + .vscode/settings.json | 3 + Dockerfile | 26 + README.md | 189 +- config.yml.template | 35 + custom/cfs/custom-size-bigger-40gb.json | 22 + docker-compose.yml | 42 + index.js => generate-api.js | 25 +- index.ts | 920 +- package.json | 22 +- playwright.config.ts | 77 + pnpm-lock.yaml | 1126 +- secrets.yml.template | 1 + src/__generated__/generated-radarr-api.ts | 6352 ++++ ...MySuperbApi.ts => generated-sonarr-api.ts} | 641 +- src/api.ts | 126 + src/config.ts | 39 + src/custom-formats.ts | 164 + src/example.test.ts | 270 + src/quality-definitions.ts | 63 + src/quality-profiles.ts | 460 + src/recyclarr-importer.ts | 51 + src/trash-guide.ts | 107 + src/types.ts | 155 + src/util.ts | 199 + tests/e2e/demo-todo-app.spec.ts | 416 + tests/samples/cfs.json | 25095 ++++++++++++++++ tests/samples/qualityDefinition.json | 294 + tests/samples/quality_profiles.json | 3734 +++ tests/samples/quality_with_grouping.json | 27 + tests/samples/quality_without_grouping.json | 10 + tsconfig.json | 10 + vitest.config.ts | 7 + 38 files changed, 39738 insertions(+), 1268 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/release.yml create mode 100644 .prettierrc.js create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 Dockerfile create mode 100644 config.yml.template create mode 100644 custom/cfs/custom-size-bigger-40gb.json create mode 100644 docker-compose.yml rename index.js => generate-api.js (52%) create mode 100644 playwright.config.ts create mode 100644 secrets.yml.template create mode 100644 src/__generated__/generated-radarr-api.ts rename src/__generated__/{MySuperbApi.ts => generated-sonarr-api.ts} (91%) create mode 100644 src/api.ts create mode 100644 src/config.ts create mode 100644 src/custom-formats.ts create mode 100644 src/example.test.ts create mode 100644 src/quality-definitions.ts create mode 100644 src/quality-profiles.ts create mode 100644 src/recyclarr-importer.ts create mode 100644 src/trash-guide.ts create mode 100644 src/types.ts create mode 100644 src/util.ts create mode 100644 tests/e2e/demo-todo-app.spec.ts create mode 100644 tests/samples/cfs.json create mode 100644 tests/samples/qualityDefinition.json create mode 100644 tests/samples/quality_profiles.json create mode 100644 tests/samples/quality_with_grouping.json create mode 100644 tests/samples/quality_without_grouping.json create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4f75c95 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.git +.gitignore +*.md +dist diff --git a/.env.template b/.env.template index 215eeb2..d3d076d 100644 --- a/.env.template +++ b/.env.template @@ -1,4 +1,5 @@ -SONARR_API_KEY= -SONARR_URL= -RADARR_API_KEY= -RADARR_URL= +#CONFIG_LOCATION=/app/config/config.yml +#SECRETS_LOCATION=/app/config/secrets.yml +DRY_RUN=true +LOAD_LOCAL_SAMPLES=false +DEBUG_CREATE_FILES=false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4dfa53a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,79 @@ +name: Docker Builds + +on: + workflow_dispatch: + push: + branches: + - "main" + tags: + - "v*.*.*" + +permissions: + contents: read + packages: write + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build: + runs-on: ubuntu-latest + steps: + # Get the repositery's code + - name: Checkout + uses: actions/checkout@v4 + + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + # - name: Available platforms + # run: echo ${{ steps.buildx.outputs.platforms }} + + - name: Login to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + # - name: Login to Docker Hub + # if: github.event_name != 'pull_request' + # uses: docker/login-action@v3 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Docker meta for PMS + id: meta_pms + uses: docker/metadata-action@v5 + with: + # list of Docker images to use as base name for tags + images: | + ghcr.io/raydak-labs/configarr + # generate Docker tags based on the following events/attributes + tags: | + type=schedule + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + - name: Build and push PMS + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta_pms.outputs.tags }} + labels: ${{ steps.meta_pms.outputs.labels }} + # - name: Docker Hub Description + # if: github.event_name != 'pull_request' + # uses: peter-evans/dockerhub-description@v3 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_PASSWORD }} + # repository: pabloromeo/clusterplex_pms + # readme-filepath: ./README.md + # short-description: "PMS image for ClusterPlex" diff --git a/.gitignore b/.gitignore index 8bfe1dd..333c330 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,159 @@ -node_modules -repos +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node + + + *.env !.env.template +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +config.yml +secrets.yml + +test*.json +repos/ diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..8aabe6f --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,4 @@ +/** @type {import("prettier").Options} */ +export default { + printWidth: 140, +}; diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9419ec1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,42 @@ +{ + "version": "0.2.0", + + "configurations": [ + { + "command": "pnpm start", + "name": "pnpm start", + "request": "launch", + "type": "node-terminal" + }, + { + "name": "tsx", + "type": "node", + "request": "launch", + + // Debug current file in VSCode + "program": "${file}", + + /* + Path to tsx binary + Assuming locally installed + */ + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", + + /* + Open terminal when debugging starts (Optional) + Useful to see console.logs + */ + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + + // Files to exclude from debugger (e.g. call stack) + "skipFiles": [ + // Node.js internal core modules + "/**", + + // Ignore all dependencies (optional) + "${workspaceFolder}/node_modules/**" + ] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..db8165b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "yaml.customTags": ["!secret"] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a7f9ec6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md +FROM node:20-slim AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN apt-get update && apt-get install -y \ + git \ + && rm -rf /var/lib/apt/lists/* \ + && corepack enable && corepack prepare pnpm@8.15.5 --activate +WORKDIR /app +COPY package.json pnpm-lock.yaml /app/ + +FROM base AS prod-deps +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile + +FROM base +COPY index.ts /app/ +COPY src/ /app/src/ +COPY --from=prod-deps /app/node_modules /app/node_modules + +ENV CONFIG_LOCATION=/app/config/config.yml +ENV SECRETS_LOCATION=/app/config/secrets.yml + +#USER node + +CMD [ "pnpm", "start" ] diff --git a/README.md b/README.md index 91d6259..c913eaa 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Configuration and synchronization tool for Sonarr and Radarr. +Supporting only Sonarr v4 and radarr v4. + This will be a project similar to [Recyclarr](https://github.com/recyclarr/recyclarr) or [Notifiarr](https://notifiarr.wiki/) but support for additional specification and not only what [TrashGuides](https://trash-guides.info/) offer. This is in very early development and trial stage to see if this is something we want to offer or not. @@ -9,9 +11,194 @@ This is in very early development and trial stage to see if this is something we > :warning: **This is in very early development and trial stage to see if this is something we want to offer or not.** Possible ideas: + - keep support for syncing trash guides - I like the possible configuration in recyclarr but I miss some features - add support for local configuration to include - I don't want to fork a project to add custom things to it -- Maybe an free GUI to sync your stuff +- Maybe an free GUI to sync your stuff - Add additional best configuration for different languages/countries like Germany +- Define CustomFormats directly in configuration maybe something like (directly translated JSON -> YAML?) + Not sure if useful if we already have to capability to provide own formats via JSON folder (additional those JSON are directly compatible with UI Import) + ```yaml + customFormats: + - name: NewCF + score: 125 + includeCustomFormatWhenRenaming: false + specifications: + - name: Size + implementation: SizeSpecification + negate: false + required: true + fields: + min: 1 + max: 9 + ``` + +## Features + +- Use TrashGuide defined custom formats +- Compatible with recyclarr templates +- Include own defined custom formats +- Custom defined formats for different languages/countries like Germany +- Support all CustomFormat specifications +- Maybe this?: https://github.com/recyclarr/recyclarr/issues/225 + +## Work TODOs + +- [ ] Optimize types. Generated ones work for first step but not very optimal because they do not correctly represent request/response types. +- [x] Default scores from trash guide +- [x] Radarr support +- [ ] Debug logging switchable +- [ ] Improved Diff output +- [ ] Feature completion with recyclarr +- [ ] Cross references to: + - [ ] https://github.com/PCJones/radarr-sonarr-german-dual-language + - [ ] https://github.com/PCJones/usenet-guide +- [ ] Build docker container +- [ ] Build multi arch containers +- [ ] Add Github Actions stuff +- [ ] Improve code & tidy up lint errors +- [ ] write docs for running with container + - [ ] Plain docker + - [ ] Kubernetes +- [ ] Simple Config validation +- [ ] Custom recyclarr templates? + - [ ] Lets say you want the same template but with a different name + +## Development + +1. Optionally setup the local sonarr instance + 1. Run `docker compose up -d` to run the container + 2. Open sonarr in your browser at http://localhost:8989 / radarr @ http://localhost:7878 + 3. Configure basic authentication, disable local authentication and create an initial user by specifying the e-mail and password +2. Open the sonarr [Settings > General](http://localhost:8989/settings/general) page and copy the API key +3. Create a `secrets.yml` from the template + 1. `cp secrets.yml.template secrets.yml` + 2. Replace the placeholder with your sonarr API key +4. Create a `config.yml` from the template + 1. `cp config.yml.template config.yml` + 2. Overwrite the hosts in case you are not using the local setup with docker compose +5. Run the app with `pnpm start` or with the vscode task + +## How to run + +Required files: +- `config.yml` +- `secrets.yml` + +Optional: +- Custom Formats in folders + +### Docker + +`docker run --rm -v ./:/app/config ghcr.io/raydak-labs/configarr:work` + +### Docker-compose + +```yml + +services: + configarr: + image: ghcr.io/raydak-labs/configarr:work + build: . + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + volumes: + - ./config:/app/config # Contains the config.yml and secrets.yml + - ./dockerrepos:/app/repos # Cache repositories + - ./custom/cfs:/app/cfs # Optional if custom formats locally provided +``` + +### Kubernetes + +Example how to run `CronJob` which will regulary sync your configs. + +```yml +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: configarr +spec: + schedule: "0 * * * *" + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + template: + spec: + containers: + - name: configarr + image: ghcr.io/raydak-labs/configarr:work + imagePullPolicy: Always + envFrom: + - configMapRef: + name: common-deployment-environment + volumeMounts: + - mountPath: /app/repos # Cache repositories + name: app-data + subPath: configarr-repos + - name: config-volume # Mount specifc config + mountPath: /app/config/config.yml + subPath: config.yml + - name: secret-volume + mountPath: /app/config/secrets.yml # Mount secrets + subPath: secrets.yml + volumes: + - name: app-data + persistentVolumeClaim: + claimName: media-app-data + - name: config-volume + configMap: + name: configarr + - name: secret-volume + secret: + secretName: configarr + restartPolicy: Never +--- +apiVersion: v1 +kind: Secret +metadata: + name: configarr +type: Opaque +stringData: + secrets.yml: | + SONARR_API_KEY: "{{ configarr.sonarrApiKey }}" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: configarr +data: + config.yml: | + trashGuideUrl: https://github.com/TRaSH-Guides/Guides + recyclarrConfigUrl: https://github.com/recyclarr/config-templates + + sonarr: + series: + # Set the URL/API Key to your actual instance + base_url: http://sonarr:8989 + api_key: !secret SONARR_API_KEY + + # Quality definitions from the guide to sync to Sonarr. Choices: series, anime + quality_definition: + type: series + + include: + # Comment out any of the following includes to disable them + #### WEB-1080p + - template: sonarr-quality-definition-series + - template: sonarr-v4-quality-profile-web-1080p + - template: sonarr-v4-custom-formats-web-1080p + + #### WEB-2160p + - template: sonarr-v4-quality-profile-web-2160p + - template: sonarr-v4-custom-formats-web-2160p + + # Custom Formats: https://recyclarr.dev/wiki/yaml/config-reference/custom-formats/ + custom_formats: [] + radarr: {} +``` diff --git a/config.yml.template b/config.yml.template new file mode 100644 index 0000000..d8a8d98 --- /dev/null +++ b/config.yml.template @@ -0,0 +1,35 @@ +trashGuideUrl: https://github.com/TRaSH-Guides/Guides +#trashRevision: master # Optional to specify sha +recyclarrConfigUrl: https://github.com/recyclarr/config-templates +#recyclarrRevision: master # Optional to specify sha + +# Optional if you want to add custom formats locally +#localCustomFormatsPath: ./custom/cfs + +sonarr: + series: + # Set the URL/API Key to your actual instance + base_url: http://localhost:8989 + api_key: !secret SONARR_API_KEY + + # Quality definitions from the guide to sync to Sonarr. Choices: series, anime + quality_definition: + type: series + + include: + # Comment out any of the following includes to disable them + #### WEB-1080p + - template: sonarr-quality-definition-series + - template: sonarr-v4-quality-profile-web-1080p + - template: sonarr-v4-custom-formats-web-1080p + + #### WEB-2160p + - template: sonarr-v4-quality-profile-web-2160p + - template: sonarr-v4-custom-formats-web-2160p + + # Custom Formats: https://recyclarr.dev/wiki/yaml/config-reference/custom-formats/ + custom_formats: [] + +radarr: + instance1: + define: true diff --git a/custom/cfs/custom-size-bigger-40gb.json b/custom/cfs/custom-size-bigger-40gb.json new file mode 100644 index 0000000..aedadd0 --- /dev/null +++ b/custom/cfs/custom-size-bigger-40gb.json @@ -0,0 +1,22 @@ +{ + "trash_id": "custom-size-more-40gb", + "trash_scores": { + "default": -10000 + }, + "trash_description": "Size: Block sizes over 40GB", + "custom_inf": "Does not work because SizeSpecification is not supported by recyclarr", + "name": "Size: Block More 40GB", + "includeCustomFormatWhenRenaming": false, + "specifications": [ + { + "name": "Size", + "implementation": "SizeSpecification", + "negate": false, + "required": true, + "fields": { + "min": 1, + "max": 9 + } + } + ] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..158d7b0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + configarr: + image: configarr:test + build: . + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + volumes: + - ./config:/app/config + - ./dockerrepos:/app/repos + - ./custom/cfs:/app/cfs + + sonarr: + image: lscr.io/linuxserver/sonarr:4.0.2 + container_name: sonarr + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + volumes: + - sonarr:/config + ports: + - 8989:8989 + restart: unless-stopped + + radarr: + image: lscr.io/linuxserver/radarr:5.3.6 + container_name: radarr + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + volumes: + - radarr:/config + ports: + - 7878:7878 + restart: unless-stopped + +volumes: + sonarr: + radarr: diff --git a/index.js b/generate-api.js similarity index 52% rename from index.js rename to generate-api.js index db17358..d9364c8 100644 --- a/index.js +++ b/generate-api.js @@ -1,15 +1,28 @@ -const { generateApi, generateTemplates } = require("swagger-typescript-api"); -const path = require("path"); -const fs = require("fs"); +import fs from "fs"; +import path from "path"; +import { generateApi } from "swagger-typescript-api"; const PATH_TO_OUTPUT_DIR = path.resolve(process.cwd(), "./src/__generated__"); /* NOTE: all fields are optional expect one of `input`, `url`, `spec` */ generateApi({ - name: "MySuperbApi.ts", - output: path.resolve(process.cwd(), "./src/__generated__"), + name: "generated-sonarr-api.ts", + output: PATH_TO_OUTPUT_DIR, url: "https://raw.githubusercontent.com/Sonarr/Sonarr/develop/src/Sonarr.Api.V3/openapi.json", - httpClientType: "fetch", + httpClientType: "axios", +}) + .then(({ files, configuration }) => { + files.forEach(({ content, name }) => { + fs.writeFile(path, content); + }); + }) + .catch((e) => console.error(e)); + +generateApi({ + name: "generated-radarr-api.ts", + output: PATH_TO_OUTPUT_DIR, + url: "https://raw.githubusercontent.com/Radarr/Radarr/develop/src/Radarr.Api.V3/openapi.json", + httpClientType: "axios", }) .then(({ files, configuration }) => { files.forEach(({ content, name }) => { diff --git a/index.ts b/index.ts index beb5f33..de5b8e0 100644 --- a/index.ts +++ b/index.ts @@ -1,770 +1,280 @@ import "dotenv/config"; -import { readFileSync, readdirSync } from "fs"; -import path from "path"; -import simpleGit from "simple-git"; -import { parse } from "yaml"; +import fs from "fs"; +import { CustomFormatResource } from "./src/__generated__/generated-sonarr-api"; +import { configureRadarrApi, configureSonarrApi, getArrApi, unsetApi } from "./src/api"; +import { getConfig } from "./src/config"; +import { calculateCFsToManage, loadLocalCfs, loadServerCustomFormats, manageCf, mergeCfSources } from "./src/custom-formats"; +import { calculateQualityDefinitionDiff, loadQualityDefinitionFromServer } from "./src/quality-definitions"; import { - Api, - CustomFormatResource, - CustomFormatSpecificationSchema, - Field, - PrivacyLevel, -} from "./src/__generated__/MySuperbApi"; - -const api = new Api({ - // headers: { - // "X-Api-Key": process.env.SONARR_API_KEY!, - // }, - // url: process.env.SONARR_URL!, - // baseURL: process.env.SONARR_URL!, - baseUrl: process.env.SONARR_URL!, - baseApiParams: { - headers: { - "X-Api-Key": process.env.SONARR_API_KEY!, - }, - }, -}); - -type DynamicImportType = { default: T }; -type TrashCFSpF = { min: number; max: number }; - -type TC1 = Omit & { - implementation: "ReleaseTitleSpecification" | "LanguageSpecification"; - fields?: Field | null; -}; - -type TC2 = Omit & { - implementation: "SizeSpecification"; - fields?: TrashCFSpF; -}; - -type TCM = TC1 | TC2; - -type TrashCFResource = Omit & { - specifications?: TCM[] | null; -}; - -type TrashCF = { - trash_id: string; - trash_scores?: { - default: number; - }; - trash_regex?: string; -} & TrashCFResource; - -const ROOT_PATH = path.resolve(process.cwd()); - -// const getObjectDiff = (obj1, obj2, compareRef = false) => { -// return Object.keys(obj1).reduce((result, key) => { -// if (!obj2.hasOwnProperty(key)) { -// result.push(key); -// } else if (_.isEqual(obj1[key], obj2[key])) { -// const resultKeyIndex = result.indexOf(key); - -// if (compareRef && obj1[key] !== obj2[key]) { -// result[resultKeyIndex] = `${key} (ref)`; -// } else { -// result.splice(resultKeyIndex, 1); -// } -// } -// return result; -// }, Object.keys(obj2)); -// }; - -function differenceInObj(firstObj: any, secondObj: any): any { - let differenceObj: any = {}; - for (const key in firstObj) { - if (Object.prototype.hasOwnProperty.call(firstObj, key)) { - if (firstObj[key] !== secondObj[key]) { - differenceObj[key] = firstObj[key]; - } - } - } - - return differenceObj; -} - -const testGo = async () => { - const object2: TrashCF = { - trash_id: "eb3d5cc0a2be0db205fb823640db6a3c", - trash_scores: { - default: 6, - }, - name: "Repack v2", - includeCustomFormatWhenRenaming: false, - specifications: [ - { - name: "Repack v2", - implementation: "ReleaseTitleSpecification", - negate: false, - required: false, - fields: { - value: "\\b(repack2)\\b", - }, - }, - { - name: "Proper v2", - implementation: "ReleaseTitleSpecification", - negate: false, - required: false, - fields: { - value: "\\b(proper2)\\b", - }, - }, - ], - }; - - const object1 = { - id: 7, - name: "Repack v2", - includeCustomFormatWhenRenaming: false, - specifications: [ - { - name: "Repack v2", - implementation: "ReleaseTitleSpecification", - implementationName: "Release Title", - infoLink: "https://wiki.servarr.com/sonarr/settings#custom-formats-2", - negate: false, - required: false, - fields: [ - { - order: 0, - name: "value", - label: "Regular Expression", - helpText: "Custom Format RegEx is Case Insensitive", - value: "\\b(repack2)\\b", - type: "textbox", - advanced: false, - privacy: PrivacyLevel.Normal, - isFloat: false, - }, - ], - }, - { - name: "Proper v2", - implementation: "ReleaseTitleSpecification", - implementationName: "Release Title", - infoLink: "https://wiki.servarr.com/sonarr/settings#custom-formats-2", - negate: false, - required: false, - fields: [ - { - order: 0, - name: "value", - label: "Regular Expression", - helpText: "Custom Format RegEx is Case Insensitive", - value: "\\b(proper2)\\b", - type: "textbox", - advanced: false, - privacy: PrivacyLevel.Normal, - isFloat: false, - }, - ], - }, - ], + calculateQualityProfilesDiff, + filterInvalidQualityProfiles, + loadQualityProfilesFromServer, + mapQualityProfiles, +} from "./src/quality-profiles"; +import { cloneRecyclarrTemplateRepo, loadRecyclarrTemplates } from "./src/recyclarr-importer"; +import { cloneTrashRepo, loadQualityDefinitionSonarrFromTrash, loadSonarrTrashCFs } from "./src/trash-guide"; +import { ArrType, RecyclarrMergedTemplates, TrashQualityDefintion, YamlConfigInstance, YamlConfigQualityProfile } from "./src/types"; +import { DEBUG_CREATE_FILES, IS_DRY_RUN } from "./src/util"; + +const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => { + const api = getArrApi(); + const recyclarrTemplateMap = loadRecyclarrTemplates(arrType); + + const recylarrMergedTemplates: RecyclarrMergedTemplates = { + custom_formats: [], + quality_profiles: [], }; - console.log(differenceInObj(object2, object1)); + if (value.include) { + console.log(`Recyclarr Includes:\n${value.include.map((e) => e.template).join("\n")}`); + value.include.forEach((e) => { + const template = recyclarrTemplateMap.get(e.template); - console.log(compareObjects2(object1, object2)); // Output: true -}; - -const trashCfToValidCf = (trashCf: TrashCF): CustomFormatResource => { - const { trash_id, trash_regex, trash_scores, ...rest } = trashCf; - - if (!rest.specifications) { - console.log(`TrashCF is wrong ${trash_id}, ${rest.name}.`); - throw new Error("TrashCF wrong"); - } - - const specs = rest.specifications.map((spec) => { - const newFields: Field[] = []; + if (!template) { + console.log(`Unknown recyclarr template requested: ${e.template}`); + return; + } - if (!spec.fields) { - console.log(`Spec: ${spec.name} fields is not defined`); - throw new Error(`Spec is not correctly defined: ${spec.name}`); - } + if (template.custom_formats) { + recylarrMergedTemplates.custom_formats?.push(...template.custom_formats); + } - switch (spec.implementation) { - case "SizeSpecification": - newFields.push({ name: "min", value: spec.fields.min }); - newFields.push({ name: "max", value: spec.fields.max }); - break; - case "ReleaseTitleSpecification": - case "LanguageSpecification": - default: - newFields.push({ value: spec.fields.value }); - break; - } + if (template.quality_definition) { + recylarrMergedTemplates.quality_definition = template.quality_definition; + } - return { - ...spec, - fields: newFields.map((f) => { - if (f.name) { - return f; + if (template.quality_profiles) { + for (const qp of template.quality_profiles) { + recylarrMergedTemplates.quality_profiles.push(qp); } + } - return { name: "value", ...f }; - }), - }; - }); - - return { ...rest, specifications: specs }; -}; - -function compareObjects( - fullObject: CustomFormatResource, - partialObject: Partial -): boolean { - if (!partialObject.name || !partialObject.specifications) { - return false; + // TODO Ignore recursive include for now + }); } - if ( - fullObject.name !== partialObject.name || - fullObject.includeCustomFormatWhenRenaming !== - partialObject.includeCustomFormatWhenRenaming - ) { - return false; + if (value.custom_formats) { + recylarrMergedTemplates.custom_formats.push(...value.custom_formats); } - if (!fullObject.specifications || !partialObject.specifications) { - // TODO not sure - return false; + if (value.quality_profiles) { + recylarrMergedTemplates.quality_profiles.push(...value.quality_profiles); } - if ( - fullObject.specifications && - fullObject.specifications.length !== partialObject.specifications.length - ) { - return false; - } + // TODO "real" merge missing of profiles? - for (let i = 0; i < fullObject.specifications.length; i++) { - const fullSpec = fullObject.specifications[i]; - const partialSpec = partialObject.specifications.find( - (spec) => spec.name === fullSpec.name - ); + recylarrMergedTemplates.quality_profiles = filterInvalidQualityProfiles(recylarrMergedTemplates.quality_profiles); - if (!partialSpec) { - return false; - } + const result = await loadLocalCfs(); + const trashCFs = await loadSonarrTrashCFs(arrType); + const mergedCFs = mergeCfSources([trashCFs, result]); - if (!fullSpec.fields || !partialSpec.fields) { - // TODO not sure - return false; - } + const idsToManage = calculateCFsToManage(recylarrMergedTemplates); - if ( - typeof fullSpec.fields === "object" && - !Array.isArray(fullSpec.fields) - ) { - // Assume single object as array with single element - if ( - !Array.isArray(partialSpec.fields) || - partialSpec.fields.length !== 1 - ) { - return false; - } + console.log(`Stuff to manage: ${Array.from(idsToManage)}`); - const fullFieldValue = (fullSpec.fields as { value: string }).value; - const partialFieldValue = (partialSpec.fields[0] as Field).value; + const serverCFs = await loadServerCustomFormats(); + console.log(`CFs on server: ${serverCFs.length}`); - if (fullFieldValue !== partialFieldValue) { - return false; - } - } else { - if ( - !Array.isArray(fullSpec.fields) || - !Array.isArray(partialSpec.fields) || - fullSpec.fields.length !== partialSpec.fields.length - ) { - return false; - } + const serverCFMapping = serverCFs.reduce((p, c) => { + p.set(c.name!, c); + return p; + }, new Map()); - for (let j = 0; j < fullSpec.fields.length; j++) { - const fullField = fullSpec.fields[j]; - const partialField = partialSpec.fields.find( - (field) => field.name === fullField.name - ); + await manageCf(mergedCFs, serverCFMapping, idsToManage); + console.log(`CustomFormats should be in sync`); - if (!partialField || partialField.value !== fullField.value) { - return false; - } - } - } - } + const qualityDefinition = recylarrMergedTemplates.quality_definition?.type; - return true; -} - -function compareObjects2( - object1: CustomFormatResource, - object2: Partial -): { equal: boolean; changes: string[] } { - const changes: string[] = []; - - for (const key in object2) { - if (Object.prototype.hasOwnProperty.call(object2, key)) { - if (object1.hasOwnProperty(key)) { - const value1 = object1[key]; - let value2 = object2[key]; - - if (key === "fields") { - if (!Array.isArray(value2)) { - value2 = [value2]; - } - } + if (qualityDefinition) { + const qdSonarr = await loadQualityDefinitionFromServer(); + let qdTrash: TrashQualityDefintion; - if (Array.isArray(value1)) { - if (!Array.isArray(value2)) { - changes.push(`Expected array for key ${key} in object2`); - continue; - } - - if (value1.length !== value2.length) { - changes.push( - `Array length mismatch for key ${key}: object1 length ${value1.length}, object2 length ${value2.length}` - ); - continue; - } - - for (let i = 0; i < value1.length; i++) { - const { equal: isEqual, changes: subChanges } = compareObjects2( - value1[i], - value2[i] - ); - // changes.push( - // ...subChanges.map((subChange) => `${key}[${i}].${subChange}`) - // ); - - if (subChanges.length > 0) { - changes.push(`${key}[${i}].${subChanges[0]}`); - } - - if (!isEqual && changes.length <= 0) { - changes.push( - `Mismatch found in array element at index ${i} for key ${key}` - ); - } - } - } else if (typeof value1 === "object" && value1 !== null) { - if (typeof value2 !== "object" || value2 === null) { - changes.push(`Expected object for key ${key} in object2`); - continue; - } - - const { equal: isEqual, changes: subChanges } = compareObjects2( - value1, - value2 - ); - changes.push(...subChanges.map((subChange) => `${key}.${subChange}`)); - if (!isEqual) { - changes.push(`Mismatch found for key ${key}`); - } - } else { - if (value1 !== value2) { - changes.push( - `Mismatch found for key ${key}: server value ${value1}, value to set ${value2}` - ); - } - } - } else { - } + switch (qualityDefinition) { + case "anime": + qdTrash = await loadQualityDefinitionSonarrFromTrash("anime", "SONARR"); + break; + case "series": + qdTrash = await loadQualityDefinitionSonarrFromTrash("series", "SONARR"); + break; + case "movie": + qdTrash = await loadQualityDefinitionSonarrFromTrash("movie", "RADARR"); + break; + default: + throw new Error(`Unsupported quality defintion ${qualityDefinition}`); } - } - - const equal = changes.length === 0; - return { equal, changes }; -} -function compareObjects3( - object1: CustomFormatResource, - trashCf: TrashCF -): { equal: boolean; changes: string[] } { - const changes: string[] = []; - - const object2 = trashCfToValidCf(trashCf); - - for (const key in object2) { - if (Object.prototype.hasOwnProperty.call(object2, key)) { - if (object1.hasOwnProperty(key)) { - const value1 = object1[key]; - let value2 = object2[key]; - - if (key === "fields") { - if (!Array.isArray(value2)) { - value2 = [value2]; - } - } + const { changeMap, create, restData } = calculateQualityDefinitionDiff(qdSonarr, qdTrash); - if (Array.isArray(value1)) { - if (!Array.isArray(value2)) { - changes.push(`Expected array for key ${key} in object2`); - continue; - } - - if (value1.length !== value2.length) { - changes.push( - `Array length mismatch for key ${key}: object1 length ${value1.length}, object2 length ${value2.length}` - ); - continue; - } - - for (let i = 0; i < value1.length; i++) { - const { equal: isEqual, changes: subChanges } = compareObjects2( - value1[i], - value2[i] - ); - // changes.push( - // ...subChanges.map((subChange) => `${key}[${i}].${subChange}`) - // ); - - if (subChanges.length > 0) { - changes.push(`${key}[${i}].${subChanges[0]}`); - } - - if (!isEqual && changes.length <= 0) { - changes.push( - `Mismatch found in array element at index ${i} for key ${key}` - ); - } - } - } else if (typeof value1 === "object" && value1 !== null) { - if (typeof value2 !== "object" || value2 === null) { - changes.push(`Expected object for key ${key} in object2`); - continue; - } - - const { equal: isEqual, changes: subChanges } = compareObjects2( - value1, - value2 - ); - changes.push(...subChanges.map((subChange) => `${key}.${subChange}`)); - if (!isEqual) { - changes.push(`Mismatch found for key ${key}`); - } - } else { - if (value1 !== value2) { - changes.push( - `Mismatch found for key ${key}: server value ${value1}, value to set ${value2}` - ); - } - } + if (changeMap.size > 0) { + if (IS_DRY_RUN) { + console.log("DryRun: Would update QualityDefinitions."); } else { + console.log(`Diffs in quality definitions found`, changeMap.values()); + await api.v3QualitydefinitionUpdateUpdate(restData as any); // Ignore types + console.log(`Updated QualityDefinitions`); } + } else { + console.log(`QualityDefinitions do not need update!`); } - } - - const equal = changes.length === 0; - return { equal, changes }; -} - -type YamlList = { - trash_ids?: string[]; - quality_profiles: { name: string }[]; -}; - -type YamlInput = { - custom_formats: YamlList[]; -}; - -const loadYamlFile = () => { - const PATH_TO_OUTPUT_DIR = path.resolve(process.cwd(), "."); - - const file = readFileSync(`${PATH_TO_OUTPUT_DIR}/input.yml`, "utf8"); - const yamlRes = parse(file) as YamlInput; - - console.log(yamlRes); - - return yamlRes; -}; - -const gitStuff = async () => { - const trashRepoPath = "./repos/trash-guides"; - - const gitClient = simpleGit(trashRepoPath); - const r = await gitClient.checkIsRepo(); - if (r) { - await gitClient.pull(); - } else { - await simpleGit().clone( - "https://github.com/BlackDark/fork-TRASH-Guides", - "." - ); + if (create.length > 0) { + console.log(`Currently not implemented this case for quality definitions.`); + } } - console.log(r); -}; - -const go = async () => { - const yamlStuff = loadYamlFile(); + // merge CFs of templates and custom CFs into one mapping of QualityProfile -> CFs + Score + // TODO traversing the merged templates probably to often once should be enough. Loop once and extract a couple of different maps, arrays as needed. Future optimization. + const cfToQualityProfiles = mapQualityProfiles(mergedCFs, recylarrMergedTemplates.custom_formats, recylarrMergedTemplates); - const cfTrashToManage: Set = new Set(); + // merge profiles from recyclarr templates into one + const qualityProfilesMerged = recylarrMergedTemplates.quality_profiles.reduce((p, c) => { + let existingQp = p.get(c.name); - yamlStuff.custom_formats.map((cf) => { - if (cf.trash_ids) { - cf.trash_ids.forEach((tid) => cfTrashToManage.add(tid)); + if (!existingQp) { + p.set(c.name, { ...c }); + } else { + existingQp = { + ...existingQp, + ...c, + // Overwriting qualities array for now + upgrade: { ...existingQp.upgrade, ...c.upgrade }, + reset_unmatched_scores: { + ...existingQp.reset_unmatched_scores, + ...c.reset_unmatched_scores, + enabled: (c.reset_unmatched_scores?.enabled ?? existingQp.reset_unmatched_scores?.enabled) || false, + }, + }; + p.set(c.name, existingQp); } - }); - - const trashRepoPath = "./repos/trash-guides"; - const trashJsonDir = "docs/json"; - const trashRadarrPath = `${trashJsonDir}/radarr`; - const trashRadarrCfPath = `${trashRadarrPath}/cf`; - const trashSonarrPath = `${trashJsonDir}/sonarr`; - const trashSonarrCfPath = `${trashSonarrPath}/cf`; + return p; + }, new Map()); - const files = readdirSync(`${trashRepoPath}/${trashSonarrCfPath}`).filter( - (fn) => fn.endsWith("json") - ); + // calculate diff from server <-> what we want to be there - const trashIdToObject = new Map< - string, - { trashConfig: TrashCF; requestConfig: CustomFormatResource } - >(); - const cfNameToTrashId = new Map(); + const qpServer = await loadQualityProfilesFromServer(); - for (const file of files) { - const name = `${trashRepoPath}/${trashSonarrCfPath}/${file}`; - const cf: DynamicImportType = await import(`${ROOT_PATH}/${name}`); + const { changedQPs, create, noChanges } = await calculateQualityProfilesDiff( + mergedCFs, + qualityProfilesMerged, + cfToQualityProfiles, + qpServer, + ); - trashIdToObject.set(cf.default.trash_id, { - trashConfig: cf.default, - requestConfig: trashCfToValidCf(cf.default), + if (DEBUG_CREATE_FILES) { + create.concat(changedQPs).forEach((e, i) => { + fs.writeFileSync(`test${i}.json`, JSON.stringify(e, null, 2), "utf-8"); }); - - if (cf.default.name) { - cfNameToTrashId.set(cf.default.name, cf.default.trash_id); - } } - console.log(`Trash CFs: ${trashIdToObject.size}`); - - const cfOnServer = await api.api.v3CustomformatList(); - - console.log(`CFs on server: ${cfOnServer.data.length}`); - - const existingCfToObject = new Map(); - const unknownCfToObject = new Map(); - - for (const cfServer of cfOnServer.data) { - if (!cfServer.name) { - console.log("CF without name found", cfServer); - continue; - } - if (cfNameToTrashId.has(cfServer.name)) { - existingCfToObject.set(cfServer.name, cfServer); - } else { - unknownCfToObject.set(cfServer.name, cfServer); - } - } + console.log(`QPs: Create: ${create.length}, Update: ${changedQPs.length}, Unchanged: ${noChanges.length}`); - const manageCf = async (trashId: string) => { - const tr = trashIdToObject.get(trashId); + if (!IS_DRY_RUN) { + for (const element of create) { + try { + const newProfile = await api.v3QualityprofileCreate(element as any); // Ignore types + console.log(`Created QualityProfile: ${newProfile.data.name}`); + } catch (error: any) { + let message; + + if (error.response) { + console.log(error.response); + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + message = `Failed creating QualityProfile (${element.name}): Data ${JSON.stringify(error.response.data)}`; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + console.log(error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.log("Error", error.message); + } - if (!tr) { - console.log(`TrashID to manage ${trashId} does not exists`); - return; + throw new Error(message); + } } - const existingCf = existingCfToObject.get(tr.trashConfig.name!); - - if (existingCf) { - // Update if necessary - const comparison = compareObjects3(existingCf, tr.trashConfig); - - if (!comparison.equal) { - console.log( - `Found mismatch for ${tr.requestConfig.name}.`, - comparison.changes - ); - - try { - const updateResult = await api.api.v3CustomformatUpdate( - existingCf.id, - { - id: existingCf.id, - ...tr.requestConfig, - } - ); - - console.log(`Updated CF ${tr.requestConfig.name}`); - } catch (err) { - console.log(`Failed updating CF ${tr.requestConfig.name}`, err.error); + for (const element of changedQPs) { + try { + const newProfile = await api.v3QualityprofileUpdate("" + element.id, element as any); // Ignore types + console.log(`Updated QualityProfile: ${newProfile.data.name}`); + } catch (error: any) { + let message; + + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + message = `Failed updating QualityProfile (${element.name}): Data ${JSON.stringify(error.response.data)}`; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + console.log(error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.log("Error", error.message); } - } else { - console.log(`CF ${tr.requestConfig.name} does not need update.`); - } - } else { - // Create - try { - console.log(JSON.stringify(tr.requestConfig)); - const createResult = await api.api.v3CustomformatCreate( - tr.requestConfig - ); - - console.log(`Created CF ${tr.requestConfig.name}`); - } catch (err) { - console.log(`Failed creating CF ${tr.requestConfig.name}`, err.error); + throw new Error(message); } } - }; - // console.log( - // `Matching CF names found on server: ${existingCfToObject.size}`, - // existingCfToObject.keys() - // ); - - for (const manage of cfTrashToManage) { - console.log(`Manage ${manage}`); - - await manageCf(manage); } + /* + - load trash + - load custom resources + - merge stuff together to see what actually needs to be done + - create/update CFs + - future: somehow track managed CFs? + - create/update quality profiles + - future: quality definitions + */ +}; - // for (const [key, value] of existingCfToObject.entries()) { - // const trashThing = trashIdToObject.get(cfNameToTrashId.get(key)!)!; - - // const comparison = compareObjects2(value, trashThing.trashConfig); - - // if (!comparison.equal) { - // console.log(`Found mismatch for ${key}.`, comparison.changes); - // console.log(JSON.stringify(trashThing.requestConfig)); - // console.log(JSON.stringify(trashThing.trashConfig)); - // console.log(JSON.stringify(value)); - - // try { - // const updateResult = await api.api.v3CustomformatUpdate(value.id, { - // id: value.id, - // ...trashThing.requestConfig, - // }); - - // console.log(updateResult.data); - // } catch (err) { - // console.log(err.error); - // } - // } - // } - - console.log( - `Unknown CF names found on server: ${unknownCfToObject.size}`, - unknownCfToObject.keys() - ); - - return; - - const PATH_TO_OUTPUT_DIR = path.resolve(process.cwd(), "."); - - const file = readFileSync(`${PATH_TO_OUTPUT_DIR}/input.yml`, "utf8"); - const yamlRes = parse(file); +const run = async () => { + const applicationConfig = getConfig(); - console.log(yamlRes); + await cloneRecyclarrTemplateRepo(); + await cloneTrashRepo(); - return; + // TODO currently this has to be run sequentially because of the centrally configured api - const result = await api.api.v3CustomformatList(); - console.log(result.data); + for (const instanceName in applicationConfig.sonarr) { + const instance = applicationConfig.sonarr[instanceName]; + console.log(`Processing Sonarr Instance: ${instanceName}`); + await configureSonarrApi(instance.base_url, instance.api_key); + await pipeline(instance, "SONARR"); + unsetApi(); + } - // const test = await api.api.v3CustomformatCreate({ - // name: "Test this", - // specifications: [ - // { - // implementation: "SizeSpecification", - // name: "Hello", - // fields: [ - // { name: "min", value: 15 }, - // { name: "max", value: 40 }, - // ], - // }, - // ], - // }); -}; + if ( + typeof applicationConfig.sonarr === "object" && + !Array.isArray(applicationConfig.sonarr) && + applicationConfig.sonarr !== null && + Object.keys(applicationConfig.sonarr).length <= 0 + ) { + console.log(`No sonarr instances defined.`); + } -go(); -//testGo(); - -const object22: TrashCF = { - trash_id: "eb3d5cc0a2be0db205fb823640db6a3c", - trash_scores: { - default: 6, - }, - name: "Repack v2", - includeCustomFormatWhenRenaming: false, - specifications: [ - { - name: "Repack v2", - implementation: "ReleaseTitleSpecification", - negate: false, - required: false, - fields: { - value: "\\b(repack22)\\b", - }, - }, - { - name: "Proper v2", - implementation: "ReleaseTitleSpecification", - negate: false, - required: false, - fields: { - value: "\\b(proper2)\\b", - }, - }, - ], -}; + for (const instanceName in applicationConfig.radarr) { + console.log(`Processing Radarr instance: ${instanceName}`); + const instance = applicationConfig.radarr[instanceName]; + await configureRadarrApi(instance.base_url, instance.api_key); + await pipeline(instance, "RADARR"); + unsetApi(); + } -const object11 = { - id: 7, - name: "Repack v2", - includeCustomFormatWhenRenaming: false, - specifications: [ - { - name: "Repack v2", - implementation: "ReleaseTitleSpecification", - implementationName: "Release Title", - infoLink: "https://wiki.servarr.com/sonarr/settings#custom-formats-2", - negate: false, - required: false, - fields: [ - { - order: 0, - name: "value", - label: "Regular Expression", - helpText: "Custom Format RegEx is Case Insensitive", - value: "\\b(repack2)\\b", - type: "textbox", - advanced: false, - privacy: PrivacyLevel.Normal, - isFloat: false, - }, - ], - }, - { - name: "Proper v2", - implementation: "ReleaseTitleSpecification", - implementationName: "Release Title", - infoLink: "https://wiki.servarr.com/sonarr/settings#custom-formats-2", - negate: false, - required: false, - fields: [ - { - order: 0, - name: "value", - label: "Regular Expression", - helpText: "Custom Format RegEx is Case Insensitive", - value: "\\b(proper2)\\b", - type: "textbox", - advanced: false, - privacy: PrivacyLevel.Normal, - isFloat: false, - }, - ], - }, - ], + if ( + typeof applicationConfig.radarr === "object" && + !Array.isArray(applicationConfig.radarr) && + applicationConfig.radarr !== null && + Object.keys(applicationConfig.radarr).length <= 0 + ) { + console.log(`No radarr instances defined.`); + } }; -const comparisonResult2 = compareObjects2(object11, object22); -console.log("Objects are equal:", comparisonResult2.equal); -console.log("Changes:", comparisonResult2.changes); +run(); diff --git a/package.json b/package.json index f861f95..a013658 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { - "name": "swagger-ts-gen", - "version": "1.0.0", + "name": "configarr", + "version": "0.0.0", "description": "", - "main": "index.js", + "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "coverage": "vitest run --coverage", + "generateApi": "tsx generate-api.js", + "start": "tsx index.ts", + "test": "vitest", "tsg": "swagger-to-ts -f input.json -o test.ts", - "go": "tsx index.ts" + "type-check": "tsc --noEmit" }, - "keywords": [], "author": "", "license": "ISC", "dependencies": { @@ -20,5 +22,13 @@ "swagger-typescript-api": "^13.0.3", "tsx": "^4.7.1", "yaml": "^2.4.1" + }, + "devDependencies": { + "@playwright/test": "^1.42.1", + "@types/node": "^20.11.30", + "@vitest/coverage-v8": "^1.4.0", + "prettier": "3.2.5", + "typescript": "5.4.3", + "vitest": "1.4.0" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..2fa5812 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests/e2e", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c6971e..f3a89b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: dependencies: '@infinite-debugger/swagger-to-ts': specifier: 0.1.0-alpha.124 - version: 0.1.0-alpha.124(@types/node@20.11.30)(crypto-js@4.2.0)(date-fns@3.6.0)(typescript@5.4.2) + version: 0.1.0-alpha.124(@types/node@20.11.30)(crypto-js@4.2.0)(date-fns@3.6.0)(typescript@5.4.3) axios: specifier: ^1.6.8 version: 1.6.8 @@ -30,6 +30,26 @@ dependencies: specifier: ^2.4.1 version: 2.4.1 +devDependencies: + '@playwright/test': + specifier: ^1.42.1 + version: 1.42.1 + '@types/node': + specifier: ^20.11.30 + version: 20.11.30 + '@vitest/coverage-v8': + specifier: ^1.4.0 + version: 1.4.0(vitest@1.4.0) + prettier: + specifier: 3.2.5 + version: 3.2.5 + typescript: + specifier: 5.4.3 + version: 5.4.3 + vitest: + specifier: 1.4.0 + version: 1.4.0(@types/node@20.11.30) + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -37,6 +57,14 @@ packages: engines: {node: '>=0.10.0'} dev: false + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + /@babel/code-frame@7.24.2: resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} engines: {node: '>=6.9.0'} @@ -45,10 +73,14 @@ packages: picocolors: 1.0.0 dev: false + /@babel/helper-string-parser@7.24.1: + resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} - dev: false /@babel/highlight@7.24.2: resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} @@ -60,6 +92,27 @@ packages: picocolors: 1.0.0 dev: false + /@babel/parser@7.24.1: + resolution: {integrity: sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.0 + dev: true + + /@babel/types@7.24.0: + resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.1 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -76,6 +129,15 @@ packages: dev: false optional: true + /@esbuild/aix-ppc64@0.20.2: + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.19.12: resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} engines: {node: '>=12'} @@ -85,6 +147,15 @@ packages: dev: false optional: true + /@esbuild/android-arm64@0.20.2: + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.19.12: resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} engines: {node: '>=12'} @@ -94,6 +165,15 @@ packages: dev: false optional: true + /@esbuild/android-arm@0.20.2: + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.19.12: resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} engines: {node: '>=12'} @@ -103,6 +183,15 @@ packages: dev: false optional: true + /@esbuild/android-x64@0.20.2: + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.19.12: resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} engines: {node: '>=12'} @@ -112,6 +201,15 @@ packages: dev: false optional: true + /@esbuild/darwin-arm64@0.20.2: + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.19.12: resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} engines: {node: '>=12'} @@ -121,6 +219,15 @@ packages: dev: false optional: true + /@esbuild/darwin-x64@0.20.2: + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.19.12: resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} engines: {node: '>=12'} @@ -130,6 +237,15 @@ packages: dev: false optional: true + /@esbuild/freebsd-arm64@0.20.2: + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.19.12: resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} engines: {node: '>=12'} @@ -139,6 +255,15 @@ packages: dev: false optional: true + /@esbuild/freebsd-x64@0.20.2: + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.19.12: resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} engines: {node: '>=12'} @@ -148,6 +273,15 @@ packages: dev: false optional: true + /@esbuild/linux-arm64@0.20.2: + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.19.12: resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} engines: {node: '>=12'} @@ -157,6 +291,15 @@ packages: dev: false optional: true + /@esbuild/linux-arm@0.20.2: + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.19.12: resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} engines: {node: '>=12'} @@ -166,6 +309,15 @@ packages: dev: false optional: true + /@esbuild/linux-ia32@0.20.2: + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.19.12: resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} engines: {node: '>=12'} @@ -175,6 +327,15 @@ packages: dev: false optional: true + /@esbuild/linux-loong64@0.20.2: + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.19.12: resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} engines: {node: '>=12'} @@ -184,6 +345,15 @@ packages: dev: false optional: true + /@esbuild/linux-mips64el@0.20.2: + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.19.12: resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} engines: {node: '>=12'} @@ -193,6 +363,15 @@ packages: dev: false optional: true + /@esbuild/linux-ppc64@0.20.2: + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.19.12: resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} engines: {node: '>=12'} @@ -202,6 +381,15 @@ packages: dev: false optional: true + /@esbuild/linux-riscv64@0.20.2: + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.19.12: resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} engines: {node: '>=12'} @@ -211,6 +399,15 @@ packages: dev: false optional: true + /@esbuild/linux-s390x@0.20.2: + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.19.12: resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} engines: {node: '>=12'} @@ -220,6 +417,15 @@ packages: dev: false optional: true + /@esbuild/linux-x64@0.20.2: + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.19.12: resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} engines: {node: '>=12'} @@ -229,6 +435,15 @@ packages: dev: false optional: true + /@esbuild/netbsd-x64@0.20.2: + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.19.12: resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} engines: {node: '>=12'} @@ -238,6 +453,15 @@ packages: dev: false optional: true + /@esbuild/openbsd-x64@0.20.2: + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.19.12: resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} @@ -247,6 +471,15 @@ packages: dev: false optional: true + /@esbuild/sunos-x64@0.20.2: + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.19.12: resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} engines: {node: '>=12'} @@ -256,6 +489,15 @@ packages: dev: false optional: true + /@esbuild/win32-arm64@0.20.2: + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.19.12: resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} engines: {node: '>=12'} @@ -265,6 +507,15 @@ packages: dev: false optional: true + /@esbuild/win32-ia32@0.20.2: + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.19.12: resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} engines: {node: '>=12'} @@ -274,6 +525,15 @@ packages: dev: false optional: true + /@esbuild/win32-x64@0.20.2: + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -351,20 +611,20 @@ packages: lodash: 4.17.21 dev: false - /@infinite-debugger/swagger-to-ts@0.1.0-alpha.124(@types/node@20.11.30)(crypto-js@4.2.0)(date-fns@3.6.0)(typescript@5.4.2): + /@infinite-debugger/swagger-to-ts@0.1.0-alpha.124(@types/node@20.11.30)(crypto-js@4.2.0)(date-fns@3.6.0)(typescript@5.4.3): resolution: {integrity: sha512-fKnvRUaTL1rUX094QP7Bm6O0oqL/DCjHA46fnWqyq7H/dgU1QxxlqWQB0UBpa4AkuTHeIu4iNJQPGB0k4y74cw==} hasBin: true dependencies: '@infinite-debugger/rmk-js-extensions': 0.1.0-alpha.28 '@infinite-debugger/rmk-utils': 0.1.0-alpha.29(crypto-js@4.2.0)(date-fns@3.6.0)(lodash@4.17.21) - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.3) eslint: 8.57.0 fs-extra: 11.2.0 lodash: 4.17.21 pluralize: 8.0.0 prettier: 2.8.8 - ts-node: 10.9.2(@types/node@20.11.30)(typescript@5.4.2) + ts-node: 10.9.2(@types/node@20.11.30)(typescript@5.4.3) walk-sync: 3.0.0 zod: 3.22.4 transitivePeerDependencies: @@ -377,14 +637,45 @@ packages: - typescript dev: false + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + /@jridgewell/resolve-uri@3.1.2: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - dev: false + + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: true /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: false + + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -426,12 +717,136 @@ packages: fastq: 1.17.1 dev: false + /@playwright/test@1.42.1: + resolution: {integrity: sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.42.1 + dev: true + /@rauschma/stringio@1.4.0: resolution: {integrity: sha512-3uor2f/MXZkmX5RJf8r+OC3WvZVzpSme0yyL0rQDPEnatE02qRcqwEwnsgpgriEck0S/n4vWtUd6tTtrJwk45Q==} dependencies: '@types/node': 10.17.60 dev: false + /@rollup/rollup-android-arm-eabi@4.13.1: + resolution: {integrity: sha512-4C4UERETjXpC4WpBXDbkgNVgHyWfG3B/NKY46e7w5H134UDOFqUJKpsLm0UYmuupW+aJmRgeScrDNfvZ5WV80A==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.13.1: + resolution: {integrity: sha512-TrTaFJ9pXgfXEiJKQ3yQRelpQFqgRzVR9it8DbeRzG0RX7mKUy0bqhCFsgevwXLJepQKTnLl95TnPGf9T9AMOA==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.13.1: + resolution: {integrity: sha512-fz7jN6ahTI3cKzDO2otQuybts5cyu0feymg0bjvYCBrZQ8tSgE8pc0sSNEuGvifrQJWiwx9F05BowihmLxeQKw==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.13.1: + resolution: {integrity: sha512-WTvdz7SLMlJpektdrnWRUN9C0N2qNHwNbWpNo0a3Tod3gb9leX+yrYdCeB7VV36OtoyiPAivl7/xZ3G1z5h20g==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.13.1: + resolution: {integrity: sha512-dBHQl+7wZzBYcIF6o4k2XkAfwP2ks1mYW2q/Gzv9n39uDcDiAGDqEyml08OdY0BIct0yLSPkDTqn4i6czpBLLw==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.13.1: + resolution: {integrity: sha512-bur4JOxvYxfrAmocRJIW0SADs3QdEYK6TQ7dTNz6Z4/lySeu3Z1H/+tl0a4qDYv0bCdBpUYM0sYa/X+9ZqgfSQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.13.1: + resolution: {integrity: sha512-ssp77SjcDIUSoUyj7DU7/5iwM4ZEluY+N8umtCT9nBRs3u045t0KkW02LTyHouHDomnMXaXSZcCSr2bdMK63kA==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.13.1: + resolution: {integrity: sha512-Jv1DkIvwEPAb+v25/Unrnnq9BO3F5cbFPT821n3S5litkz+O5NuXuNhqtPx5KtcwOTtaqkTsO+IVzJOsxd11aQ==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-s390x-gnu@4.13.1: + resolution: {integrity: sha512-U564BrhEfaNChdATQaEODtquCC7Ez+8Hxz1h5MAdMYj0AqD0GA9rHCpElajb/sQcaFL6NXmHc5O+7FXpWMa73Q==} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.13.1: + resolution: {integrity: sha512-zGRDulLTeDemR8DFYyFIQ8kMP02xpUsX4IBikc7lwL9PrwR3gWmX2NopqiGlI2ZVWMl15qZeUjumTwpv18N7sQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.13.1: + resolution: {integrity: sha512-VTk/MveyPdMFkYJJPCkYBw07KcTkGU2hLEyqYMsU4NjiOfzoaDTW9PWGRsNwiOA3qI0k/JQPjkl/4FCK1smskQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.13.1: + resolution: {integrity: sha512-L+hX8Dtibb02r/OYCsp4sQQIi3ldZkFI0EUkMTDwRfFykXBPptoz/tuuGqEd3bThBSLRWPR6wsixDSgOx/U3Zw==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.13.1: + resolution: {integrity: sha512-+dI2jVPfM5A8zme8riEoNC7UKk0Lzc7jCj/U89cQIrOjrZTCWZl/+IXUeRT2rEZ5j25lnSA9G9H1Ob9azaF/KQ==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.13.1: + resolution: {integrity: sha512-YY1Exxo2viZ/O2dMHuwQvimJ0SqvL+OAWQLLY6rvXavgQKjhQUzn7nc1Dd29gjB5Fqi00nrBWctJBOyfVMIVxw==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@sindresorhus/is@3.1.2: resolution: {integrity: sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==} engines: {node: '>=10'} @@ -453,6 +868,14 @@ packages: resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} dev: false + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: false @@ -469,7 +892,6 @@ packages: resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} dependencies: undici-types: 5.26.5 - dev: false /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -479,7 +901,7 @@ packages: resolution: {integrity: sha512-7yQiX6MWSFSvc/1wW5smJMZTZ4fHOd+hqLr3qr/HONDxHEa2bnYAsOcGBOEqFIjd4yetwMOdEDdeW+udRAQnHA==} dev: false - /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.2): + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.3): resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -491,23 +913,23 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.3) '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.3) debug: 4.3.4 eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare-lite: 1.4.0 semver: 7.6.0 - tsutils: 3.21.0(typescript@5.4.2) - typescript: 5.4.2 + tsutils: 3.21.0(typescript@5.4.3) + typescript: 5.4.3 transitivePeerDependencies: - supports-color dev: false - /@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.2): + /@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.3): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -519,10 +941,10 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.2) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.3) debug: 4.3.4 eslint: 8.57.0 - typescript: 5.4.2 + typescript: 5.4.3 transitivePeerDependencies: - supports-color dev: false @@ -535,7 +957,7 @@ packages: '@typescript-eslint/visitor-keys': 5.62.0 dev: false - /@typescript-eslint/type-utils@5.62.0(eslint@8.57.0)(typescript@5.4.2): + /@typescript-eslint/type-utils@5.62.0(eslint@8.57.0)(typescript@5.4.3): resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -545,12 +967,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.2) - '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.3) debug: 4.3.4 eslint: 8.57.0 - tsutils: 3.21.0(typescript@5.4.2) - typescript: 5.4.2 + tsutils: 3.21.0(typescript@5.4.3) + typescript: 5.4.3 transitivePeerDependencies: - supports-color dev: false @@ -560,7 +982,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.2): + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.3): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -575,13 +997,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.0 - tsutils: 3.21.0(typescript@5.4.2) - typescript: 5.4.2 + tsutils: 3.21.0(typescript@5.4.3) + typescript: 5.4.3 transitivePeerDependencies: - supports-color dev: false - /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.2): + /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.3): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -592,7 +1014,7 @@ packages: '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.2) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.3) eslint: 8.57.0 eslint-scope: 5.1.1 semver: 7.6.0 @@ -613,6 +1035,69 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: false + /@vitest/coverage-v8@1.4.0(vitest@1.4.0): + resolution: {integrity: sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==} + peerDependencies: + vitest: 1.4.0 + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.4 + istanbul-reports: 3.1.7 + magic-string: 0.30.8 + magicast: 0.3.3 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.0.0 + test-exclude: 6.0.0 + v8-to-istanbul: 9.2.0 + vitest: 1.4.0(@types/node@20.11.30) + transitivePeerDependencies: + - supports-color + dev: true + + /@vitest/expect@1.4.0: + resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} + dependencies: + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + chai: 4.4.1 + dev: true + + /@vitest/runner@1.4.0: + resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} + dependencies: + '@vitest/utils': 1.4.0 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: true + + /@vitest/snapshot@1.4.0: + resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} + dependencies: + magic-string: 0.30.8 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: true + + /@vitest/spy@1.4.0: + resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} + dependencies: + tinyspy: 2.2.1 + dev: true + + /@vitest/utils@1.4.0: + resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -624,13 +1109,11 @@ packages: /acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} - dev: false /acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} hasBin: true - dev: false /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -660,6 +1143,11 @@ packages: color-convert: 2.0.1 dev: false + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: false @@ -673,6 +1161,10 @@ packages: engines: {node: '>=8'} dev: false + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false @@ -689,14 +1181,12 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: false /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: false /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} @@ -705,6 +1195,11 @@ packages: fill-range: 7.0.1 dev: false + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + /call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} dev: false @@ -714,6 +1209,19 @@ packages: engines: {node: '>=6'} dev: false + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -736,6 +1244,12 @@ packages: engines: {node: '>=10'} dev: false + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -775,7 +1289,10 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: false + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true /cosmiconfig@8.2.0: resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} @@ -798,7 +1315,6 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: false /crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} @@ -823,7 +1339,13 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: false + + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -838,6 +1360,11 @@ packages: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: false + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -915,6 +1442,37 @@ packages: '@esbuild/win32-x64': 0.19.12 dev: false + /esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + dev: true + /escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} @@ -1031,6 +1589,12 @@ packages: engines: {node: '>=4.0'} dev: false + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1041,6 +1605,21 @@ packages: engines: {node: '>=6.0.0'} dev: false + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: false @@ -1154,14 +1733,20 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: false + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true - dev: false optional: true /get-caller-file@2.0.5: @@ -1169,6 +1754,15 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: false + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true + /get-tsconfig@4.7.3: resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} dependencies: @@ -1198,7 +1792,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: false /globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} @@ -1235,12 +1828,20 @@ packages: /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - dev: false + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true /http2-client@1.3.5: resolution: {integrity: sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==} dev: false + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + /ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -1264,11 +1865,9 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: false /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1301,14 +1900,55 @@ packages: engines: {node: '>=8'} dev: false + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: false + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@5.0.4: + resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} + engines: {node: '>=10'} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: false + /js-tokens@8.0.3: + resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + dev: true + /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1332,6 +1972,10 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: false + /jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: true + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -1358,6 +2002,14 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: false + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.6.1 + pkg-types: 1.0.3 + dev: true + /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1373,19 +2025,38 @@ packages: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: false + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: true + /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: false + + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /magicast@0.3.3: + resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==} + dependencies: + '@babel/parser': 7.24.1 + '@babel/types': 7.24.0 + source-map-js: 1.2.0 + dev: true /make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} dependencies: semver: 7.6.0 - dev: false /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -1399,6 +2070,10 @@ packages: minimatch: 3.1.2 dev: false + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1424,15 +2099,27 @@ packages: mime-db: 1.52.0 dev: false + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - dev: false + + /mlly@1.6.1: + resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} + dependencies: + acorn: 8.11.3 + pathe: 1.1.2 + pkg-types: 1.0.3 + ufo: 1.5.3 + dev: true /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: false /nanoid@3.3.6: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} @@ -1440,6 +2127,12 @@ packages: hasBin: true dev: false + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: false @@ -1504,6 +2197,13 @@ packages: '@types/node': 20.11.30 dev: false + /npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + /oas-kit-common@1.0.8: resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==} dependencies: @@ -1550,7 +2250,13 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 - dev: false + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} @@ -1571,6 +2277,13 @@ packages: yocto-queue: 0.1.0 dev: false + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -1603,32 +2316,75 @@ packages: /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - dev: false /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - dev: false + + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} dev: false + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: false /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} dev: false + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.1 + mlly: 1.6.1 + pathe: 1.1.2 + dev: true + + /playwright-core@1.42.1: + resolution: {integrity: sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==} + engines: {node: '>=16'} + hasBin: true + dev: true + + /playwright@1.42.1: + resolution: {integrity: sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.42.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} dev: false + /postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 + dev: true + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1646,6 +2402,21 @@ packages: hasBin: true dev: false + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false @@ -1659,6 +2430,10 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: false + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + /reftools@1.1.9: resolution: {integrity: sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==} dev: false @@ -1689,6 +2464,30 @@ packages: glob: 7.2.3 dev: false + /rollup@4.13.1: + resolution: {integrity: sha512-hFi+fU132IvJ2ZuihN56dwgpltpmLZHZWsx27rMCTZ2sYwrqlgL5sECGy1eeV2lAihD8EzChBVVhsXci0wD4Tg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.13.1 + '@rollup/rollup-android-arm64': 4.13.1 + '@rollup/rollup-darwin-arm64': 4.13.1 + '@rollup/rollup-darwin-x64': 4.13.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.13.1 + '@rollup/rollup-linux-arm64-gnu': 4.13.1 + '@rollup/rollup-linux-arm64-musl': 4.13.1 + '@rollup/rollup-linux-riscv64-gnu': 4.13.1 + '@rollup/rollup-linux-s390x-gnu': 4.13.1 + '@rollup/rollup-linux-x64-gnu': 4.13.1 + '@rollup/rollup-linux-x64-musl': 4.13.1 + '@rollup/rollup-win32-arm64-msvc': 4.13.1 + '@rollup/rollup-win32-ia32-msvc': 4.13.1 + '@rollup/rollup-win32-x64-msvc': 4.13.1 + fsevents: 2.3.3 + dev: true + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -1701,19 +2500,16 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: false /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 - dev: false /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - dev: false /should-equal@2.0.0: resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==} @@ -1753,6 +2549,15 @@ packages: should-util: 1.0.1 dev: false + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + /simple-git@3.23.0: resolution: {integrity: sha512-P9ggTW8vb/21CAL/AmnACAhqBDfnqSSZVpV7WuFtsFR9HLunf5IqQvk+OXAQTfkcZep8pKnt3DV3o7w3TegEkQ==} dependencies: @@ -1775,6 +2580,19 @@ packages: engines: {node: '>=8'} dev: false + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: true + + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + dev: true + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1791,11 +2609,22 @@ packages: ansi-regex: 5.0.1 dev: false + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} dev: false + /strip-literal@2.0.0: + resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} + dependencies: + js-tokens: 8.0.3 + dev: true + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -1808,7 +2637,6 @@ packages: engines: {node: '>=8'} dependencies: has-flag: 4.0.0 - dev: false /swagger-schema-official@2.0.0-bab6bed: resolution: {integrity: sha512-rCC0NWGKr/IJhtRuPq/t37qvZHI/mH4I4sxflVM+qgVe5Z2uOCivzWaVbuioJaB61kvm5UvB7b49E+oBY0M8jA==} @@ -1855,10 +2683,38 @@ packages: - encoding dev: false + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: false + /tinybench@2.6.0: + resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + dev: true + + /tinypool@0.8.3: + resolution: {integrity: sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1870,7 +2726,7 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false - /ts-node@10.9.2(@types/node@20.11.30)(typescript@5.4.2): + /ts-node@10.9.2(@types/node@20.11.30)(typescript@5.4.3): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: @@ -1896,7 +2752,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.4.2 + typescript: 5.4.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: false @@ -1905,14 +2761,14 @@ packages: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: false - /tsutils@3.21.0(typescript@5.4.2): + /tsutils@3.21.0(typescript@5.4.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.4.2 + typescript: 5.4.3 dev: false /tsx@4.7.1: @@ -1933,6 +2789,11 @@ packages: prelude-ls: 1.2.1 dev: false + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -1944,15 +2805,17 @@ packages: hasBin: true dev: false - /typescript@5.4.2: - resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} + /typescript@5.4.3: + resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==} engines: {node: '>=14.17'} hasBin: true - dev: false + + /ufo@1.5.3: + resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + dev: true /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: false /unicode-emoji-modifier-base@1.0.0: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} @@ -1974,6 +2837,128 @@ packages: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: false + /v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + dev: true + + /vite-node@1.4.0(@types/node@20.11.30): + resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.0 + vite: 5.2.6(@types/node@20.11.30) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite@5.2.6(@types/node@20.11.30): + resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.11.30 + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.13.1 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitest@1.4.0(@types/node@20.11.30): + resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.4.0 + '@vitest/ui': 1.4.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 20.11.30 + '@vitest/expect': 1.4.0 + '@vitest/runner': 1.4.0 + '@vitest/snapshot': 1.4.0 + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.8 + pathe: 1.1.2 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.0.0 + tinybench: 2.6.0 + tinypool: 0.8.3 + vite: 5.2.6(@types/node@20.11.30) + vite-node: 1.4.0(@types/node@20.11.30) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /walk-sync@3.0.0: resolution: {integrity: sha512-41TvKmDGVpm2iuH7o+DAOt06yyu/cSHpX3uzAwetzASvlNtVddgIjXIb2DfB/Wa20B1Jo86+1Dv1CraSU7hWdw==} engines: {node: 10.* || >= 12.*} @@ -2006,7 +2991,15 @@ packages: hasBin: true dependencies: isexe: 2.0.0 - dev: false + + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} @@ -2019,7 +3012,6 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: false /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} @@ -2028,7 +3020,6 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: false /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} @@ -2069,6 +3060,11 @@ packages: engines: {node: '>=10'} dev: false + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true + /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false diff --git a/secrets.yml.template b/secrets.yml.template new file mode 100644 index 0000000..982bfed --- /dev/null +++ b/secrets.yml.template @@ -0,0 +1 @@ +SONARR_API_KEY: replace-with-sonarr-api-key diff --git a/src/__generated__/generated-radarr-api.ts b/src/__generated__/generated-radarr-api.ts new file mode 100644 index 0000000..4406eab --- /dev/null +++ b/src/__generated__/generated-radarr-api.ts @@ -0,0 +1,6352 @@ +/* eslint-disable */ +/* tslint:disable */ +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +export enum AddMovieMethod { + Manual = "manual", + List = "list", + Collection = "collection", +} + +export interface AddMovieOptions { + ignoreEpisodesWithFiles?: boolean; + ignoreEpisodesWithoutFiles?: boolean; + monitor?: MonitorTypes; + searchForMovie?: boolean; + addMethod?: AddMovieMethod; +} + +export interface AlternativeTitleResource { + /** @format int32 */ + id?: number; + sourceType?: SourceType; + /** @format int32 */ + movieMetadataId?: number; + title?: string | null; + cleanTitle?: string | null; +} + +export interface ApiInfoResource { + current?: string | null; + deprecated?: string[] | null; +} + +export enum ApplyTags { + Add = "add", + Remove = "remove", + Replace = "replace", +} + +export enum AuthenticationRequiredType { + Enabled = "enabled", + DisabledForLocalAddresses = "disabledForLocalAddresses", +} + +export enum AuthenticationType { + None = "none", + Basic = "basic", + Forms = "forms", + External = "external", +} + +export interface AutoTaggingResource { + /** @format int32 */ + id?: number; + name?: string | null; + removeTagsAutomatically?: boolean; + /** @uniqueItems true */ + tags?: number[] | null; + specifications?: AutoTaggingSpecificationSchema[] | null; +} + +export interface AutoTaggingSpecificationSchema { + /** @format int32 */ + id?: number; + name?: string | null; + implementation?: string | null; + implementationName?: string | null; + negate?: boolean; + required?: boolean; + fields?: Field[] | null; +} + +export interface BackupResource { + /** @format int32 */ + id?: number; + name?: string | null; + path?: string | null; + type?: BackupType; + /** @format int64 */ + size?: number; + /** @format date-time */ + time?: string; +} + +export enum BackupType { + Scheduled = "scheduled", + Manual = "manual", + Update = "update", +} + +export interface BlocklistBulkResource { + ids?: number[] | null; +} + +export interface BlocklistResource { + /** @format int32 */ + id?: number; + /** @format int32 */ + movieId?: number; + sourceTitle?: string | null; + languages?: Language[] | null; + quality?: QualityModel; + customFormats?: CustomFormatResource[] | null; + /** @format date-time */ + date?: string; + protocol?: DownloadProtocol; + indexer?: string | null; + message?: string | null; + movie?: MovieResource; +} + +export interface BlocklistResourcePagingResource { + /** @format int32 */ + page?: number; + /** @format int32 */ + pageSize?: number; + sortKey?: string | null; + sortDirection?: SortDirection; + /** @format int32 */ + totalRecords?: number; + records?: BlocklistResource[] | null; +} + +export enum CertificateValidationType { + Enabled = "enabled", + DisabledForLocalAddresses = "disabledForLocalAddresses", + Disabled = "disabled", +} + +export interface CollectionMovieResource { + /** @format int32 */ + tmdbId?: number; + imdbId?: string | null; + title?: string | null; + cleanTitle?: string | null; + sortTitle?: string | null; + status?: MovieStatusType; + overview?: string | null; + /** @format int32 */ + runtime?: number; + images?: MediaCover[] | null; + /** @format int32 */ + year?: number; + ratings?: Ratings; + genres?: string[] | null; + folder?: string | null; +} + +export interface CollectionResource { + /** @format int32 */ + id?: number; + title?: string | null; + sortTitle?: string | null; + /** @format int32 */ + tmdbId?: number; + images?: MediaCover[] | null; + overview?: string | null; + monitored?: boolean; + rootFolderPath?: string | null; + /** @format int32 */ + qualityProfileId?: number; + searchOnAdd?: boolean; + minimumAvailability?: MovieStatusType; + movies?: CollectionMovieResource[] | null; + /** @format int32 */ + missingMovies?: number; + /** @uniqueItems true */ + tags?: number[] | null; +} + +export interface CollectionUpdateResource { + collectionIds?: number[] | null; + monitored?: boolean | null; + monitorMovies?: boolean | null; + searchOnAdd?: boolean | null; + /** @format int32 */ + qualityProfileId?: number | null; + rootFolderPath?: string | null; + minimumAvailability?: MovieStatusType; +} + +export enum ColonReplacementFormat { + Delete = "delete", + Dash = "dash", + SpaceDash = "spaceDash", + SpaceDashSpace = "spaceDashSpace", +} + +export interface Command { + sendUpdatesToClient?: boolean; + updateScheduledTask?: boolean; + completionMessage?: string | null; + requiresDiskAccess?: boolean; + isExclusive?: boolean; + isTypeExclusive?: boolean; + isLongRunning?: boolean; + name?: string | null; + /** @format date-time */ + lastExecutionTime?: string | null; + /** @format date-time */ + lastStartTime?: string | null; + trigger?: CommandTrigger; + suppressMessages?: boolean; + clientUserAgent?: string | null; +} + +export enum CommandPriority { + Normal = "normal", + High = "high", + Low = "low", +} + +export interface CommandResource { + /** @format int32 */ + id?: number; + name?: string | null; + commandName?: string | null; + message?: string | null; + body?: Command; + priority?: CommandPriority; + status?: CommandStatus; + result?: CommandResult; + /** @format date-time */ + queued?: string; + /** @format date-time */ + started?: string | null; + /** @format date-time */ + ended?: string | null; + duration?: TimeSpan; + exception?: string | null; + trigger?: CommandTrigger; + clientUserAgent?: string | null; + /** @format date-time */ + stateChangeTime?: string | null; + sendUpdatesToClient?: boolean; + updateScheduledTask?: boolean; + /** @format date-time */ + lastExecutionTime?: string | null; +} + +export enum CommandResult { + Unknown = "unknown", + Successful = "successful", + Unsuccessful = "unsuccessful", +} + +export enum CommandStatus { + Queued = "queued", + Started = "started", + Completed = "completed", + Failed = "failed", + Aborted = "aborted", + Cancelled = "cancelled", + Orphaned = "orphaned", +} + +export enum CommandTrigger { + Unspecified = "unspecified", + Manual = "manual", + Scheduled = "scheduled", +} + +export interface CreditResource { + /** @format int32 */ + id?: number; + personName?: string | null; + creditTmdbId?: string | null; + /** @format int32 */ + personTmdbId?: number; + /** @format int32 */ + movieMetadataId?: number; + images?: MediaCover[] | null; + department?: string | null; + job?: string | null; + character?: string | null; + /** @format int32 */ + order?: number; + type?: CreditType; +} + +export enum CreditType { + Cast = "cast", + Crew = "crew", +} + +export interface CustomFilterResource { + /** @format int32 */ + id?: number; + type?: string | null; + label?: string | null; + filters?: Record[] | null; +} + +export interface CustomFormatResource { + /** @format int32 */ + id?: number; + name?: string | null; + includeCustomFormatWhenRenaming?: boolean | null; + specifications?: CustomFormatSpecificationSchema[] | null; +} + +export interface CustomFormatSpecificationSchema { + /** @format int32 */ + id?: number; + name?: string | null; + implementation?: string | null; + implementationName?: string | null; + infoLink?: string | null; + negate?: boolean; + required?: boolean; + fields?: Field[] | null; + presets?: CustomFormatSpecificationSchema[] | null; +} + +export enum DatabaseType { + SqLite = "sqLite", + PostgreSQL = "postgreSQL", +} + +export interface DelayProfileResource { + /** @format int32 */ + id?: number; + enableUsenet?: boolean; + enableTorrent?: boolean; + preferredProtocol?: DownloadProtocol; + /** @format int32 */ + usenetDelay?: number; + /** @format int32 */ + torrentDelay?: number; + bypassIfHighestQuality?: boolean; + bypassIfAboveCustomFormatScore?: boolean; + /** @format int32 */ + minimumCustomFormatScore?: number; + /** @format int32 */ + order?: number; + /** @uniqueItems true */ + tags?: number[] | null; +} + +export interface DiskSpaceResource { + /** @format int32 */ + id?: number; + path?: string | null; + label?: string | null; + /** @format int64 */ + freeSpace?: number; + /** @format int64 */ + totalSpace?: number; +} + +export interface DownloadClientBulkResource { + ids?: number[] | null; + tags?: number[] | null; + applyTags?: ApplyTags; + enable?: boolean | null; + /** @format int32 */ + priority?: number | null; + removeCompletedDownloads?: boolean | null; + removeFailedDownloads?: boolean | null; +} + +export interface DownloadClientConfigResource { + /** @format int32 */ + id?: number; + downloadClientWorkingFolders?: string | null; + enableCompletedDownloadHandling?: boolean; + /** @format int32 */ + checkForFinishedDownloadInterval?: number; + autoRedownloadFailed?: boolean; + autoRedownloadFailedFromInteractiveSearch?: boolean; +} + +export interface DownloadClientResource { + /** @format int32 */ + id?: number; + name?: string | null; + fields?: Field[] | null; + implementationName?: string | null; + implementation?: string | null; + configContract?: string | null; + infoLink?: string | null; + message?: ProviderMessage; + /** @uniqueItems true */ + tags?: number[] | null; + presets?: DownloadClientResource[] | null; + enable?: boolean; + protocol?: DownloadProtocol; + /** @format int32 */ + priority?: number; + removeCompletedDownloads?: boolean; + removeFailedDownloads?: boolean; +} + +export enum DownloadProtocol { + Unknown = "unknown", + Usenet = "usenet", + Torrent = "torrent", +} + +export interface ExtraFileResource { + /** @format int32 */ + id?: number; + /** @format int32 */ + movieId?: number; + /** @format int32 */ + movieFileId?: number | null; + relativePath?: string | null; + extension?: string | null; + type?: ExtraFileType; +} + +export enum ExtraFileType { + Subtitle = "subtitle", + Metadata = "metadata", + Other = "other", +} + +export interface Field { + /** @format int32 */ + order?: number; + name?: string | null; + label?: string | null; + unit?: string | null; + helpText?: string | null; + helpTextWarning?: string | null; + helpLink?: string | null; + value?: any; + type?: string | null; + advanced?: boolean; + selectOptions?: SelectOption[] | null; + selectOptionsProviderAction?: string | null; + section?: string | null; + hidden?: string | null; + privacy?: PrivacyLevel; + placeholder?: string | null; + isFloat?: boolean; +} + +export enum FileDateType { + None = "none", + Cinemas = "cinemas", + Release = "release", +} + +export enum HealthCheckResult { + Ok = "ok", + Notice = "notice", + Warning = "warning", + Error = "error", +} + +export interface HealthResource { + /** @format int32 */ + id?: number; + source?: string | null; + type?: HealthCheckResult; + message?: string | null; + wikiUrl?: HttpUri; +} + +export interface HistoryResource { + /** @format int32 */ + id?: number; + /** @format int32 */ + movieId?: number; + sourceTitle?: string | null; + languages?: Language[] | null; + quality?: QualityModel; + customFormats?: CustomFormatResource[] | null; + /** @format int32 */ + customFormatScore?: number; + qualityCutoffNotMet?: boolean; + /** @format date-time */ + date?: string; + downloadId?: string | null; + eventType?: MovieHistoryEventType; + data?: Record; + movie?: MovieResource; +} + +export interface HistoryResourcePagingResource { + /** @format int32 */ + page?: number; + /** @format int32 */ + pageSize?: number; + sortKey?: string | null; + sortDirection?: SortDirection; + /** @format int32 */ + totalRecords?: number; + records?: HistoryResource[] | null; +} + +export interface HostConfigResource { + /** @format int32 */ + id?: number; + bindAddress?: string | null; + /** @format int32 */ + port?: number; + /** @format int32 */ + sslPort?: number; + enableSsl?: boolean; + launchBrowser?: boolean; + authenticationMethod?: AuthenticationType; + authenticationRequired?: AuthenticationRequiredType; + analyticsEnabled?: boolean; + username?: string | null; + password?: string | null; + passwordConfirmation?: string | null; + logLevel?: string | null; + consoleLogLevel?: string | null; + branch?: string | null; + apiKey?: string | null; + sslCertPath?: string | null; + sslCertPassword?: string | null; + urlBase?: string | null; + instanceName?: string | null; + applicationUrl?: string | null; + updateAutomatically?: boolean; + updateMechanism?: UpdateMechanism; + updateScriptPath?: string | null; + proxyEnabled?: boolean; + proxyType?: ProxyType; + proxyHostname?: string | null; + /** @format int32 */ + proxyPort?: number; + proxyUsername?: string | null; + proxyPassword?: string | null; + proxyBypassFilter?: string | null; + proxyBypassLocalAddresses?: boolean; + certificateValidation?: CertificateValidationType; + backupFolder?: string | null; + /** @format int32 */ + backupInterval?: number; + /** @format int32 */ + backupRetention?: number; +} + +export interface HttpUri { + fullUri?: string | null; + scheme?: string | null; + host?: string | null; + /** @format int32 */ + port?: number | null; + path?: string | null; + query?: string | null; + fragment?: string | null; +} + +export interface ImportExclusionsResource { + /** @format int32 */ + id?: number; + name?: string | null; + fields?: Field[] | null; + implementationName?: string | null; + implementation?: string | null; + configContract?: string | null; + infoLink?: string | null; + message?: ProviderMessage; + /** @uniqueItems true */ + tags?: number[] | null; + presets?: ImportExclusionsResource[] | null; + /** @format int32 */ + tmdbId?: number; + movieTitle?: string | null; + /** @format int32 */ + movieYear?: number; +} + +export interface ImportListBulkResource { + ids?: number[] | null; + tags?: number[] | null; + applyTags?: ApplyTags; + enabled?: boolean | null; + enableAuto?: boolean | null; + rootFolderPath?: string | null; + /** @format int32 */ + qualityProfileId?: number | null; + minimumAvailability?: MovieStatusType; +} + +export interface ImportListConfigResource { + /** @format int32 */ + id?: number; + listSyncLevel?: string | null; + importExclusions?: string | null; +} + +export interface ImportListResource { + /** @format int32 */ + id?: number; + name?: string | null; + fields?: Field[] | null; + implementationName?: string | null; + implementation?: string | null; + configContract?: string | null; + infoLink?: string | null; + message?: ProviderMessage; + /** @uniqueItems true */ + tags?: number[] | null; + presets?: ImportListResource[] | null; + enabled?: boolean; + enableAuto?: boolean; + monitor?: MonitorTypes; + rootFolderPath?: string | null; + /** @format int32 */ + qualityProfileId?: number; + searchOnAdd?: boolean; + minimumAvailability?: MovieStatusType; + listType?: ImportListType; + /** @format int32 */ + listOrder?: number; + minRefreshInterval?: TimeSpan; +} + +export enum ImportListType { + Program = "program", + Tmdb = "tmdb", + Trakt = "trakt", + Plex = "plex", + Simkl = "simkl", + Other = "other", + Advanced = "advanced", +} + +export interface IndexerBulkResource { + ids?: number[] | null; + tags?: number[] | null; + applyTags?: ApplyTags; + enableRss?: boolean | null; + enableAutomaticSearch?: boolean | null; + enableInteractiveSearch?: boolean | null; + /** @format int32 */ + priority?: number | null; +} + +export interface IndexerConfigResource { + /** @format int32 */ + id?: number; + /** @format int32 */ + minimumAge?: number; + /** @format int32 */ + maximumSize?: number; + /** @format int32 */ + retention?: number; + /** @format int32 */ + rssSyncInterval?: number; + preferIndexerFlags?: boolean; + /** @format int32 */ + availabilityDelay?: number; + allowHardcodedSubs?: boolean; + whitelistedHardcodedSubs?: string | null; +} + +export interface IndexerFlagResource { + /** @format int32 */ + id?: number; + name?: string | null; + nameLower?: string | null; +} + +export interface IndexerResource { + /** @format int32 */ + id?: number; + name?: string | null; + fields?: Field[] | null; + implementationName?: string | null; + implementation?: string | null; + configContract?: string | null; + infoLink?: string | null; + message?: ProviderMessage; + /** @uniqueItems true */ + tags?: number[] | null; + presets?: IndexerResource[] | null; + enableRss?: boolean; + enableAutomaticSearch?: boolean; + enableInteractiveSearch?: boolean; + supportsRss?: boolean; + supportsSearch?: boolean; + protocol?: DownloadProtocol; + /** @format int32 */ + priority?: number; + /** @format int32 */ + downloadClientId?: number; +} + +export interface Language { + /** @format int32 */ + id?: number; + name?: string | null; +} + +export interface LanguageResource { + /** @format int32 */ + id?: number; + name?: string | null; + nameLower?: string | null; +} + +export interface LocalizationLanguageResource { + identifier?: string | null; +} + +export interface LogFileResource { + /** @format int32 */ + id?: number; + filename?: string | null; + /** @format date-time */ + lastWriteTime?: string; + contentsUrl?: string | null; + downloadUrl?: string | null; +} + +export interface LogResource { + /** @format int32 */ + id?: number; + /** @format date-time */ + time?: string; + exception?: string | null; + exceptionType?: string | null; + level?: string | null; + logger?: string | null; + message?: string | null; + method?: string | null; +} + +export interface LogResourcePagingResource { + /** @format int32 */ + page?: number; + /** @format int32 */ + pageSize?: number; + sortKey?: string | null; + sortDirection?: SortDirection; + /** @format int32 */ + totalRecords?: number; + records?: LogResource[] | null; +} + +export interface ManualImportReprocessResource { + /** @format int32 */ + id?: number; + path?: string | null; + /** @format int32 */ + movieId?: number; + movie?: MovieResource; + quality?: QualityModel; + languages?: Language[] | null; + releaseGroup?: string | null; + downloadId?: string | null; + customFormats?: CustomFormatResource[] | null; + /** @format int32 */ + customFormatScore?: number; + /** @format int32 */ + indexerFlags?: number; + rejections?: Rejection[] | null; +} + +export interface ManualImportResource { + /** @format int32 */ + id?: number; + path?: string | null; + relativePath?: string | null; + folderName?: string | null; + name?: string | null; + /** @format int64 */ + size?: number; + movie?: MovieResource; + quality?: QualityModel; + languages?: Language[] | null; + releaseGroup?: string | null; + /** @format int32 */ + qualityWeight?: number; + downloadId?: string | null; + customFormats?: CustomFormatResource[] | null; + /** @format int32 */ + customFormatScore?: number; + /** @format int32 */ + indexerFlags?: number; + rejections?: Rejection[] | null; +} + +export interface MediaCover { + coverType?: MediaCoverTypes; + url?: string | null; + remoteUrl?: string | null; +} + +export enum MediaCoverTypes { + Unknown = "unknown", + Poster = "poster", + Banner = "banner", + Fanart = "fanart", + Screenshot = "screenshot", + Headshot = "headshot", + Clearlogo = "clearlogo", +} + +export interface MediaInfoResource { + /** @format int32 */ + id?: number; + /** @format int64 */ + audioBitrate?: number; + /** @format double */ + audioChannels?: number; + audioCodec?: string | null; + audioLanguages?: string | null; + /** @format int32 */ + audioStreamCount?: number; + /** @format int32 */ + videoBitDepth?: number; + /** @format int64 */ + videoBitrate?: number; + videoCodec?: string | null; + /** @format double */ + videoFps?: number; + videoDynamicRange?: string | null; + videoDynamicRangeType?: string | null; + resolution?: string | null; + runTime?: string | null; + scanType?: string | null; + subtitles?: string | null; +} + +export interface MediaManagementConfigResource { + /** @format int32 */ + id?: number; + autoUnmonitorPreviouslyDownloadedMovies?: boolean; + recycleBin?: string | null; + /** @format int32 */ + recycleBinCleanupDays?: number; + downloadPropersAndRepacks?: ProperDownloadTypes; + createEmptyMovieFolders?: boolean; + deleteEmptyFolders?: boolean; + fileDate?: FileDateType; + rescanAfterRefresh?: RescanAfterRefreshType; + autoRenameFolders?: boolean; + pathsDefaultStatic?: boolean; + setPermissionsLinux?: boolean; + chmodFolder?: string | null; + chownGroup?: string | null; + skipFreeSpaceCheckWhenImporting?: boolean; + /** @format int32 */ + minimumFreeSpaceWhenImporting?: number; + copyUsingHardlinks?: boolean; + useScriptImport?: boolean; + scriptImportPath?: string | null; + importExtraFiles?: boolean; + extraFileExtensions?: string | null; + enableMediaInfo?: boolean; +} + +export interface MetadataConfigResource { + /** @format int32 */ + id?: number; + certificationCountry?: TMDbCountryCode; +} + +export interface MetadataResource { + /** @format int32 */ + id?: number; + name?: string | null; + fields?: Field[] | null; + implementationName?: string | null; + implementation?: string | null; + configContract?: string | null; + infoLink?: string | null; + message?: ProviderMessage; + /** @uniqueItems true */ + tags?: number[] | null; + presets?: MetadataResource[] | null; + enable?: boolean; +} + +export enum Modifier { + None = "none", + Regional = "regional", + Screener = "screener", + Rawhd = "rawhd", + Brdisk = "brdisk", + Remux = "remux", +} + +export enum MonitorTypes { + MovieOnly = "movieOnly", + MovieAndCollection = "movieAndCollection", + None = "none", +} + +export interface MovieCollectionResource { + title?: string | null; + /** @format int32 */ + tmdbId?: number; +} + +export interface MovieEditorResource { + movieIds?: number[] | null; + monitored?: boolean | null; + /** @format int32 */ + qualityProfileId?: number | null; + minimumAvailability?: MovieStatusType; + rootFolderPath?: string | null; + tags?: number[] | null; + applyTags?: ApplyTags; + moveFiles?: boolean; + deleteFiles?: boolean; + addImportExclusion?: boolean; +} + +export interface MovieFileListResource { + movieFileIds?: number[] | null; + languages?: Language[] | null; + quality?: QualityModel; + edition?: string | null; + releaseGroup?: string | null; + sceneName?: string | null; + /** @format int32 */ + indexerFlags?: number | null; +} + +export interface MovieFileResource { + /** @format int32 */ + id?: number; + /** @format int32 */ + movieId?: number; + relativePath?: string | null; + path?: string | null; + /** @format int64 */ + size?: number; + /** @format date-time */ + dateAdded?: string; + sceneName?: string | null; + releaseGroup?: string | null; + edition?: string | null; + languages?: Language[] | null; + quality?: QualityModel; + customFormats?: CustomFormatResource[] | null; + /** @format int32 */ + customFormatScore?: number; + /** @format int32 */ + indexerFlags?: number | null; + mediaInfo?: MediaInfoResource; + originalFilePath?: string | null; + qualityCutoffNotMet?: boolean; +} + +export enum MovieHistoryEventType { + Unknown = "unknown", + Grabbed = "grabbed", + DownloadFolderImported = "downloadFolderImported", + DownloadFailed = "downloadFailed", + MovieFileDeleted = "movieFileDeleted", + MovieFolderImported = "movieFolderImported", + MovieFileRenamed = "movieFileRenamed", + DownloadIgnored = "downloadIgnored", +} + +export interface MovieResource { + /** @format int32 */ + id?: number; + title?: string | null; + originalTitle?: string | null; + originalLanguage?: Language; + alternateTitles?: AlternativeTitleResource[] | null; + /** @format int32 */ + secondaryYear?: number | null; + /** @format int32 */ + secondaryYearSourceId?: number; + sortTitle?: string | null; + /** @format int64 */ + sizeOnDisk?: number | null; + status?: MovieStatusType; + overview?: string | null; + /** @format date-time */ + inCinemas?: string | null; + /** @format date-time */ + physicalRelease?: string | null; + /** @format date-time */ + digitalRelease?: string | null; + physicalReleaseNote?: string | null; + images?: MediaCover[] | null; + website?: string | null; + remotePoster?: string | null; + /** @format int32 */ + year?: number; + youTubeTrailerId?: string | null; + studio?: string | null; + path?: string | null; + /** @format int32 */ + qualityProfileId?: number; + hasFile?: boolean | null; + monitored?: boolean; + minimumAvailability?: MovieStatusType; + isAvailable?: boolean; + folderName?: string | null; + /** @format int32 */ + runtime?: number; + cleanTitle?: string | null; + imdbId?: string | null; + /** @format int32 */ + tmdbId?: number; + titleSlug?: string | null; + rootFolderPath?: string | null; + folder?: string | null; + certification?: string | null; + genres?: string[] | null; + /** @uniqueItems true */ + tags?: number[] | null; + /** @format date-time */ + added?: string; + addOptions?: AddMovieOptions; + ratings?: Ratings; + movieFile?: MovieFileResource; + collection?: MovieCollectionResource; + /** @format float */ + popularity?: number; + statistics?: MovieStatisticsResource; +} + +export enum MovieRuntimeFormatType { + HoursMinutes = "hoursMinutes", + Minutes = "minutes", +} + +export interface MovieStatisticsResource { + /** @format int32 */ + movieFileCount?: number; + /** @format int64 */ + sizeOnDisk?: number; + releaseGroups?: string[] | null; +} + +export enum MovieStatusType { + Tba = "tba", + Announced = "announced", + InCinemas = "inCinemas", + Released = "released", + Deleted = "deleted", +} + +export interface NamingConfigResource { + /** @format int32 */ + id?: number; + renameMovies?: boolean; + replaceIllegalCharacters?: boolean; + colonReplacementFormat?: ColonReplacementFormat; + standardMovieFormat?: string | null; + movieFolderFormat?: string | null; +} + +export interface NotificationResource { + /** @format int32 */ + id?: number; + name?: string | null; + fields?: Field[] | null; + implementationName?: string | null; + implementation?: string | null; + configContract?: string | null; + infoLink?: string | null; + message?: ProviderMessage; + /** @uniqueItems true */ + tags?: number[] | null; + presets?: NotificationResource[] | null; + link?: string | null; + onGrab?: boolean; + onDownload?: boolean; + onUpgrade?: boolean; + onRename?: boolean; + onMovieAdded?: boolean; + onMovieDelete?: boolean; + onMovieFileDelete?: boolean; + onMovieFileDeleteForUpgrade?: boolean; + onHealthIssue?: boolean; + onHealthRestored?: boolean; + onApplicationUpdate?: boolean; + onManualInteractionRequired?: boolean; + supportsOnGrab?: boolean; + supportsOnDownload?: boolean; + supportsOnUpgrade?: boolean; + supportsOnRename?: boolean; + supportsOnMovieAdded?: boolean; + supportsOnMovieDelete?: boolean; + supportsOnMovieFileDelete?: boolean; + supportsOnMovieFileDeleteForUpgrade?: boolean; + supportsOnHealthIssue?: boolean; + supportsOnHealthRestored?: boolean; + supportsOnApplicationUpdate?: boolean; + supportsOnManualInteractionRequired?: boolean; + includeHealthWarnings?: boolean; + testCommand?: string | null; +} + +export interface ParseResource { + /** @format int32 */ + id?: number; + title?: string | null; + parsedMovieInfo?: ParsedMovieInfo; + movie?: MovieResource; + languages?: Language[] | null; + customFormats?: CustomFormatResource[] | null; + /** @format int32 */ + customFormatScore?: number; +} + +export interface ParsedMovieInfo { + movieTitles?: string[] | null; + originalTitle?: string | null; + releaseTitle?: string | null; + simpleReleaseTitle?: string | null; + quality?: QualityModel; + languages?: Language[] | null; + releaseGroup?: string | null; + releaseHash?: string | null; + edition?: string | null; + /** @format int32 */ + year?: number; + imdbId?: string | null; + /** @format int32 */ + tmdbId?: number; + hardcodedSubs?: string | null; + movieTitle?: string | null; + primaryMovieTitle?: string | null; +} + +export interface PingResource { + status?: string | null; +} + +export enum PrivacyLevel { + Normal = "normal", + Password = "password", + ApiKey = "apiKey", + UserName = "userName", +} + +export interface ProfileFormatItemResource { + /** @format int32 */ + id?: number; + /** @format int32 */ + format?: number; + name?: string | null; + /** @format int32 */ + score?: number; +} + +export enum ProperDownloadTypes { + PreferAndUpgrade = "preferAndUpgrade", + DoNotUpgrade = "doNotUpgrade", + DoNotPrefer = "doNotPrefer", +} + +export interface ProviderMessage { + message?: string | null; + type?: ProviderMessageType; +} + +export enum ProviderMessageType { + Info = "info", + Warning = "warning", + Error = "error", +} + +export enum ProxyType { + Http = "http", + Socks4 = "socks4", + Socks5 = "socks5", +} + +export interface Quality { + /** @format int32 */ + id?: number; + name?: string | null; + source?: QualitySource; + /** @format int32 */ + resolution?: number; + modifier?: Modifier; +} + +export interface QualityDefinitionResource { + /** @format int32 */ + id?: number; + quality?: Quality; + title?: string | null; + /** @format int32 */ + weight?: number; + /** @format double */ + minSize?: number | null; + /** @format double */ + maxSize?: number | null; + /** @format double */ + preferredSize?: number | null; +} + +export interface QualityModel { + quality?: Quality; + revision?: Revision; +} + +export interface QualityProfileQualityItemResource { + /** @format int32 */ + id?: number; + name?: string | null; + quality?: Quality; + items?: QualityProfileQualityItemResource[] | null; + allowed?: boolean; +} + +export interface QualityProfileResource { + /** @format int32 */ + id?: number; + name?: string | null; + upgradeAllowed?: boolean; + /** @format int32 */ + cutoff?: number; + items?: QualityProfileQualityItemResource[] | null; + /** @format int32 */ + minFormatScore?: number; + /** @format int32 */ + cutoffFormatScore?: number; + formatItems?: ProfileFormatItemResource[] | null; + language?: Language; +} + +export enum QualitySource { + Unknown = "unknown", + Cam = "cam", + Telesync = "telesync", + Telecine = "telecine", + Workprint = "workprint", + Dvd = "dvd", + Tv = "tv", + Webdl = "webdl", + Webrip = "webrip", + Bluray = "bluray", +} + +export interface QueueBulkResource { + ids?: number[] | null; +} + +export interface QueueResource { + /** @format int32 */ + id?: number; + /** @format int32 */ + movieId?: number | null; + movie?: MovieResource; + languages?: Language[] | null; + quality?: QualityModel; + customFormats?: CustomFormatResource[] | null; + /** @format int32 */ + customFormatScore?: number; + /** @format double */ + size?: number; + title?: string | null; + /** @format double */ + sizeleft?: number; + timeleft?: TimeSpan; + /** @format date-time */ + estimatedCompletionTime?: string | null; + /** @format date-time */ + added?: string | null; + status?: string | null; + trackedDownloadStatus?: TrackedDownloadStatus; + trackedDownloadState?: TrackedDownloadState; + statusMessages?: TrackedDownloadStatusMessage[] | null; + errorMessage?: string | null; + downloadId?: string | null; + protocol?: DownloadProtocol; + downloadClient?: string | null; + downloadClientHasPostImportCategory?: boolean; + indexer?: string | null; + outputPath?: string | null; +} + +export interface QueueResourcePagingResource { + /** @format int32 */ + page?: number; + /** @format int32 */ + pageSize?: number; + sortKey?: string | null; + sortDirection?: SortDirection; + /** @format int32 */ + totalRecords?: number; + records?: QueueResource[] | null; +} + +export interface QueueStatusResource { + /** @format int32 */ + id?: number; + /** @format int32 */ + totalCount?: number; + /** @format int32 */ + count?: number; + /** @format int32 */ + unknownCount?: number; + errors?: boolean; + warnings?: boolean; + unknownErrors?: boolean; + unknownWarnings?: boolean; +} + +export interface RatingChild { + /** @format int32 */ + votes?: number; + /** @format double */ + value?: number; + type?: RatingType; +} + +export enum RatingType { + User = "user", + Critic = "critic", +} + +export interface Ratings { + imdb?: RatingChild; + tmdb?: RatingChild; + metacritic?: RatingChild; + rottenTomatoes?: RatingChild; +} + +export interface Rejection { + reason?: string | null; + type?: RejectionType; +} + +export enum RejectionType { + Permanent = "permanent", + Temporary = "temporary", +} + +export interface ReleaseProfileResource { + /** @format int32 */ + id?: number; + name?: string | null; + enabled?: boolean; + required?: any; + ignored?: any; + /** @format int32 */ + indexerId?: number; + /** @uniqueItems true */ + tags?: number[] | null; +} + +export interface ReleaseResource { + /** @format int32 */ + id?: number; + guid?: string | null; + quality?: QualityModel; + customFormats?: CustomFormatResource[] | null; + /** @format int32 */ + customFormatScore?: number; + /** @format int32 */ + qualityWeight?: number; + /** @format int32 */ + age?: number; + /** @format double */ + ageHours?: number; + /** @format double */ + ageMinutes?: number; + /** @format int64 */ + size?: number; + /** @format int32 */ + indexerId?: number; + indexer?: string | null; + releaseGroup?: string | null; + subGroup?: string | null; + releaseHash?: string | null; + title?: string | null; + sceneSource?: boolean; + movieTitles?: string[] | null; + languages?: Language[] | null; + /** @format int32 */ + mappedMovieId?: number | null; + approved?: boolean; + temporarilyRejected?: boolean; + rejected?: boolean; + /** @format int32 */ + tmdbId?: number; + /** @format int32 */ + imdbId?: number; + rejections?: string[] | null; + /** @format date-time */ + publishDate?: string; + commentUrl?: string | null; + downloadUrl?: string | null; + infoUrl?: string | null; + downloadAllowed?: boolean; + /** @format int32 */ + releaseWeight?: number; + edition?: string | null; + magnetUrl?: string | null; + infoHash?: string | null; + /** @format int32 */ + seeders?: number | null; + /** @format int32 */ + leechers?: number | null; + protocol?: DownloadProtocol; + indexerFlags?: string[] | null; + /** @format int32 */ + movieId?: number | null; + /** @format int32 */ + downloadClientId?: number | null; + downloadClient?: string | null; + shouldOverride?: boolean | null; +} + +export interface RemotePathMappingResource { + /** @format int32 */ + id?: number; + host?: string | null; + remotePath?: string | null; + localPath?: string | null; +} + +export interface RenameMovieResource { + /** @format int32 */ + id?: number; + /** @format int32 */ + movieId?: number; + /** @format int32 */ + movieFileId?: number; + existingPath?: string | null; + newPath?: string | null; +} + +export enum RescanAfterRefreshType { + Always = "always", + AfterManual = "afterManual", + Never = "never", +} + +export interface Revision { + /** @format int32 */ + version?: number; + /** @format int32 */ + real?: number; + isRepack?: boolean; +} + +export interface RootFolderResource { + /** @format int32 */ + id?: number; + path?: string | null; + accessible?: boolean; + /** @format int64 */ + freeSpace?: number | null; + unmappedFolders?: UnmappedFolder[] | null; +} + +export enum RuntimeMode { + Console = "console", + Service = "service", + Tray = "tray", +} + +export interface SelectOption { + /** @format int32 */ + value?: number; + name?: string | null; + /** @format int32 */ + order?: number; + hint?: string | null; + dividerAfter?: boolean; +} + +export enum SortDirection { + Default = "default", + Ascending = "ascending", + Descending = "descending", +} + +export enum SourceType { + Tmdb = "tmdb", + Mappings = "mappings", + User = "user", + Indexer = "indexer", +} + +export interface SystemResource { + appName?: string | null; + instanceName?: string | null; + version?: string | null; + /** @format date-time */ + buildTime?: string; + isDebug?: boolean; + isProduction?: boolean; + isAdmin?: boolean; + isUserInteractive?: boolean; + startupPath?: string | null; + appData?: string | null; + osName?: string | null; + osVersion?: string | null; + isNetCore?: boolean; + isLinux?: boolean; + isOsx?: boolean; + isWindows?: boolean; + isDocker?: boolean; + mode?: RuntimeMode; + branch?: string | null; + databaseType?: DatabaseType; + databaseVersion?: Version; + authentication?: AuthenticationType; + /** @format int32 */ + migrationVersion?: number; + urlBase?: string | null; + runtimeVersion?: Version; + runtimeName?: string | null; + /** @format date-time */ + startTime?: string; + packageVersion?: string | null; + packageAuthor?: string | null; + packageUpdateMechanism?: UpdateMechanism; + packageUpdateMechanismMessage?: string | null; +} + +export enum TMDbCountryCode { + Au = "au", + Br = "br", + Ca = "ca", + Fr = "fr", + De = "de", + Gb = "gb", + Ie = "ie", + It = "it", + Es = "es", + Us = "us", + Nz = "nz", +} + +export interface TagDetailsResource { + /** @format int32 */ + id?: number; + label?: string | null; + delayProfileIds?: number[] | null; + importListIds?: number[] | null; + notificationIds?: number[] | null; + releaseProfileIds?: number[] | null; + indexerIds?: number[] | null; + downloadClientIds?: number[] | null; + autoTagIds?: number[] | null; + movieIds?: number[] | null; +} + +export interface TagResource { + /** @format int32 */ + id?: number; + label?: string | null; +} + +export interface TaskResource { + /** @format int32 */ + id?: number; + name?: string | null; + taskName?: string | null; + /** @format int32 */ + interval?: number; + /** @format date-time */ + lastExecution?: string; + /** @format date-time */ + lastStartTime?: string; + /** @format date-time */ + nextExecution?: string; + lastDuration?: TimeSpan; +} + +export interface TimeSpan { + /** @format int64 */ + ticks?: number; + /** @format int32 */ + days?: number; + /** @format int32 */ + hours?: number; + /** @format int32 */ + milliseconds?: number; + /** @format int32 */ + minutes?: number; + /** @format int32 */ + seconds?: number; + /** @format double */ + totalDays?: number; + /** @format double */ + totalHours?: number; + /** @format double */ + totalMilliseconds?: number; + /** @format double */ + totalMinutes?: number; + /** @format double */ + totalSeconds?: number; +} + +export enum TrackedDownloadState { + Downloading = "downloading", + ImportPending = "importPending", + Importing = "importing", + Imported = "imported", + FailedPending = "failedPending", + Failed = "failed", + Ignored = "ignored", +} + +export enum TrackedDownloadStatus { + Ok = "ok", + Warning = "warning", + Error = "error", +} + +export interface TrackedDownloadStatusMessage { + title?: string | null; + messages?: string[] | null; +} + +export interface UiConfigResource { + /** @format int32 */ + id?: number; + /** @format int32 */ + firstDayOfWeek?: number; + calendarWeekColumnHeader?: string | null; + movieRuntimeFormat?: MovieRuntimeFormatType; + shortDateFormat?: string | null; + longDateFormat?: string | null; + timeFormat?: string | null; + showRelativeDates?: boolean; + enableColorImpairedMode?: boolean; + /** @format int32 */ + movieInfoLanguage?: number; + /** @format int32 */ + uiLanguage?: number; + theme?: string | null; +} + +export interface UnmappedFolder { + name?: string | null; + path?: string | null; + relativePath?: string | null; +} + +export interface UpdateChanges { + new?: string[] | null; + fixed?: string[] | null; +} + +export enum UpdateMechanism { + BuiltIn = "builtIn", + Script = "script", + External = "external", + Apt = "apt", + Docker = "docker", +} + +export interface UpdateResource { + /** @format int32 */ + id?: number; + version?: Version; + branch?: string | null; + /** @format date-time */ + releaseDate?: string; + fileName?: string | null; + url?: string | null; + installed?: boolean; + /** @format date-time */ + installedOn?: string | null; + installable?: boolean; + latest?: boolean; + changes?: UpdateChanges; + hash?: string | null; +} + +export interface Version { + /** @format int32 */ + major?: number; + /** @format int32 */ + minor?: number; + /** @format int32 */ + build?: number; + /** @format int32 */ + revision?: number; + /** @format int32 */ + majorRevision?: number; + /** @format int32 */ + minorRevision?: number; +} + +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, HeadersDefaults, ResponseType } from "axios"; +import axios from "axios"; + +export type QueryParamsType = Record; + +export interface FullRequestParams extends Omit { + /** set parameter to `true` for call `securityWorker` for this request */ + secure?: boolean; + /** request path */ + path: string; + /** content type of request body */ + type?: ContentType; + /** query params */ + query?: QueryParamsType; + /** format of response (i.e. response.json() -> format: "json") */ + format?: ResponseType; + /** request body */ + body?: unknown; +} + +export type RequestParams = Omit; + +export interface ApiConfig extends Omit { + securityWorker?: (securityData: SecurityDataType | null) => Promise | AxiosRequestConfig | void; + secure?: boolean; + format?: ResponseType; +} + +export enum ContentType { + Json = "application/json", + FormData = "multipart/form-data", + UrlEncoded = "application/x-www-form-urlencoded", + Text = "text/plain", +} + +export class HttpClient { + public instance: AxiosInstance; + private securityData: SecurityDataType | null = null; + private securityWorker?: ApiConfig["securityWorker"]; + private secure?: boolean; + private format?: ResponseType; + + constructor({ securityWorker, secure, format, ...axiosConfig }: ApiConfig = {}) { + this.instance = axios.create({ ...axiosConfig, baseURL: axiosConfig.baseURL || "{protocol}://{hostpath}" }); + this.secure = secure; + this.format = format; + this.securityWorker = securityWorker; + } + + public setSecurityData = (data: SecurityDataType | null) => { + this.securityData = data; + }; + + protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig { + const method = params1.method || (params2 && params2.method); + + return { + ...this.instance.defaults, + ...params1, + ...(params2 || {}), + headers: { + ...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}), + ...(params1.headers || {}), + ...((params2 && params2.headers) || {}), + }, + }; + } + + protected stringifyFormItem(formItem: unknown) { + if (typeof formItem === "object" && formItem !== null) { + return JSON.stringify(formItem); + } else { + return `${formItem}`; + } + } + + protected createFormData(input: Record): FormData { + return Object.keys(input || {}).reduce((formData, key) => { + const property = input[key]; + const propertyContent: any[] = property instanceof Array ? property : [property]; + + for (const formItem of propertyContent) { + const isFileType = formItem instanceof Blob || formItem instanceof File; + formData.append(key, isFileType ? formItem : this.stringifyFormItem(formItem)); + } + + return formData; + }, new FormData()); + } + + public request = async ({ + secure, + path, + type, + query, + format, + body, + ...params + }: FullRequestParams): Promise> => { + const secureParams = + ((typeof secure === "boolean" ? secure : this.secure) && this.securityWorker && (await this.securityWorker(this.securityData))) || {}; + const requestParams = this.mergeRequestParams(params, secureParams); + const responseFormat = format || this.format || undefined; + + if (type === ContentType.FormData && body && body !== null && typeof body === "object") { + body = this.createFormData(body as Record); + } + + if (type === ContentType.Text && body && body !== null && typeof body !== "string") { + body = JSON.stringify(body); + } + + return this.instance.request({ + ...requestParams, + headers: { + ...(requestParams.headers || {}), + ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), + }, + params: query, + responseType: responseFormat, + data: body, + url: path, + }); + }; +} + +/** + * @title Radarr + * @version 3.0.0 + * @license GPL-3.0 (https://github.com/Radarr/Radarr/blob/develop/LICENSE) + * @baseUrl {protocol}://{hostpath} + * + * Radarr API docs + */ +export class Api extends HttpClient { + /** + * No description + * + * @tags StaticResource + * @name GetRoot + * @request GET:/ + * @secure + */ + getRoot = (path: string, params: RequestParams = {}) => + this.request({ + path: `/`, + method: "GET", + secure: true, + ...params, + }); + + api = { + /** + * No description + * + * @tags AlternativeTitle + * @name V3AlttitleList + * @request GET:/api/v3/alttitle + * @secure + */ + v3AlttitleList: ( + query?: { + /** @format int32 */ + movieId?: number; + /** @format int32 */ + movieMetadataId?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/alttitle`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags AlternativeTitle + * @name V3AlttitleDetail + * @request GET:/api/v3/alttitle/{id} + * @secure + */ + v3AlttitleDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/alttitle/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ApiInfo + * @name GetApi + * @request GET:/api + * @secure + */ + getApi: (params: RequestParams = {}) => + this.request({ + path: `/api`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags AutoTagging + * @name V3AutotaggingCreate + * @request POST:/api/v3/autotagging + * @secure + */ + v3AutotaggingCreate: (data: AutoTaggingResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/autotagging`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags AutoTagging + * @name V3AutotaggingList + * @request GET:/api/v3/autotagging + * @secure + */ + v3AutotaggingList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/autotagging`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags AutoTagging + * @name V3AutotaggingUpdate + * @request PUT:/api/v3/autotagging/{id} + * @secure + */ + v3AutotaggingUpdate: (id: string, data: AutoTaggingResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/autotagging/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags AutoTagging + * @name V3AutotaggingDelete + * @request DELETE:/api/v3/autotagging/{id} + * @secure + */ + v3AutotaggingDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/autotagging/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags AutoTagging + * @name V3AutotaggingDetail + * @request GET:/api/v3/autotagging/{id} + * @secure + */ + v3AutotaggingDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/autotagging/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags AutoTagging + * @name V3AutotaggingSchemaList + * @request GET:/api/v3/autotagging/schema + * @secure + */ + v3AutotaggingSchemaList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/autotagging/schema`, + method: "GET", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Backup + * @name V3SystemBackupList + * @request GET:/api/v3/system/backup + * @secure + */ + v3SystemBackupList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/backup`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Backup + * @name V3SystemBackupDelete + * @request DELETE:/api/v3/system/backup/{id} + * @secure + */ + v3SystemBackupDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/backup/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Backup + * @name V3SystemBackupRestoreCreate + * @request POST:/api/v3/system/backup/restore/{id} + * @secure + */ + v3SystemBackupRestoreCreate: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/backup/restore/${id}`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Backup + * @name V3SystemBackupRestoreUploadCreate + * @request POST:/api/v3/system/backup/restore/upload + * @secure + */ + v3SystemBackupRestoreUploadCreate: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/backup/restore/upload`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Blocklist + * @name V3BlocklistList + * @request GET:/api/v3/blocklist + * @secure + */ + v3BlocklistList: ( + query?: { + /** + * @format int32 + * @default 1 + */ + page?: number; + /** + * @format int32 + * @default 10 + */ + pageSize?: number; + sortKey?: string; + sortDirection?: SortDirection; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/blocklist`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Blocklist + * @name V3BlocklistMovieList + * @request GET:/api/v3/blocklist/movie + * @secure + */ + v3BlocklistMovieList: ( + query?: { + /** @format int32 */ + movieId?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/blocklist/movie`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Blocklist + * @name V3BlocklistDelete + * @request DELETE:/api/v3/blocklist/{id} + * @secure + */ + v3BlocklistDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/blocklist/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Blocklist + * @name V3BlocklistBulkDelete + * @request DELETE:/api/v3/blocklist/bulk + * @secure + */ + v3BlocklistBulkDelete: (data: BlocklistBulkResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/blocklist/bulk`, + method: "DELETE", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags Calendar + * @name V3CalendarList + * @request GET:/api/v3/calendar + * @secure + */ + v3CalendarList: ( + query?: { + /** @format date-time */ + start?: string; + /** @format date-time */ + end?: string; + /** @default false */ + unmonitored?: boolean; + /** @default "" */ + tags?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/calendar`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Calendar + * @name V3CalendarDetail + * @request GET:/api/v3/calendar/{id} + * @secure + */ + v3CalendarDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/calendar/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Collection + * @name V3CollectionList + * @request GET:/api/v3/collection + * @secure + */ + v3CollectionList: ( + query?: { + /** @format int32 */ + tmdbId?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/collection`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Collection + * @name V3CollectionUpdate + * @request PUT:/api/v3/collection + * @secure + */ + v3CollectionUpdate: (data: CollectionUpdateResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/collection`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags Collection + * @name V3CollectionUpdate2 + * @request PUT:/api/v3/collection/{id} + * @originalName v3CollectionUpdate + * @duplicate + * @secure + */ + v3CollectionUpdate2: (id: string, data: CollectionResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/collection/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Collection + * @name V3CollectionDetail + * @request GET:/api/v3/collection/{id} + * @secure + */ + v3CollectionDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/collection/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Command + * @name V3CommandCreate + * @request POST:/api/v3/command + * @secure + */ + v3CommandCreate: (data: CommandResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/command`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Command + * @name V3CommandList + * @request GET:/api/v3/command + * @secure + */ + v3CommandList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/command`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Command + * @name V3CommandDelete + * @request DELETE:/api/v3/command/{id} + * @secure + */ + v3CommandDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/command/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Command + * @name V3CommandDetail + * @request GET:/api/v3/command/{id} + * @secure + */ + v3CommandDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/command/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Credit + * @name V3CreditList + * @request GET:/api/v3/credit + * @secure + */ + v3CreditList: ( + query?: { + /** @format int32 */ + movieId?: number; + /** @format int32 */ + movieMetadataId?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/credit`, + method: "GET", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Credit + * @name V3CreditDetail + * @request GET:/api/v3/credit/{id} + * @secure + */ + v3CreditDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/credit/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags CustomFilter + * @name V3CustomfilterList + * @request GET:/api/v3/customfilter + * @secure + */ + v3CustomfilterList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/customfilter`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags CustomFilter + * @name V3CustomfilterCreate + * @request POST:/api/v3/customfilter + * @secure + */ + v3CustomfilterCreate: (data: CustomFilterResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/customfilter`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags CustomFilter + * @name V3CustomfilterUpdate + * @request PUT:/api/v3/customfilter/{id} + * @secure + */ + v3CustomfilterUpdate: (id: string, data: CustomFilterResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/customfilter/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags CustomFilter + * @name V3CustomfilterDelete + * @request DELETE:/api/v3/customfilter/{id} + * @secure + */ + v3CustomfilterDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/customfilter/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags CustomFilter + * @name V3CustomfilterDetail + * @request GET:/api/v3/customfilter/{id} + * @secure + */ + v3CustomfilterDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/customfilter/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags CustomFormat + * @name V3CustomformatCreate + * @request POST:/api/v3/customformat + * @secure + */ + v3CustomformatCreate: (data: CustomFormatResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/customformat`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags CustomFormat + * @name V3CustomformatList + * @request GET:/api/v3/customformat + * @secure + */ + v3CustomformatList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/customformat`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags CustomFormat + * @name V3CustomformatUpdate + * @request PUT:/api/v3/customformat/{id} + * @secure + */ + v3CustomformatUpdate: (id: string, data: CustomFormatResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/customformat/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags CustomFormat + * @name V3CustomformatDelete + * @request DELETE:/api/v3/customformat/{id} + * @secure + */ + v3CustomformatDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/customformat/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags CustomFormat + * @name V3CustomformatDetail + * @request GET:/api/v3/customformat/{id} + * @secure + */ + v3CustomformatDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/customformat/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags CustomFormat + * @name V3CustomformatSchemaList + * @request GET:/api/v3/customformat/schema + * @secure + */ + v3CustomformatSchemaList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/customformat/schema`, + method: "GET", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags DelayProfile + * @name V3DelayprofileCreate + * @request POST:/api/v3/delayprofile + * @secure + */ + v3DelayprofileCreate: (data: DelayProfileResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/delayprofile`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DelayProfile + * @name V3DelayprofileList + * @request GET:/api/v3/delayprofile + * @secure + */ + v3DelayprofileList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/delayprofile`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DelayProfile + * @name V3DelayprofileDelete + * @request DELETE:/api/v3/delayprofile/{id} + * @secure + */ + v3DelayprofileDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/delayprofile/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags DelayProfile + * @name V3DelayprofileUpdate + * @request PUT:/api/v3/delayprofile/{id} + * @secure + */ + v3DelayprofileUpdate: (id: string, data: DelayProfileResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/delayprofile/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DelayProfile + * @name V3DelayprofileDetail + * @request GET:/api/v3/delayprofile/{id} + * @secure + */ + v3DelayprofileDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/delayprofile/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DiskSpace + * @name V3DiskspaceList + * @request GET:/api/v3/diskspace + * @secure + */ + v3DiskspaceList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/diskspace`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientList + * @request GET:/api/v3/downloadclient + * @secure + */ + v3DownloadclientList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/downloadclient`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientCreate + * @request POST:/api/v3/downloadclient + * @secure + */ + v3DownloadclientCreate: ( + data: DownloadClientResource, + query?: { + /** @default false */ + forceSave?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/downloadclient`, + method: "POST", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientUpdate + * @request PUT:/api/v3/downloadclient/{id} + * @secure + */ + v3DownloadclientUpdate: ( + id: string, + data: DownloadClientResource, + query?: { + /** @default false */ + forceSave?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/downloadclient/${id}`, + method: "PUT", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientDelete + * @request DELETE:/api/v3/downloadclient/{id} + * @secure + */ + v3DownloadclientDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/downloadclient/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientDetail + * @request GET:/api/v3/downloadclient/{id} + * @secure + */ + v3DownloadclientDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/downloadclient/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientBulkUpdate + * @request PUT:/api/v3/downloadclient/bulk + * @secure + */ + v3DownloadclientBulkUpdate: (data: DownloadClientBulkResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/downloadclient/bulk`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientBulkDelete + * @request DELETE:/api/v3/downloadclient/bulk + * @secure + */ + v3DownloadclientBulkDelete: (data: DownloadClientBulkResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/downloadclient/bulk`, + method: "DELETE", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientSchemaList + * @request GET:/api/v3/downloadclient/schema + * @secure + */ + v3DownloadclientSchemaList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/downloadclient/schema`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientTestCreate + * @request POST:/api/v3/downloadclient/test + * @secure + */ + v3DownloadclientTestCreate: (data: DownloadClientResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/downloadclient/test`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientTestallCreate + * @request POST:/api/v3/downloadclient/testall + * @secure + */ + v3DownloadclientTestallCreate: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/downloadclient/testall`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags DownloadClient + * @name V3DownloadclientActionCreate + * @request POST:/api/v3/downloadclient/action/{name} + * @secure + */ + v3DownloadclientActionCreate: (name: string, data: DownloadClientResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/downloadclient/action/${name}`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags DownloadClientConfig + * @name V3ConfigDownloadclientList + * @request GET:/api/v3/config/downloadclient + * @secure + */ + v3ConfigDownloadclientList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/downloadclient`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DownloadClientConfig + * @name V3ConfigDownloadclientUpdate + * @request PUT:/api/v3/config/downloadclient/{id} + * @secure + */ + v3ConfigDownloadclientUpdate: (id: string, data: DownloadClientConfigResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/downloadclient/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags DownloadClientConfig + * @name V3ConfigDownloadclientDetail + * @request GET:/api/v3/config/downloadclient/{id} + * @secure + */ + v3ConfigDownloadclientDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/downloadclient/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ExtraFile + * @name V3ExtrafileList + * @request GET:/api/v3/extrafile + * @secure + */ + v3ExtrafileList: ( + query?: { + /** @format int32 */ + movieId?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/extrafile`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags FileSystem + * @name V3FilesystemList + * @request GET:/api/v3/filesystem + * @secure + */ + v3FilesystemList: ( + query?: { + path?: string; + /** @default false */ + includeFiles?: boolean; + /** @default false */ + allowFoldersWithoutTrailingSlashes?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/filesystem`, + method: "GET", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags FileSystem + * @name V3FilesystemTypeList + * @request GET:/api/v3/filesystem/type + * @secure + */ + v3FilesystemTypeList: ( + query?: { + path?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/filesystem/type`, + method: "GET", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags FileSystem + * @name V3FilesystemMediafilesList + * @request GET:/api/v3/filesystem/mediafiles + * @secure + */ + v3FilesystemMediafilesList: ( + query?: { + path?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/filesystem/mediafiles`, + method: "GET", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Health + * @name V3HealthList + * @request GET:/api/v3/health + * @secure + */ + v3HealthList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/health`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Health + * @name V3HealthDetail + * @request GET:/api/v3/health/{id} + * @secure + */ + v3HealthDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/health/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags History + * @name V3HistoryList + * @request GET:/api/v3/history + * @secure + */ + v3HistoryList: ( + query?: { + /** + * @format int32 + * @default 1 + */ + page?: number; + /** + * @format int32 + * @default 10 + */ + pageSize?: number; + sortKey?: string; + sortDirection?: SortDirection; + includeMovie?: boolean; + eventType?: number[]; + downloadId?: string; + movieIds?: number[]; + languages?: number[]; + quality?: number[]; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/history`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags History + * @name V3HistorySinceList + * @request GET:/api/v3/history/since + * @secure + */ + v3HistorySinceList: ( + query?: { + /** @format date-time */ + date?: string; + eventType?: MovieHistoryEventType; + /** @default false */ + includeMovie?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/history/since`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags History + * @name V3HistoryMovieList + * @request GET:/api/v3/history/movie + * @secure + */ + v3HistoryMovieList: ( + query?: { + /** @format int32 */ + movieId?: number; + eventType?: MovieHistoryEventType; + /** @default false */ + includeMovie?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/history/movie`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags History + * @name V3HistoryFailedCreate + * @request POST:/api/v3/history/failed/{id} + * @secure + */ + v3HistoryFailedCreate: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/history/failed/${id}`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags HostConfig + * @name V3ConfigHostList + * @request GET:/api/v3/config/host + * @secure + */ + v3ConfigHostList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/host`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags HostConfig + * @name V3ConfigHostUpdate + * @request PUT:/api/v3/config/host/{id} + * @secure + */ + v3ConfigHostUpdate: (id: string, data: HostConfigResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/host/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags HostConfig + * @name V3ConfigHostDetail + * @request GET:/api/v3/config/host/{id} + * @secure + */ + v3ConfigHostDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/host/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportExclusions + * @name V3ExclusionsList + * @request GET:/api/v3/exclusions + * @secure + */ + v3ExclusionsList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/exclusions`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportExclusions + * @name V3ExclusionsCreate + * @request POST:/api/v3/exclusions + * @secure + */ + v3ExclusionsCreate: (data: ImportExclusionsResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/exclusions`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportExclusions + * @name V3ExclusionsUpdate + * @request PUT:/api/v3/exclusions/{id} + * @secure + */ + v3ExclusionsUpdate: (id: string, data: ImportExclusionsResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/exclusions/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportExclusions + * @name V3ExclusionsDelete + * @request DELETE:/api/v3/exclusions/{id} + * @secure + */ + v3ExclusionsDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/exclusions/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags ImportExclusions + * @name V3ExclusionsDetail + * @request GET:/api/v3/exclusions/{id} + * @secure + */ + v3ExclusionsDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/exclusions/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportExclusions + * @name V3ExclusionsBulkCreate + * @request POST:/api/v3/exclusions/bulk + * @secure + */ + v3ExclusionsBulkCreate: (data: ImportExclusionsResource[], params: RequestParams = {}) => + this.request({ + path: `/api/v3/exclusions/bulk`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistList + * @request GET:/api/v3/importlist + * @secure + */ + v3ImportlistList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/importlist`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistCreate + * @request POST:/api/v3/importlist + * @secure + */ + v3ImportlistCreate: ( + data: ImportListResource, + query?: { + /** @default false */ + forceSave?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/importlist`, + method: "POST", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistUpdate + * @request PUT:/api/v3/importlist/{id} + * @secure + */ + v3ImportlistUpdate: ( + id: string, + data: ImportListResource, + query?: { + /** @default false */ + forceSave?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/importlist/${id}`, + method: "PUT", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistDelete + * @request DELETE:/api/v3/importlist/{id} + * @secure + */ + v3ImportlistDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/importlist/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistDetail + * @request GET:/api/v3/importlist/{id} + * @secure + */ + v3ImportlistDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/importlist/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistBulkUpdate + * @request PUT:/api/v3/importlist/bulk + * @secure + */ + v3ImportlistBulkUpdate: (data: ImportListBulkResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/importlist/bulk`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistBulkDelete + * @request DELETE:/api/v3/importlist/bulk + * @secure + */ + v3ImportlistBulkDelete: (data: ImportListBulkResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/importlist/bulk`, + method: "DELETE", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistSchemaList + * @request GET:/api/v3/importlist/schema + * @secure + */ + v3ImportlistSchemaList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/importlist/schema`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistTestCreate + * @request POST:/api/v3/importlist/test + * @secure + */ + v3ImportlistTestCreate: (data: ImportListResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/importlist/test`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistTestallCreate + * @request POST:/api/v3/importlist/testall + * @secure + */ + v3ImportlistTestallCreate: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/importlist/testall`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags ImportList + * @name V3ImportlistActionCreate + * @request POST:/api/v3/importlist/action/{name} + * @secure + */ + v3ImportlistActionCreate: (name: string, data: ImportListResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/importlist/action/${name}`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags ImportListConfig + * @name V3ConfigImportlistList + * @request GET:/api/v3/config/importlist + * @secure + */ + v3ConfigImportlistList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/importlist`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportListConfig + * @name V3ConfigImportlistUpdate + * @request PUT:/api/v3/config/importlist/{id} + * @secure + */ + v3ConfigImportlistUpdate: (id: string, data: ImportListConfigResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/importlist/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportListConfig + * @name V3ConfigImportlistDetail + * @request GET:/api/v3/config/importlist/{id} + * @secure + */ + v3ConfigImportlistDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/importlist/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ImportListMovies + * @name V3ImportlistMovieList + * @request GET:/api/v3/importlist/movie + * @secure + */ + v3ImportlistMovieList: ( + query?: { + /** @default false */ + includeRecommendations?: boolean; + /** @default false */ + includeTrending?: boolean; + /** @default false */ + includePopular?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/importlist/movie`, + method: "GET", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags ImportListMovies + * @name V3ImportlistMovieCreate + * @request POST:/api/v3/importlist/movie + * @secure + */ + v3ImportlistMovieCreate: (data: MovieResource[], params: RequestParams = {}) => + this.request({ + path: `/api/v3/importlist/movie`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerList + * @request GET:/api/v3/indexer + * @secure + */ + v3IndexerList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/indexer`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerCreate + * @request POST:/api/v3/indexer + * @secure + */ + v3IndexerCreate: ( + data: IndexerResource, + query?: { + /** @default false */ + forceSave?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/indexer`, + method: "POST", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerUpdate + * @request PUT:/api/v3/indexer/{id} + * @secure + */ + v3IndexerUpdate: ( + id: string, + data: IndexerResource, + query?: { + /** @default false */ + forceSave?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/indexer/${id}`, + method: "PUT", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerDelete + * @request DELETE:/api/v3/indexer/{id} + * @secure + */ + v3IndexerDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/indexer/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerDetail + * @request GET:/api/v3/indexer/{id} + * @secure + */ + v3IndexerDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/indexer/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerBulkUpdate + * @request PUT:/api/v3/indexer/bulk + * @secure + */ + v3IndexerBulkUpdate: (data: IndexerBulkResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/indexer/bulk`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerBulkDelete + * @request DELETE:/api/v3/indexer/bulk + * @secure + */ + v3IndexerBulkDelete: (data: IndexerBulkResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/indexer/bulk`, + method: "DELETE", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerSchemaList + * @request GET:/api/v3/indexer/schema + * @secure + */ + v3IndexerSchemaList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/indexer/schema`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerTestCreate + * @request POST:/api/v3/indexer/test + * @secure + */ + v3IndexerTestCreate: (data: IndexerResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/indexer/test`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerTestallCreate + * @request POST:/api/v3/indexer/testall + * @secure + */ + v3IndexerTestallCreate: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/indexer/testall`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Indexer + * @name V3IndexerActionCreate + * @request POST:/api/v3/indexer/action/{name} + * @secure + */ + v3IndexerActionCreate: (name: string, data: IndexerResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/indexer/action/${name}`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags IndexerConfig + * @name V3ConfigIndexerList + * @request GET:/api/v3/config/indexer + * @secure + */ + v3ConfigIndexerList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/indexer`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags IndexerConfig + * @name V3ConfigIndexerUpdate + * @request PUT:/api/v3/config/indexer/{id} + * @secure + */ + v3ConfigIndexerUpdate: (id: string, data: IndexerConfigResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/indexer/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags IndexerConfig + * @name V3ConfigIndexerDetail + * @request GET:/api/v3/config/indexer/{id} + * @secure + */ + v3ConfigIndexerDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/indexer/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags IndexerFlag + * @name V3IndexerflagList + * @request GET:/api/v3/indexerflag + * @secure + */ + v3IndexerflagList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/indexerflag`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Language + * @name V3LanguageList + * @request GET:/api/v3/language + * @secure + */ + v3LanguageList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/language`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Language + * @name V3LanguageDetail + * @request GET:/api/v3/language/{id} + * @secure + */ + v3LanguageDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/language/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Localization + * @name V3LocalizationList + * @request GET:/api/v3/localization + * @secure + */ + v3LocalizationList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/localization`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Localization + * @name V3LocalizationLanguageList + * @request GET:/api/v3/localization/language + * @secure + */ + v3LocalizationLanguageList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/localization/language`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Log + * @name V3LogList + * @request GET:/api/v3/log + * @secure + */ + v3LogList: ( + query?: { + /** + * @format int32 + * @default 1 + */ + page?: number; + /** + * @format int32 + * @default 10 + */ + pageSize?: number; + sortKey?: string; + sortDirection?: SortDirection; + level?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/log`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags LogFile + * @name V3LogFileList + * @request GET:/api/v3/log/file + * @secure + */ + v3LogFileList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/log/file`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags LogFile + * @name V3LogFileDetail + * @request GET:/api/v3/log/file/{filename} + * @secure + */ + v3LogFileDetail: (filename: string, params: RequestParams = {}) => + this.request({ + path: `/api/v3/log/file/${filename}`, + method: "GET", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags ManualImport + * @name V3ManualimportList + * @request GET:/api/v3/manualimport + * @secure + */ + v3ManualimportList: ( + query?: { + folder?: string; + downloadId?: string; + /** @format int32 */ + movieId?: number; + /** @default true */ + filterExistingFiles?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/manualimport`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ManualImport + * @name V3ManualimportCreate + * @request POST:/api/v3/manualimport + * @secure + */ + v3ManualimportCreate: (data: ManualImportReprocessResource[], params: RequestParams = {}) => + this.request({ + path: `/api/v3/manualimport`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags MediaCover + * @name V3MediacoverDetail + * @request GET:/api/v3/mediacover/{movieId}/{filename} + * @secure + */ + v3MediacoverDetail: (movieId: number, filename: string, params: RequestParams = {}) => + this.request({ + path: `/api/v3/mediacover/${movieId}/${filename}`, + method: "GET", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags MediaManagementConfig + * @name V3ConfigMediamanagementList + * @request GET:/api/v3/config/mediamanagement + * @secure + */ + v3ConfigMediamanagementList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/mediamanagement`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags MediaManagementConfig + * @name V3ConfigMediamanagementUpdate + * @request PUT:/api/v3/config/mediamanagement/{id} + * @secure + */ + v3ConfigMediamanagementUpdate: (id: string, data: MediaManagementConfigResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/mediamanagement/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags MediaManagementConfig + * @name V3ConfigMediamanagementDetail + * @request GET:/api/v3/config/mediamanagement/{id} + * @secure + */ + v3ConfigMediamanagementDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/mediamanagement/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Metadata + * @name V3MetadataList + * @request GET:/api/v3/metadata + * @secure + */ + v3MetadataList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/metadata`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Metadata + * @name V3MetadataCreate + * @request POST:/api/v3/metadata + * @secure + */ + v3MetadataCreate: ( + data: MetadataResource, + query?: { + /** @default false */ + forceSave?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/metadata`, + method: "POST", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Metadata + * @name V3MetadataUpdate + * @request PUT:/api/v3/metadata/{id} + * @secure + */ + v3MetadataUpdate: ( + id: string, + data: MetadataResource, + query?: { + /** @default false */ + forceSave?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/metadata/${id}`, + method: "PUT", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Metadata + * @name V3MetadataDelete + * @request DELETE:/api/v3/metadata/{id} + * @secure + */ + v3MetadataDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/metadata/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Metadata + * @name V3MetadataDetail + * @request GET:/api/v3/metadata/{id} + * @secure + */ + v3MetadataDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/metadata/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Metadata + * @name V3MetadataSchemaList + * @request GET:/api/v3/metadata/schema + * @secure + */ + v3MetadataSchemaList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/metadata/schema`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Metadata + * @name V3MetadataTestCreate + * @request POST:/api/v3/metadata/test + * @secure + */ + v3MetadataTestCreate: (data: MetadataResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/metadata/test`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags Metadata + * @name V3MetadataTestallCreate + * @request POST:/api/v3/metadata/testall + * @secure + */ + v3MetadataTestallCreate: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/metadata/testall`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Metadata + * @name V3MetadataActionCreate + * @request POST:/api/v3/metadata/action/{name} + * @secure + */ + v3MetadataActionCreate: (name: string, data: MetadataResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/metadata/action/${name}`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags MetadataConfig + * @name V3ConfigMetadataList + * @request GET:/api/v3/config/metadata + * @secure + */ + v3ConfigMetadataList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/metadata`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags MetadataConfig + * @name V3ConfigMetadataUpdate + * @request PUT:/api/v3/config/metadata/{id} + * @secure + */ + v3ConfigMetadataUpdate: (id: string, data: MetadataConfigResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/metadata/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags MetadataConfig + * @name V3ConfigMetadataDetail + * @request GET:/api/v3/config/metadata/{id} + * @secure + */ + v3ConfigMetadataDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/metadata/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Movie + * @name V3MovieList + * @request GET:/api/v3/movie + * @secure + */ + v3MovieList: ( + query?: { + /** @format int32 */ + tmdbId?: number; + /** @default false */ + excludeLocalCovers?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/movie`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Movie + * @name V3MovieCreate + * @request POST:/api/v3/movie + * @secure + */ + v3MovieCreate: (data: MovieResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/movie`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Movie + * @name V3MovieUpdate + * @request PUT:/api/v3/movie/{id} + * @secure + */ + v3MovieUpdate: ( + id: string, + data: MovieResource, + query?: { + /** @default false */ + moveFiles?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/movie/${id}`, + method: "PUT", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Movie + * @name V3MovieDelete + * @request DELETE:/api/v3/movie/{id} + * @secure + */ + v3MovieDelete: ( + id: number, + query?: { + /** @default false */ + deleteFiles?: boolean; + /** @default false */ + addImportExclusion?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/movie/${id}`, + method: "DELETE", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Movie + * @name V3MovieDetail + * @request GET:/api/v3/movie/{id} + * @secure + */ + v3MovieDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/movie/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags MovieEditor + * @name V3MovieEditorUpdate + * @request PUT:/api/v3/movie/editor + * @secure + */ + v3MovieEditorUpdate: (data: MovieEditorResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/movie/editor`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags MovieEditor + * @name V3MovieEditorDelete + * @request DELETE:/api/v3/movie/editor + * @secure + */ + v3MovieEditorDelete: (data: MovieEditorResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/movie/editor`, + method: "DELETE", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags MovieFile + * @name V3MoviefileList + * @request GET:/api/v3/moviefile + * @secure + */ + v3MoviefileList: ( + query?: { + movieId?: number[]; + movieFileIds?: number[]; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/moviefile`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags MovieFile + * @name V3MoviefileUpdate + * @request PUT:/api/v3/moviefile/{id} + * @secure + */ + v3MoviefileUpdate: (id: string, data: MovieFileResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/moviefile/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags MovieFile + * @name V3MoviefileDelete + * @request DELETE:/api/v3/moviefile/{id} + * @secure + */ + v3MoviefileDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/moviefile/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags MovieFile + * @name V3MoviefileDetail + * @request GET:/api/v3/moviefile/{id} + * @secure + */ + v3MoviefileDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/moviefile/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags MovieFile + * @name V3MoviefileEditorUpdate + * @request PUT:/api/v3/moviefile/editor + * @secure + */ + v3MoviefileEditorUpdate: (data: MovieFileListResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/moviefile/editor`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags MovieFile + * @name V3MoviefileBulkDelete + * @request DELETE:/api/v3/moviefile/bulk + * @secure + */ + v3MoviefileBulkDelete: (data: MovieFileListResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/moviefile/bulk`, + method: "DELETE", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags MovieImport + * @name V3MovieImportCreate + * @request POST:/api/v3/movie/import + * @secure + */ + v3MovieImportCreate: (data: MovieResource[], params: RequestParams = {}) => + this.request({ + path: `/api/v3/movie/import`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags MovieImport + * @name V3MovieImportDetail + * @request GET:/api/v3/movie/import/{id} + * @secure + */ + v3MovieImportDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/movie/import/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags MovieLookup + * @name V3MovieLookupTmdbList + * @request GET:/api/v3/movie/lookup/tmdb + * @secure + */ + v3MovieLookupTmdbList: ( + query?: { + /** @format int32 */ + tmdbId?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/movie/lookup/tmdb`, + method: "GET", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags MovieLookup + * @name V3MovieLookupImdbList + * @request GET:/api/v3/movie/lookup/imdb + * @secure + */ + v3MovieLookupImdbList: ( + query?: { + imdbId?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/movie/lookup/imdb`, + method: "GET", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags MovieLookup + * @name V3MovieLookupList + * @request GET:/api/v3/movie/lookup + * @secure + */ + v3MovieLookupList: ( + query?: { + term?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/movie/lookup`, + method: "GET", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags MovieLookup + * @name V3MovieLookupDetail + * @request GET:/api/v3/movie/lookup/{id} + * @secure + */ + v3MovieLookupDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/movie/lookup/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags NamingConfig + * @name V3ConfigNamingList + * @request GET:/api/v3/config/naming + * @secure + */ + v3ConfigNamingList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/naming`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags NamingConfig + * @name V3ConfigNamingUpdate + * @request PUT:/api/v3/config/naming/{id} + * @secure + */ + v3ConfigNamingUpdate: (id: string, data: NamingConfigResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/naming/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags NamingConfig + * @name V3ConfigNamingDetail + * @request GET:/api/v3/config/naming/{id} + * @secure + */ + v3ConfigNamingDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/naming/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags NamingConfig + * @name V3ConfigNamingExamplesList + * @request GET:/api/v3/config/naming/examples + * @secure + */ + v3ConfigNamingExamplesList: ( + query?: { + renameMovies?: boolean; + replaceIllegalCharacters?: boolean; + colonReplacementFormat?: ColonReplacementFormat; + standardMovieFormat?: string; + movieFolderFormat?: string; + /** @format int32 */ + id?: number; + resourceName?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/config/naming/examples`, + method: "GET", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Notification + * @name V3NotificationList + * @request GET:/api/v3/notification + * @secure + */ + v3NotificationList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/notification`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Notification + * @name V3NotificationCreate + * @request POST:/api/v3/notification + * @secure + */ + v3NotificationCreate: ( + data: NotificationResource, + query?: { + /** @default false */ + forceSave?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/notification`, + method: "POST", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Notification + * @name V3NotificationUpdate + * @request PUT:/api/v3/notification/{id} + * @secure + */ + v3NotificationUpdate: ( + id: string, + data: NotificationResource, + query?: { + /** @default false */ + forceSave?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/notification/${id}`, + method: "PUT", + query: query, + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Notification + * @name V3NotificationDelete + * @request DELETE:/api/v3/notification/{id} + * @secure + */ + v3NotificationDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/notification/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Notification + * @name V3NotificationDetail + * @request GET:/api/v3/notification/{id} + * @secure + */ + v3NotificationDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/notification/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Notification + * @name V3NotificationSchemaList + * @request GET:/api/v3/notification/schema + * @secure + */ + v3NotificationSchemaList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/notification/schema`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Notification + * @name V3NotificationTestCreate + * @request POST:/api/v3/notification/test + * @secure + */ + v3NotificationTestCreate: (data: NotificationResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/notification/test`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags Notification + * @name V3NotificationTestallCreate + * @request POST:/api/v3/notification/testall + * @secure + */ + v3NotificationTestallCreate: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/notification/testall`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Notification + * @name V3NotificationActionCreate + * @request POST:/api/v3/notification/action/{name} + * @secure + */ + v3NotificationActionCreate: (name: string, data: NotificationResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/notification/action/${name}`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags Parse + * @name V3ParseList + * @request GET:/api/v3/parse + * @secure + */ + v3ParseList: ( + query?: { + title?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/parse`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QualityDefinition + * @name V3QualitydefinitionUpdate + * @request PUT:/api/v3/qualitydefinition/{id} + * @secure + */ + v3QualitydefinitionUpdate: (id: string, data: QualityDefinitionResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/qualitydefinition/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QualityDefinition + * @name V3QualitydefinitionDetail + * @request GET:/api/v3/qualitydefinition/{id} + * @secure + */ + v3QualitydefinitionDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/qualitydefinition/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QualityDefinition + * @name V3QualitydefinitionList + * @request GET:/api/v3/qualitydefinition + * @secure + */ + v3QualitydefinitionList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/qualitydefinition`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QualityDefinition + * @name V3QualitydefinitionUpdateUpdate + * @request PUT:/api/v3/qualitydefinition/update + * @secure + */ + v3QualitydefinitionUpdateUpdate: (data: QualityDefinitionResource[], params: RequestParams = {}) => + this.request({ + path: `/api/v3/qualitydefinition/update`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags QualityProfile + * @name V3QualityprofileCreate + * @request POST:/api/v3/qualityprofile + * @secure + */ + v3QualityprofileCreate: (data: QualityProfileResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/qualityprofile`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QualityProfile + * @name V3QualityprofileList + * @request GET:/api/v3/qualityprofile + * @secure + */ + v3QualityprofileList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/qualityprofile`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QualityProfile + * @name V3QualityprofileDelete + * @request DELETE:/api/v3/qualityprofile/{id} + * @secure + */ + v3QualityprofileDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/qualityprofile/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags QualityProfile + * @name V3QualityprofileUpdate + * @request PUT:/api/v3/qualityprofile/{id} + * @secure + */ + v3QualityprofileUpdate: (id: string, data: QualityProfileResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/qualityprofile/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QualityProfile + * @name V3QualityprofileDetail + * @request GET:/api/v3/qualityprofile/{id} + * @secure + */ + v3QualityprofileDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/qualityprofile/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QualityProfileSchema + * @name V3QualityprofileSchemaList + * @request GET:/api/v3/qualityprofile/schema + * @secure + */ + v3QualityprofileSchemaList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/qualityprofile/schema`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Queue + * @name V3QueueDelete + * @request DELETE:/api/v3/queue/{id} + * @secure + */ + v3QueueDelete: ( + id: number, + query?: { + /** @default true */ + removeFromClient?: boolean; + /** @default false */ + blocklist?: boolean; + /** @default false */ + skipRedownload?: boolean; + /** @default false */ + changeCategory?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/queue/${id}`, + method: "DELETE", + query: query, + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Queue + * @name V3QueueDetail + * @request GET:/api/v3/queue/{id} + * @secure + */ + v3QueueDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/queue/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Queue + * @name V3QueueBulkDelete + * @request DELETE:/api/v3/queue/bulk + * @secure + */ + v3QueueBulkDelete: ( + data: QueueBulkResource, + query?: { + /** @default true */ + removeFromClient?: boolean; + /** @default false */ + blocklist?: boolean; + /** @default false */ + skipRedownload?: boolean; + /** @default false */ + changeCategory?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/queue/bulk`, + method: "DELETE", + query: query, + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags Queue + * @name V3QueueList + * @request GET:/api/v3/queue + * @secure + */ + v3QueueList: ( + query?: { + /** + * @format int32 + * @default 1 + */ + page?: number; + /** + * @format int32 + * @default 10 + */ + pageSize?: number; + sortKey?: string; + sortDirection?: SortDirection; + /** @default false */ + includeUnknownMovieItems?: boolean; + /** @default false */ + includeMovie?: boolean; + movieIds?: number[]; + protocol?: DownloadProtocol; + languages?: number[]; + /** @format int32 */ + quality?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/queue`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QueueAction + * @name V3QueueGrabCreate + * @request POST:/api/v3/queue/grab/{id} + * @secure + */ + v3QueueGrabCreate: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/queue/grab/${id}`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags QueueAction + * @name V3QueueGrabBulkCreate + * @request POST:/api/v3/queue/grab/bulk + * @secure + */ + v3QueueGrabBulkCreate: (data: QueueBulkResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/queue/grab/bulk`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags QueueDetails + * @name V3QueueDetailsList + * @request GET:/api/v3/queue/details + * @secure + */ + v3QueueDetailsList: ( + query?: { + /** @format int32 */ + movieId?: number; + /** @default false */ + includeMovie?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/queue/details`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QueueDetails + * @name V3QueueDetailsDetail + * @request GET:/api/v3/queue/details/{id} + * @secure + */ + v3QueueDetailsDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/queue/details/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QueueStatus + * @name V3QueueStatusList + * @request GET:/api/v3/queue/status + * @secure + */ + v3QueueStatusList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/queue/status`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags QueueStatus + * @name V3QueueStatusDetail + * @request GET:/api/v3/queue/status/{id} + * @secure + */ + v3QueueStatusDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/queue/status/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Release + * @name V3ReleaseCreate + * @request POST:/api/v3/release + * @secure + */ + v3ReleaseCreate: (data: ReleaseResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/release`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + ...params, + }), + + /** + * No description + * + * @tags Release + * @name V3ReleaseList + * @request GET:/api/v3/release + * @secure + */ + v3ReleaseList: ( + query?: { + /** @format int32 */ + movieId?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/release`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Release + * @name V3ReleaseDetail + * @request GET:/api/v3/release/{id} + * @secure + */ + v3ReleaseDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/release/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ReleaseProfile + * @name V3ReleaseprofileCreate + * @request POST:/api/v3/releaseprofile + * @secure + */ + v3ReleaseprofileCreate: (data: ReleaseProfileResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/releaseprofile`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ReleaseProfile + * @name V3ReleaseprofileList + * @request GET:/api/v3/releaseprofile + * @secure + */ + v3ReleaseprofileList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/releaseprofile`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ReleaseProfile + * @name V3ReleaseprofileDelete + * @request DELETE:/api/v3/releaseprofile/{id} + * @secure + */ + v3ReleaseprofileDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/releaseprofile/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags ReleaseProfile + * @name V3ReleaseprofileUpdate + * @request PUT:/api/v3/releaseprofile/{id} + * @secure + */ + v3ReleaseprofileUpdate: (id: string, data: ReleaseProfileResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/releaseprofile/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ReleaseProfile + * @name V3ReleaseprofileDetail + * @request GET:/api/v3/releaseprofile/{id} + * @secure + */ + v3ReleaseprofileDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/releaseprofile/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ReleasePush + * @name V3ReleasePushCreate + * @request POST:/api/v3/release/push + * @secure + */ + v3ReleasePushCreate: (data: ReleaseResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/release/push`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags ReleasePush + * @name V3ReleasePushDetail + * @request GET:/api/v3/release/push/{id} + * @secure + */ + v3ReleasePushDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/release/push/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags RemotePathMapping + * @name V3RemotepathmappingCreate + * @request POST:/api/v3/remotepathmapping + * @secure + */ + v3RemotepathmappingCreate: (data: RemotePathMappingResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/remotepathmapping`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags RemotePathMapping + * @name V3RemotepathmappingList + * @request GET:/api/v3/remotepathmapping + * @secure + */ + v3RemotepathmappingList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/remotepathmapping`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags RemotePathMapping + * @name V3RemotepathmappingDelete + * @request DELETE:/api/v3/remotepathmapping/{id} + * @secure + */ + v3RemotepathmappingDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/remotepathmapping/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags RemotePathMapping + * @name V3RemotepathmappingUpdate + * @request PUT:/api/v3/remotepathmapping/{id} + * @secure + */ + v3RemotepathmappingUpdate: (id: string, data: RemotePathMappingResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/remotepathmapping/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags RemotePathMapping + * @name V3RemotepathmappingDetail + * @request GET:/api/v3/remotepathmapping/{id} + * @secure + */ + v3RemotepathmappingDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/remotepathmapping/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags RenameMovie + * @name V3RenameList + * @request GET:/api/v3/rename + * @secure + */ + v3RenameList: ( + query?: { + /** @format int32 */ + movieId?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/v3/rename`, + method: "GET", + query: query, + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags RootFolder + * @name V3RootfolderCreate + * @request POST:/api/v3/rootfolder + * @secure + */ + v3RootfolderCreate: (data: RootFolderResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/rootfolder`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags RootFolder + * @name V3RootfolderList + * @request GET:/api/v3/rootfolder + * @secure + */ + v3RootfolderList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/rootfolder`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags RootFolder + * @name V3RootfolderDelete + * @request DELETE:/api/v3/rootfolder/{id} + * @secure + */ + v3RootfolderDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/rootfolder/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags RootFolder + * @name V3RootfolderDetail + * @request GET:/api/v3/rootfolder/{id} + * @secure + */ + v3RootfolderDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/rootfolder/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags System + * @name V3SystemStatusList + * @request GET:/api/v3/system/status + * @secure + */ + v3SystemStatusList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/status`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags System + * @name V3SystemRoutesList + * @request GET:/api/v3/system/routes + * @secure + */ + v3SystemRoutesList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/routes`, + method: "GET", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags System + * @name V3SystemRoutesDuplicateList + * @request GET:/api/v3/system/routes/duplicate + * @secure + */ + v3SystemRoutesDuplicateList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/routes/duplicate`, + method: "GET", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags System + * @name V3SystemShutdownCreate + * @request POST:/api/v3/system/shutdown + * @secure + */ + v3SystemShutdownCreate: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/shutdown`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags System + * @name V3SystemRestartCreate + * @request POST:/api/v3/system/restart + * @secure + */ + v3SystemRestartCreate: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/restart`, + method: "POST", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Tag + * @name V3TagList + * @request GET:/api/v3/tag + * @secure + */ + v3TagList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/tag`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Tag + * @name V3TagCreate + * @request POST:/api/v3/tag + * @secure + */ + v3TagCreate: (data: TagResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/tag`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Tag + * @name V3TagUpdate + * @request PUT:/api/v3/tag/{id} + * @secure + */ + v3TagUpdate: (id: string, data: TagResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/tag/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Tag + * @name V3TagDelete + * @request DELETE:/api/v3/tag/{id} + * @secure + */ + v3TagDelete: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/tag/${id}`, + method: "DELETE", + secure: true, + ...params, + }), + + /** + * No description + * + * @tags Tag + * @name V3TagDetail + * @request GET:/api/v3/tag/{id} + * @secure + */ + v3TagDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/tag/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags TagDetails + * @name V3TagDetailList + * @request GET:/api/v3/tag/detail + * @secure + */ + v3TagDetailList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/tag/detail`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags TagDetails + * @name V3TagDetailDetail + * @request GET:/api/v3/tag/detail/{id} + * @secure + */ + v3TagDetailDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/tag/detail/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Task + * @name V3SystemTaskList + * @request GET:/api/v3/system/task + * @secure + */ + v3SystemTaskList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/task`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Task + * @name V3SystemTaskDetail + * @request GET:/api/v3/system/task/{id} + * @secure + */ + v3SystemTaskDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/system/task/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags UiConfig + * @name V3ConfigUiUpdate + * @request PUT:/api/v3/config/ui/{id} + * @secure + */ + v3ConfigUiUpdate: (id: string, data: UiConfigResource, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/ui/${id}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags UiConfig + * @name V3ConfigUiDetail + * @request GET:/api/v3/config/ui/{id} + * @secure + */ + v3ConfigUiDetail: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/ui/${id}`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags UiConfig + * @name V3ConfigUiList + * @request GET:/api/v3/config/ui + * @secure + */ + v3ConfigUiList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/config/ui`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags Update + * @name V3UpdateList + * @request GET:/api/v3/update + * @secure + */ + v3UpdateList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/update`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags UpdateLogFile + * @name V3LogFileUpdateList + * @request GET:/api/v3/log/file/update + * @secure + */ + v3LogFileUpdateList: (params: RequestParams = {}) => + this.request({ + path: `/api/v3/log/file/update`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags UpdateLogFile + * @name V3LogFileUpdateDetail + * @request GET:/api/v3/log/file/update/{filename} + * @secure + */ + v3LogFileUpdateDetail: (filename: string, params: RequestParams = {}) => + this.request({ + path: `/api/v3/log/file/update/${filename}`, + method: "GET", + secure: true, + ...params, + }), + }; + login = { + /** + * No description + * + * @tags Authentication + * @name LoginCreate + * @request POST:/login + * @secure + */ + loginCreate: ( + data: { + username?: string; + password?: string; + rememberMe?: string; + }, + query?: { + returnUrl?: string; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/login`, + method: "POST", + query: query, + body: data, + secure: true, + type: ContentType.FormData, + ...params, + }), + + /** + * No description + * + * @tags StaticResource + * @name LoginList + * @request GET:/login + * @secure + */ + loginList: (params: RequestParams = {}) => + this.request({ + path: `/login`, + method: "GET", + secure: true, + ...params, + }), + }; + logout = { + /** + * No description + * + * @tags Authentication + * @name LogoutList + * @request GET:/logout + * @secure + */ + logoutList: (params: RequestParams = {}) => + this.request({ + path: `/logout`, + method: "GET", + secure: true, + ...params, + }), + }; + feed = { + /** + * No description + * + * @tags CalendarFeed + * @name V3CalendarRadarrIcsList + * @request GET:/feed/v3/calendar/radarr.ics + * @secure + */ + v3CalendarRadarrIcsList: ( + query?: { + /** + * @format int32 + * @default 7 + */ + pastDays?: number; + /** + * @format int32 + * @default 28 + */ + futureDays?: number; + /** @default "" */ + tags?: string; + /** @default false */ + unmonitored?: boolean; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/feed/v3/calendar/radarr.ics`, + method: "GET", + query: query, + secure: true, + ...params, + }), + }; + ping = { + /** + * No description + * + * @tags Ping + * @name PingList + * @request GET:/ping + * @secure + */ + pingList: (params: RequestParams = {}) => + this.request({ + path: `/ping`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + }; + content = { + /** + * No description + * + * @tags StaticResource + * @name ContentDetail + * @request GET:/content/{path} + * @secure + */ + contentDetail: (path: string, params: RequestParams = {}) => + this.request({ + path: `/content/${path}`, + method: "GET", + secure: true, + ...params, + }), + }; + path = { + /** + * No description + * + * @tags StaticResource + * @name GetPath + * @request GET:/{path} + * @secure + */ + getPath: (path: string, params: RequestParams = {}) => + this.request({ + path: `/${path}`, + method: "GET", + secure: true, + ...params, + }), + }; +} diff --git a/src/__generated__/MySuperbApi.ts b/src/__generated__/generated-sonarr-api.ts similarity index 91% rename from src/__generated__/MySuperbApi.ts rename to src/__generated__/generated-sonarr-api.ts index 32d8e8b..4565df3 100644 --- a/src/__generated__/MySuperbApi.ts +++ b/src/__generated__/generated-sonarr-api.ts @@ -1763,10 +1763,12 @@ export interface Version { minorRevision?: number; } +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, HeadersDefaults, ResponseType } from "axios"; +import axios from "axios"; + export type QueryParamsType = Record; -export type ResponseFormat = keyof Omit; -export interface FullRequestParams extends Omit { +export interface FullRequestParams extends Omit { /** set parameter to `true` for call `securityWorker` for this request */ secure?: boolean; /** request path */ @@ -1776,37 +1778,19 @@ export interface FullRequestParams extends Omit { /** query params */ query?: QueryParamsType; /** format of response (i.e. response.json() -> format: "json") */ - format?: ResponseFormat; + format?: ResponseType; /** request body */ body?: unknown; - /** base url */ - baseUrl?: string; - /** request cancellation token */ - cancelToken?: CancelToken; } -export type RequestParams = Omit< - FullRequestParams, - "body" | "method" | "query" | "path" ->; - -export interface ApiConfig { - baseUrl?: string; - baseApiParams?: Omit; - securityWorker?: ( - securityData: SecurityDataType | null - ) => Promise | RequestParams | void; - customFetch?: typeof fetch; -} +export type RequestParams = Omit; -export interface HttpResponse - extends Response { - data: D; - error: E; +export interface ApiConfig extends Omit { + securityWorker?: (securityData: SecurityDataType | null) => Promise | AxiosRequestConfig | void; + secure?: boolean; + format?: ResponseType; } -type CancelToken = Symbol | string | number; - export enum ContentType { Json = "application/json", FormData = "multipart/form-data", @@ -1815,198 +1799,92 @@ export enum ContentType { } export class HttpClient { - public baseUrl: string = "{protocol}://{hostpath}"; + public instance: AxiosInstance; private securityData: SecurityDataType | null = null; private securityWorker?: ApiConfig["securityWorker"]; - private abortControllers = new Map(); - private customFetch = (...fetchParams: Parameters) => - fetch(...fetchParams); - - private baseApiParams: RequestParams = { - credentials: "same-origin", - headers: {}, - redirect: "follow", - referrerPolicy: "no-referrer", - }; - - constructor(apiConfig: ApiConfig = {}) { - Object.assign(this, apiConfig); + private secure?: boolean; + private format?: ResponseType; + + constructor({ securityWorker, secure, format, ...axiosConfig }: ApiConfig = {}) { + this.instance = axios.create({ ...axiosConfig, baseURL: axiosConfig.baseURL || "{protocol}://{hostpath}" }); + this.secure = secure; + this.format = format; + this.securityWorker = securityWorker; } public setSecurityData = (data: SecurityDataType | null) => { this.securityData = data; }; - protected encodeQueryParam(key: string, value: any) { - const encodedKey = encodeURIComponent(key); - return `${encodedKey}=${encodeURIComponent( - typeof value === "number" ? value : `${value}` - )}`; - } - - protected addQueryParam(query: QueryParamsType, key: string) { - return this.encodeQueryParam(key, query[key]); - } - - protected addArrayQueryParam(query: QueryParamsType, key: string) { - const value = query[key]; - return value.map((v: any) => this.encodeQueryParam(key, v)).join("&"); - } - - protected toQueryString(rawQuery?: QueryParamsType): string { - const query = rawQuery || {}; - const keys = Object.keys(query).filter( - (key) => "undefined" !== typeof query[key] - ); - return keys - .map((key) => - Array.isArray(query[key]) - ? this.addArrayQueryParam(query, key) - : this.addQueryParam(query, key) - ) - .join("&"); - } - - protected addQueryParams(rawQuery?: QueryParamsType): string { - const queryString = this.toQueryString(rawQuery); - return queryString ? `?${queryString}` : ""; - } - - private contentFormatters: Record any> = { - [ContentType.Json]: (input: any) => - input !== null && (typeof input === "object" || typeof input === "string") - ? JSON.stringify(input) - : input, - [ContentType.Text]: (input: any) => - input !== null && typeof input !== "string" - ? JSON.stringify(input) - : input, - [ContentType.FormData]: (input: any) => - Object.keys(input || {}).reduce((formData, key) => { - const property = input[key]; - formData.append( - key, - property instanceof Blob - ? property - : typeof property === "object" && property !== null - ? JSON.stringify(property) - : `${property}` - ); - return formData; - }, new FormData()), - [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), - }; + protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig { + const method = params1.method || (params2 && params2.method); - protected mergeRequestParams( - params1: RequestParams, - params2?: RequestParams - ): RequestParams { return { - ...this.baseApiParams, + ...this.instance.defaults, ...params1, ...(params2 || {}), headers: { - ...(this.baseApiParams.headers || {}), + ...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}), ...(params1.headers || {}), ...((params2 && params2.headers) || {}), }, }; } - protected createAbortSignal = ( - cancelToken: CancelToken - ): AbortSignal | undefined => { - if (this.abortControllers.has(cancelToken)) { - const abortController = this.abortControllers.get(cancelToken); - if (abortController) { - return abortController.signal; - } - return void 0; + protected stringifyFormItem(formItem: unknown) { + if (typeof formItem === "object" && formItem !== null) { + return JSON.stringify(formItem); + } else { + return `${formItem}`; } + } - const abortController = new AbortController(); - this.abortControllers.set(cancelToken, abortController); - return abortController.signal; - }; + protected createFormData(input: Record): FormData { + return Object.keys(input || {}).reduce((formData, key) => { + const property = input[key]; + const propertyContent: any[] = property instanceof Array ? property : [property]; - public abortRequest = (cancelToken: CancelToken) => { - const abortController = this.abortControllers.get(cancelToken); + for (const formItem of propertyContent) { + const isFileType = formItem instanceof Blob || formItem instanceof File; + formData.append(key, isFileType ? formItem : this.stringifyFormItem(formItem)); + } - if (abortController) { - abortController.abort(); - this.abortControllers.delete(cancelToken); - } - }; + return formData; + }, new FormData()); + } - public request = async ({ - body, + public request = async ({ secure, path, type, query, format, - baseUrl, - cancelToken, + body, ...params - }: FullRequestParams): Promise> => { + }: FullRequestParams): Promise> => { const secureParams = - ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) && - this.securityWorker && - (await this.securityWorker(this.securityData))) || - {}; + ((typeof secure === "boolean" ? secure : this.secure) && this.securityWorker && (await this.securityWorker(this.securityData))) || {}; const requestParams = this.mergeRequestParams(params, secureParams); - const queryString = query && this.toQueryString(query); - const payloadFormatter = this.contentFormatters[type || ContentType.Json]; - const responseFormat = format || requestParams.format; - - return this.customFetch( - `${baseUrl || this.baseUrl || ""}${path}${ - queryString ? `?${queryString}` : "" - }`, - { - ...requestParams, - headers: { - ...(requestParams.headers || {}), - ...(type && type !== ContentType.FormData - ? { "Content-Type": type } - : {}), - }, - signal: - (cancelToken - ? this.createAbortSignal(cancelToken) - : requestParams.signal) || null, - body: - typeof body === "undefined" || body === null - ? null - : payloadFormatter(body), - } - ).then(async (response) => { - const r = response as HttpResponse; - r.data = null as unknown as T; - r.error = null as unknown as E; - - const data = !responseFormat - ? r - : await response[responseFormat]() - .then((data) => { - if (r.ok) { - r.data = data; - } else { - r.error = data; - } - return r; - }) - .catch((e) => { - r.error = e; - return r; - }); - - if (cancelToken) { - this.abortControllers.delete(cancelToken); - } + const responseFormat = format || this.format || undefined; + + if (type === ContentType.FormData && body && body !== null && typeof body === "object") { + body = this.createFormData(body as Record); + } + + if (type === ContentType.Text && body && body !== null && typeof body !== "string") { + body = JSON.stringify(body); + } - if (!response.ok) throw data; - return data; + return this.instance.request({ + ...requestParams, + headers: { + ...(requestParams.headers || {}), + ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), + }, + params: query, + responseType: responseFormat, + data: body, + url: path, }); }; } @@ -2019,9 +1897,7 @@ export class HttpClient { * * Sonarr API docs - The v3 API docs apply to both v3 and v4 versions of Sonarr. Some functionality may only be available in v4 of the Sonarr application. */ -export class Api< - SecurityDataType extends unknown -> extends HttpClient { +export class Api extends HttpClient { /** * No description * @@ -2063,10 +1939,7 @@ export class Api< * @request POST:/api/v3/autotagging * @secure */ - v3AutotaggingCreate: ( - data: AutoTaggingResource, - params: RequestParams = {} - ) => + v3AutotaggingCreate: (data: AutoTaggingResource, params: RequestParams = {}) => this.request({ path: `/api/v3/autotagging`, method: "POST", @@ -2102,11 +1975,7 @@ export class Api< * @request PUT:/api/v3/autotagging/{id} * @secure */ - v3AutotaggingUpdate: ( - id: string, - data: AutoTaggingResource, - params: RequestParams = {} - ) => + v3AutotaggingUpdate: (id: string, data: AutoTaggingResource, params: RequestParams = {}) => this.request({ path: `/api/v3/autotagging/${id}`, method: "PUT", @@ -2254,7 +2123,7 @@ export class Api< sortKey?: string; sortDirection?: SortDirection; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/blocklist`, @@ -2289,10 +2158,7 @@ export class Api< * @request DELETE:/api/v3/blocklist/bulk * @secure */ - v3BlocklistBulkDelete: ( - data: BlocklistBulkResource, - params: RequestParams = {} - ) => + v3BlocklistBulkDelete: (data: BlocklistBulkResource, params: RequestParams = {}) => this.request({ path: `/api/v3/blocklist/bulk`, method: "DELETE", @@ -2327,7 +2193,7 @@ export class Api< /** @default "" */ tags?: string; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/calendar`, @@ -2449,10 +2315,7 @@ export class Api< * @request POST:/api/v3/customfilter * @secure */ - v3CustomfilterCreate: ( - data: CustomFilterResource, - params: RequestParams = {} - ) => + v3CustomfilterCreate: (data: CustomFilterResource, params: RequestParams = {}) => this.request({ path: `/api/v3/customfilter`, method: "POST", @@ -2471,11 +2334,7 @@ export class Api< * @request PUT:/api/v3/customfilter/{id} * @secure */ - v3CustomfilterUpdate: ( - id: string, - data: CustomFilterResource, - params: RequestParams = {} - ) => + v3CustomfilterUpdate: (id: string, data: CustomFilterResource, params: RequestParams = {}) => this.request({ path: `/api/v3/customfilter/${id}`, method: "PUT", @@ -2527,10 +2386,7 @@ export class Api< * @request POST:/api/v3/customformat * @secure */ - v3CustomformatCreate: ( - data: CustomFormatResource, - params: RequestParams = {} - ) => + v3CustomformatCreate: (data: CustomFormatResource, params: RequestParams = {}) => this.request({ path: `/api/v3/customformat`, method: "POST", @@ -2566,11 +2422,7 @@ export class Api< * @request PUT:/api/v3/customformat/{id} * @secure */ - v3CustomformatUpdate: ( - id: string, - data: CustomFormatResource, - params: RequestParams = {} - ) => + v3CustomformatUpdate: (id: string, data: CustomFormatResource, params: RequestParams = {}) => this.request({ path: `/api/v3/customformat/${id}`, method: "PUT", @@ -2661,7 +2513,7 @@ export class Api< /** @default true */ monitored?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/wanted/cutoff`, @@ -2697,10 +2549,7 @@ export class Api< * @request POST:/api/v3/delayprofile * @secure */ - v3DelayprofileCreate: ( - data: DelayProfileResource, - params: RequestParams = {} - ) => + v3DelayprofileCreate: (data: DelayProfileResource, params: RequestParams = {}) => this.request({ path: `/api/v3/delayprofile`, method: "POST", @@ -2752,11 +2601,7 @@ export class Api< * @request PUT:/api/v3/delayprofile/{id} * @secure */ - v3DelayprofileUpdate: ( - id: string, - data: DelayProfileResource, - params: RequestParams = {} - ) => + v3DelayprofileUpdate: (id: string, data: DelayProfileResource, params: RequestParams = {}) => this.request({ path: `/api/v3/delayprofile/${id}`, method: "PUT", @@ -2798,7 +2643,7 @@ export class Api< /** @format int32 */ after?: number; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/delayprofile/reorder/${id}`, @@ -2857,7 +2702,7 @@ export class Api< /** @default false */ forceSave?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/downloadclient`, @@ -2885,7 +2730,7 @@ export class Api< /** @default false */ forceSave?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/downloadclient/${id}`, @@ -2939,10 +2784,7 @@ export class Api< * @request PUT:/api/v3/downloadclient/bulk * @secure */ - v3DownloadclientBulkUpdate: ( - data: DownloadClientBulkResource, - params: RequestParams = {} - ) => + v3DownloadclientBulkUpdate: (data: DownloadClientBulkResource, params: RequestParams = {}) => this.request({ path: `/api/v3/downloadclient/bulk`, method: "PUT", @@ -2961,10 +2803,7 @@ export class Api< * @request DELETE:/api/v3/downloadclient/bulk * @secure */ - v3DownloadclientBulkDelete: ( - data: DownloadClientBulkResource, - params: RequestParams = {} - ) => + v3DownloadclientBulkDelete: (data: DownloadClientBulkResource, params: RequestParams = {}) => this.request({ path: `/api/v3/downloadclient/bulk`, method: "DELETE", @@ -2999,10 +2838,7 @@ export class Api< * @request POST:/api/v3/downloadclient/test * @secure */ - v3DownloadclientTestCreate: ( - data: DownloadClientResource, - params: RequestParams = {} - ) => + v3DownloadclientTestCreate: (data: DownloadClientResource, params: RequestParams = {}) => this.request({ path: `/api/v3/downloadclient/test`, method: "POST", @@ -3036,11 +2872,7 @@ export class Api< * @request POST:/api/v3/downloadclient/action/{name} * @secure */ - v3DownloadclientActionCreate: ( - name: string, - data: DownloadClientResource, - params: RequestParams = {} - ) => + v3DownloadclientActionCreate: (name: string, data: DownloadClientResource, params: RequestParams = {}) => this.request({ path: `/api/v3/downloadclient/action/${name}`, method: "POST", @@ -3075,11 +2907,7 @@ export class Api< * @request PUT:/api/v3/config/downloadclient/{id} * @secure */ - v3ConfigDownloadclientUpdate: ( - id: string, - data: DownloadClientConfigResource, - params: RequestParams = {} - ) => + v3ConfigDownloadclientUpdate: (id: string, data: DownloadClientConfigResource, params: RequestParams = {}) => this.request({ path: `/api/v3/config/downloadclient/${id}`, method: "PUT", @@ -3127,7 +2955,7 @@ export class Api< /** @default false */ includeImages?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/episode`, @@ -3146,11 +2974,7 @@ export class Api< * @request PUT:/api/v3/episode/{id} * @secure */ - v3EpisodeUpdate: ( - id: number, - data: EpisodeResource, - params: RequestParams = {} - ) => + v3EpisodeUpdate: (id: number, data: EpisodeResource, params: RequestParams = {}) => this.request({ path: `/api/v3/episode/${id}`, method: "PUT", @@ -3192,7 +3016,7 @@ export class Api< /** @default false */ includeImages?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/episode/monitor`, @@ -3218,7 +3042,7 @@ export class Api< seriesId?: number; episodeFileIds?: number[]; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/episodefile`, @@ -3237,11 +3061,7 @@ export class Api< * @request PUT:/api/v3/episodefile/{id} * @secure */ - v3EpisodefileUpdate: ( - id: string, - data: EpisodeFileResource, - params: RequestParams = {} - ) => + v3EpisodefileUpdate: (id: string, data: EpisodeFileResource, params: RequestParams = {}) => this.request({ path: `/api/v3/episodefile/${id}`, method: "PUT", @@ -3293,10 +3113,7 @@ export class Api< * @request PUT:/api/v3/episodefile/editor * @secure */ - v3EpisodefileEditorUpdate: ( - data: EpisodeFileListResource, - params: RequestParams = {} - ) => + v3EpisodefileEditorUpdate: (data: EpisodeFileListResource, params: RequestParams = {}) => this.request({ path: `/api/v3/episodefile/editor`, method: "PUT", @@ -3314,10 +3131,7 @@ export class Api< * @request DELETE:/api/v3/episodefile/bulk * @secure */ - v3EpisodefileBulkDelete: ( - data: EpisodeFileListResource, - params: RequestParams = {} - ) => + v3EpisodefileBulkDelete: (data: EpisodeFileListResource, params: RequestParams = {}) => this.request({ path: `/api/v3/episodefile/bulk`, method: "DELETE", @@ -3335,10 +3149,7 @@ export class Api< * @request PUT:/api/v3/episodefile/bulk * @secure */ - v3EpisodefileBulkUpdate: ( - data: EpisodeFileResource[], - params: RequestParams = {} - ) => + v3EpisodefileBulkUpdate: (data: EpisodeFileResource[], params: RequestParams = {}) => this.request({ path: `/api/v3/episodefile/bulk`, method: "PUT", @@ -3364,7 +3175,7 @@ export class Api< /** @default false */ allowFoldersWithoutTrailingSlashes?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/filesystem`, @@ -3386,7 +3197,7 @@ export class Api< query?: { path?: string; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/filesystem/type`, @@ -3408,7 +3219,7 @@ export class Api< query?: { path?: string; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/filesystem/mediafiles`, @@ -3467,7 +3278,7 @@ export class Api< languages?: number[]; quality?: number[]; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/history`, @@ -3496,7 +3307,7 @@ export class Api< /** @default false */ includeEpisode?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/history/since`, @@ -3527,7 +3338,7 @@ export class Api< /** @default false */ includeEpisode?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/history/series`, @@ -3579,11 +3390,7 @@ export class Api< * @request PUT:/api/v3/config/host/{id} * @secure */ - v3ConfigHostUpdate: ( - id: string, - data: HostConfigResource, - params: RequestParams = {} - ) => + v3ConfigHostUpdate: (id: string, data: HostConfigResource, params: RequestParams = {}) => this.request({ path: `/api/v3/config/host/${id}`, method: "PUT", @@ -3642,7 +3449,7 @@ export class Api< /** @default false */ forceSave?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/importlist`, @@ -3670,7 +3477,7 @@ export class Api< /** @default false */ forceSave?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/importlist/${id}`, @@ -3724,10 +3531,7 @@ export class Api< * @request PUT:/api/v3/importlist/bulk * @secure */ - v3ImportlistBulkUpdate: ( - data: ImportListBulkResource, - params: RequestParams = {} - ) => + v3ImportlistBulkUpdate: (data: ImportListBulkResource, params: RequestParams = {}) => this.request({ path: `/api/v3/importlist/bulk`, method: "PUT", @@ -3746,10 +3550,7 @@ export class Api< * @request DELETE:/api/v3/importlist/bulk * @secure */ - v3ImportlistBulkDelete: ( - data: ImportListBulkResource, - params: RequestParams = {} - ) => + v3ImportlistBulkDelete: (data: ImportListBulkResource, params: RequestParams = {}) => this.request({ path: `/api/v3/importlist/bulk`, method: "DELETE", @@ -3784,10 +3585,7 @@ export class Api< * @request POST:/api/v3/importlist/test * @secure */ - v3ImportlistTestCreate: ( - data: ImportListResource, - params: RequestParams = {} - ) => + v3ImportlistTestCreate: (data: ImportListResource, params: RequestParams = {}) => this.request({ path: `/api/v3/importlist/test`, method: "POST", @@ -3821,11 +3619,7 @@ export class Api< * @request POST:/api/v3/importlist/action/{name} * @secure */ - v3ImportlistActionCreate: ( - name: string, - data: ImportListResource, - params: RequestParams = {} - ) => + v3ImportlistActionCreate: (name: string, data: ImportListResource, params: RequestParams = {}) => this.request({ path: `/api/v3/importlist/action/${name}`, method: "POST", @@ -3860,11 +3654,7 @@ export class Api< * @request PUT:/api/v3/config/importlist/{id} * @secure */ - v3ConfigImportlistUpdate: ( - id: string, - data: ImportListConfigResource, - params: RequestParams = {} - ) => + v3ConfigImportlistUpdate: (id: string, data: ImportListConfigResource, params: RequestParams = {}) => this.request({ path: `/api/v3/config/importlist/${id}`, method: "PUT", @@ -3918,10 +3708,7 @@ export class Api< * @request POST:/api/v3/importlistexclusion * @secure */ - v3ImportlistexclusionCreate: ( - data: ImportListExclusionResource, - params: RequestParams = {} - ) => + v3ImportlistexclusionCreate: (data: ImportListExclusionResource, params: RequestParams = {}) => this.request({ path: `/api/v3/importlistexclusion`, method: "POST", @@ -3955,7 +3742,7 @@ export class Api< sortKey?: string; sortDirection?: SortDirection; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/importlistexclusion/paged`, @@ -3974,11 +3761,7 @@ export class Api< * @request PUT:/api/v3/importlistexclusion/{id} * @secure */ - v3ImportlistexclusionUpdate: ( - id: string, - data: ImportListExclusionResource, - params: RequestParams = {} - ) => + v3ImportlistexclusionUpdate: (id: string, data: ImportListExclusionResource, params: RequestParams = {}) => this.request({ path: `/api/v3/importlistexclusion/${id}`, method: "PUT", @@ -4053,7 +3836,7 @@ export class Api< /** @default false */ forceSave?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/indexer`, @@ -4081,7 +3864,7 @@ export class Api< /** @default false */ forceSave?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/indexer/${id}`, @@ -4135,10 +3918,7 @@ export class Api< * @request PUT:/api/v3/indexer/bulk * @secure */ - v3IndexerBulkUpdate: ( - data: IndexerBulkResource, - params: RequestParams = {} - ) => + v3IndexerBulkUpdate: (data: IndexerBulkResource, params: RequestParams = {}) => this.request({ path: `/api/v3/indexer/bulk`, method: "PUT", @@ -4157,10 +3937,7 @@ export class Api< * @request DELETE:/api/v3/indexer/bulk * @secure */ - v3IndexerBulkDelete: ( - data: IndexerBulkResource, - params: RequestParams = {} - ) => + v3IndexerBulkDelete: (data: IndexerBulkResource, params: RequestParams = {}) => this.request({ path: `/api/v3/indexer/bulk`, method: "DELETE", @@ -4229,11 +4006,7 @@ export class Api< * @request POST:/api/v3/indexer/action/{name} * @secure */ - v3IndexerActionCreate: ( - name: string, - data: IndexerResource, - params: RequestParams = {} - ) => + v3IndexerActionCreate: (name: string, data: IndexerResource, params: RequestParams = {}) => this.request({ path: `/api/v3/indexer/action/${name}`, method: "POST", @@ -4268,11 +4041,7 @@ export class Api< * @request PUT:/api/v3/config/indexer/{id} * @secure */ - v3ConfigIndexerUpdate: ( - id: string, - data: IndexerConfigResource, - params: RequestParams = {} - ) => + v3ConfigIndexerUpdate: (id: string, data: IndexerConfigResource, params: RequestParams = {}) => this.request({ path: `/api/v3/config/indexer/${id}`, method: "PUT", @@ -4360,10 +4129,7 @@ export class Api< * @deprecated * @secure */ - v3LanguageprofileCreate: ( - data: LanguageProfileResource, - params: RequestParams = {} - ) => + v3LanguageprofileCreate: (data: LanguageProfileResource, params: RequestParams = {}) => this.request({ path: `/api/v3/languageprofile`, method: "POST", @@ -4418,11 +4184,7 @@ export class Api< * @deprecated * @secure */ - v3LanguageprofileUpdate: ( - id: string, - data: LanguageProfileResource, - params: RequestParams = {} - ) => + v3LanguageprofileUpdate: (id: string, data: LanguageProfileResource, params: RequestParams = {}) => this.request({ path: `/api/v3/languageprofile/${id}`, method: "PUT", @@ -4543,7 +4305,7 @@ export class Api< sortDirection?: SortDirection; level?: string; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/log`, @@ -4606,7 +4368,7 @@ export class Api< /** @default true */ filterExistingFiles?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/manualimport`, @@ -4625,10 +4387,7 @@ export class Api< * @request POST:/api/v3/manualimport * @secure */ - v3ManualimportCreate: ( - data: ManualImportReprocessResource[], - params: RequestParams = {} - ) => + v3ManualimportCreate: (data: ManualImportReprocessResource[], params: RequestParams = {}) => this.request({ path: `/api/v3/manualimport`, method: "POST", @@ -4646,11 +4405,7 @@ export class Api< * @request GET:/api/v3/mediacover/{seriesId}/{filename} * @secure */ - v3MediacoverDetail: ( - seriesId: number, - filename: string, - params: RequestParams = {} - ) => + v3MediacoverDetail: (seriesId: number, filename: string, params: RequestParams = {}) => this.request({ path: `/api/v3/mediacover/${seriesId}/${filename}`, method: "GET", @@ -4683,11 +4438,7 @@ export class Api< * @request PUT:/api/v3/config/mediamanagement/{id} * @secure */ - v3ConfigMediamanagementUpdate: ( - id: string, - data: MediaManagementConfigResource, - params: RequestParams = {} - ) => + v3ConfigMediamanagementUpdate: (id: string, data: MediaManagementConfigResource, params: RequestParams = {}) => this.request({ path: `/api/v3/config/mediamanagement/${id}`, method: "PUT", @@ -4746,7 +4497,7 @@ export class Api< /** @default false */ forceSave?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/metadata`, @@ -4774,7 +4525,7 @@ export class Api< /** @default false */ forceSave?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/metadata/${id}`, @@ -4845,10 +4596,7 @@ export class Api< * @request POST:/api/v3/metadata/test * @secure */ - v3MetadataTestCreate: ( - data: MetadataResource, - params: RequestParams = {} - ) => + v3MetadataTestCreate: (data: MetadataResource, params: RequestParams = {}) => this.request({ path: `/api/v3/metadata/test`, method: "POST", @@ -4882,11 +4630,7 @@ export class Api< * @request POST:/api/v3/metadata/action/{name} * @secure */ - v3MetadataActionCreate: ( - name: string, - data: MetadataResource, - params: RequestParams = {} - ) => + v3MetadataActionCreate: (name: string, data: MetadataResource, params: RequestParams = {}) => this.request({ path: `/api/v3/metadata/action/${name}`, method: "POST", @@ -4925,7 +4669,7 @@ export class Api< /** @default true */ monitored?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/wanted/missing`, @@ -4978,11 +4722,7 @@ export class Api< * @request PUT:/api/v3/config/naming/{id} * @secure */ - v3ConfigNamingUpdate: ( - id: string, - data: NamingConfigResource, - params: RequestParams = {} - ) => + v3ConfigNamingUpdate: (id: string, data: NamingConfigResource, params: RequestParams = {}) => this.request({ path: `/api/v3/config/naming/${id}`, method: "PUT", @@ -5036,7 +4776,7 @@ export class Api< id?: number; resourceName?: string; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/config/naming/examples`, @@ -5077,7 +4817,7 @@ export class Api< /** @default false */ forceSave?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/notification`, @@ -5105,7 +4845,7 @@ export class Api< /** @default false */ forceSave?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/notification/${id}`, @@ -5176,10 +4916,7 @@ export class Api< * @request POST:/api/v3/notification/test * @secure */ - v3NotificationTestCreate: ( - data: NotificationResource, - params: RequestParams = {} - ) => + v3NotificationTestCreate: (data: NotificationResource, params: RequestParams = {}) => this.request({ path: `/api/v3/notification/test`, method: "POST", @@ -5213,11 +4950,7 @@ export class Api< * @request POST:/api/v3/notification/action/{name} * @secure */ - v3NotificationActionCreate: ( - name: string, - data: NotificationResource, - params: RequestParams = {} - ) => + v3NotificationActionCreate: (name: string, data: NotificationResource, params: RequestParams = {}) => this.request({ path: `/api/v3/notification/action/${name}`, method: "POST", @@ -5240,7 +4973,7 @@ export class Api< title?: string; path?: string; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/parse`, @@ -5259,11 +4992,7 @@ export class Api< * @request PUT:/api/v3/qualitydefinition/{id} * @secure */ - v3QualitydefinitionUpdate: ( - id: string, - data: QualityDefinitionResource, - params: RequestParams = {} - ) => + v3QualitydefinitionUpdate: (id: string, data: QualityDefinitionResource, params: RequestParams = {}) => this.request({ path: `/api/v3/qualitydefinition/${id}`, method: "PUT", @@ -5316,10 +5045,7 @@ export class Api< * @request PUT:/api/v3/qualitydefinition/update * @secure */ - v3QualitydefinitionUpdateUpdate: ( - data: QualityDefinitionResource[], - params: RequestParams = {} - ) => + v3QualitydefinitionUpdateUpdate: (data: QualityDefinitionResource[], params: RequestParams = {}) => this.request({ path: `/api/v3/qualitydefinition/update`, method: "PUT", @@ -5337,10 +5063,7 @@ export class Api< * @request POST:/api/v3/qualityprofile * @secure */ - v3QualityprofileCreate: ( - data: QualityProfileResource, - params: RequestParams = {} - ) => + v3QualityprofileCreate: (data: QualityProfileResource, params: RequestParams = {}) => this.request({ path: `/api/v3/qualityprofile`, method: "POST", @@ -5392,11 +5115,7 @@ export class Api< * @request PUT:/api/v3/qualityprofile/{id} * @secure */ - v3QualityprofileUpdate: ( - id: string, - data: QualityProfileResource, - params: RequestParams = {} - ) => + v3QualityprofileUpdate: (id: string, data: QualityProfileResource, params: RequestParams = {}) => this.request({ path: `/api/v3/qualityprofile/${id}`, method: "PUT", @@ -5461,7 +5180,7 @@ export class Api< /** @default false */ changeCategory?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/queue/${id}`, @@ -5491,7 +5210,7 @@ export class Api< /** @default false */ changeCategory?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/queue/bulk`, @@ -5537,7 +5256,7 @@ export class Api< /** @format int32 */ quality?: number; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/queue`, @@ -5572,10 +5291,7 @@ export class Api< * @request POST:/api/v3/queue/grab/bulk * @secure */ - v3QueueGrabBulkCreate: ( - data: QueueBulkResource, - params: RequestParams = {} - ) => + v3QueueGrabBulkCreate: (data: QueueBulkResource, params: RequestParams = {}) => this.request({ path: `/api/v3/queue/grab/bulk`, method: "POST", @@ -5603,7 +5319,7 @@ export class Api< /** @default false */ includeEpisode?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/queue/details`, @@ -5666,7 +5382,7 @@ export class Api< /** @format int32 */ seasonNumber?: number; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/release`, @@ -5685,10 +5401,7 @@ export class Api< * @request POST:/api/v3/releaseprofile * @secure */ - v3ReleaseprofileCreate: ( - data: ReleaseProfileResource, - params: RequestParams = {} - ) => + v3ReleaseprofileCreate: (data: ReleaseProfileResource, params: RequestParams = {}) => this.request({ path: `/api/v3/releaseprofile`, method: "POST", @@ -5740,11 +5453,7 @@ export class Api< * @request PUT:/api/v3/releaseprofile/{id} * @secure */ - v3ReleaseprofileUpdate: ( - id: string, - data: ReleaseProfileResource, - params: RequestParams = {} - ) => + v3ReleaseprofileUpdate: (id: string, data: ReleaseProfileResource, params: RequestParams = {}) => this.request({ path: `/api/v3/releaseprofile/${id}`, method: "PUT", @@ -5799,10 +5508,7 @@ export class Api< * @request POST:/api/v3/remotepathmapping * @secure */ - v3RemotepathmappingCreate: ( - data: RemotePathMappingResource, - params: RequestParams = {} - ) => + v3RemotepathmappingCreate: (data: RemotePathMappingResource, params: RequestParams = {}) => this.request({ path: `/api/v3/remotepathmapping`, method: "POST", @@ -5854,11 +5560,7 @@ export class Api< * @request PUT:/api/v3/remotepathmapping/{id} * @secure */ - v3RemotepathmappingUpdate: ( - id: string, - data: RemotePathMappingResource, - params: RequestParams = {} - ) => + v3RemotepathmappingUpdate: (id: string, data: RemotePathMappingResource, params: RequestParams = {}) => this.request({ path: `/api/v3/remotepathmapping/${id}`, method: "PUT", @@ -5901,7 +5603,7 @@ export class Api< /** @format int32 */ seasonNumber?: number; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/rename`, @@ -5920,10 +5622,7 @@ export class Api< * @request POST:/api/v3/rootfolder * @secure */ - v3RootfolderCreate: ( - data: RootFolderResource, - params: RequestParams = {} - ) => + v3RootfolderCreate: (data: RootFolderResource, params: RequestParams = {}) => this.request({ path: `/api/v3/rootfolder`, method: "POST", @@ -5992,10 +5691,7 @@ export class Api< * @request POST:/api/v3/seasonpass * @secure */ - v3SeasonpassCreate: ( - data: SeasonPassResource, - params: RequestParams = {} - ) => + v3SeasonpassCreate: (data: SeasonPassResource, params: RequestParams = {}) => this.request({ path: `/api/v3/seasonpass`, method: "POST", @@ -6020,7 +5716,7 @@ export class Api< /** @default false */ includeSeasonImages?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/series`, @@ -6064,7 +5760,7 @@ export class Api< /** @default false */ includeSeasonImages?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/series/${id}`, @@ -6090,7 +5786,7 @@ export class Api< /** @default false */ moveFiles?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/series/${id}`, @@ -6119,7 +5815,7 @@ export class Api< /** @default false */ addImportListExclusion?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/series/${id}`, @@ -6137,10 +5833,7 @@ export class Api< * @request PUT:/api/v3/series/editor * @secure */ - v3SeriesEditorUpdate: ( - data: SeriesEditorResource, - params: RequestParams = {} - ) => + v3SeriesEditorUpdate: (data: SeriesEditorResource, params: RequestParams = {}) => this.request({ path: `/api/v3/series/editor`, method: "PUT", @@ -6158,10 +5851,7 @@ export class Api< * @request DELETE:/api/v3/series/editor * @secure */ - v3SeriesEditorDelete: ( - data: SeriesEditorResource, - params: RequestParams = {} - ) => + v3SeriesEditorDelete: (data: SeriesEditorResource, params: RequestParams = {}) => this.request({ path: `/api/v3/series/editor`, method: "DELETE", @@ -6179,10 +5869,7 @@ export class Api< * @request POST:/api/v3/series/import * @secure */ - v3SeriesImportCreate: ( - data: SeriesResource[], - params: RequestParams = {} - ) => + v3SeriesImportCreate: (data: SeriesResource[], params: RequestParams = {}) => this.request({ path: `/api/v3/series/import`, method: "POST", @@ -6204,7 +5891,7 @@ export class Api< query?: { term?: string; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/api/v3/series/lookup`, @@ -6459,11 +6146,7 @@ export class Api< * @request PUT:/api/v3/config/ui/{id} * @secure */ - v3ConfigUiUpdate: ( - id: string, - data: UiConfigResource, - params: RequestParams = {} - ) => + v3ConfigUiUpdate: (id: string, data: UiConfigResource, params: RequestParams = {}) => this.request({ path: `/api/v3/config/ui/${id}`, method: "PUT", @@ -6576,7 +6259,7 @@ export class Api< query?: { returnUrl?: string; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/login`, @@ -6651,7 +6334,7 @@ export class Api< /** @default false */ asAllDay?: boolean; }, - params: RequestParams = {} + params: RequestParams = {}, ) => this.request({ path: `/feed/v3/calendar/sonarr.ics`, diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..80e8249 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,126 @@ +import { Api as RadarrApi } from "./__generated__/generated-radarr-api"; +import { Api as SonarrApi } from "./__generated__/generated-sonarr-api"; + +let sonarrClient: SonarrApi["api"] | undefined; +let radarrClient: RadarrApi["api"] | undefined; + +export const unsetApi = () => { + sonarrClient = undefined; + radarrClient = undefined; +}; + +export const getArrApi = () => { + const client = sonarrClient || radarrClient; + + if (client) { + return client; + } + + throw new Error("Please configure API first."); +}; + +export const getSonarrApi = () => { + if (sonarrClient) { + return sonarrClient; + } + + throw new Error("Please configure API first."); +}; + +export const configureSonarrApi = async (url: string, apiKey: string) => { + sonarrClient = undefined; + radarrClient = undefined; + + const api = new SonarrApi({ + headers: { + "X-Api-Key": apiKey, + }, + url: url, + baseURL: url, + // baseUrl: url, + // baseApiParams: { + // headers: { + // "X-Api-Key": apiKey, + // }, + // }, + }); + + sonarrClient = api.api; + + try { + await sonarrClient.v3MetadataList(); + } catch (error: any) { + let message; + + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + message = `Could not load from Sonarr API: Status ${error.response.status} - ${error.response.statusText}`; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + console.log(error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.log("Error", error.message); + } + + throw new Error(message); + } + + return sonarrClient; +}; + +export const getRadarrpi = () => { + if (radarrClient) { + return radarrClient; + } + + throw new Error("Please configure API first."); +}; + +export const configureRadarrApi = async (url: string, apiKey: string) => { + sonarrClient = undefined; + radarrClient = undefined; + + const api = new RadarrApi({ + headers: { + "X-Api-Key": apiKey, + }, + url: url, + baseURL: url, + // baseUrl: url, + // baseApiParams: { + // headers: { + // "X-Api-Key": apiKey, + // }, + // }, + }); + + radarrClient = api.api; + + try { + await radarrClient.v3MetadataList(); + } catch (error: any) { + let message; + + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + message = `Could not load from Radarr API: Status ${error.response.status} - ${error.response.statusText}`; + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + console.log(error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.log("Error", error.message); + } + + throw new Error(message); + } + + return radarrClient; +}; diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..b11cf3f --- /dev/null +++ b/src/config.ts @@ -0,0 +1,39 @@ +import { readFileSync } from "fs"; +import yaml from "yaml"; +import { YamlConfig } from "./types"; +import { ROOT_PATH } from "./util"; + +const CONFIG_LOCATION = process.env.CONFIG_LOCATION ?? `${ROOT_PATH}/config.yml`; +const SECRETS_LOCATION = process.env.SECRETS_LOCATION ?? `${ROOT_PATH}/secrets.yml`; + +let config: YamlConfig; +let secrets: any; + +const secretsTag = { + identify: (value: any) => value instanceof String, + tag: "!secret", + resolve(str: string) { + return getSecrets()[str]; + }, +}; + +// TODO some schema validation. For now only check if something can be imported +export const getConfig = (): YamlConfig => { + if (config) { + return config; + } + + const file = readFileSync(CONFIG_LOCATION, "utf8"); + config = yaml.parse(file, { customTags: [secretsTag] }) as YamlConfig; + return config; +}; + +export const getSecrets = () => { + if (secrets) { + return secrets; + } + + const file = readFileSync(SECRETS_LOCATION, "utf8"); + config = yaml.parse(file); + return config; +}; diff --git a/src/custom-formats.ts b/src/custom-formats.ts new file mode 100644 index 0000000..4cd26f2 --- /dev/null +++ b/src/custom-formats.ts @@ -0,0 +1,164 @@ +import fs, { readdirSync } from "fs"; +import path from "path"; +import { CustomFormatResource } from "./__generated__/generated-sonarr-api"; +import { getArrApi } from "./api"; +import { getConfig } from "./config"; +import { CFProcessing, ConfigarrCF, DynamicImportType, TrashCF, YamlInput } from "./types"; +import { IS_DRY_RUN, IS_LOCAL_SAMPLE_MODE, compareObjectsCarr, mapImportCfToRequestCf, toCarrCF } from "./util"; + +export const deleteAllCustomFormats = async () => { + const api = getArrApi(); + const cfOnServer = await api.v3CustomformatList(); + + for (const cf of cfOnServer.data) { + await api.v3CustomformatDelete(cf.id!); + console.log(`Deleted CF: '${cf.name}'`); + } +}; + +export const loadServerCustomFormats = async (): Promise => { + if (IS_LOCAL_SAMPLE_MODE) { + return (await import(path.resolve("./tests/samples/cfs.json"))).default as unknown as Promise; + } + const api = getArrApi(); + const cfOnServer = await api.v3CustomformatList(); + return cfOnServer.data; +}; + +export const manageCf = async (cfProcessing: CFProcessing, serverCfs: Map, cfsToManage: Set) => { + const { carrIdMapping: trashIdToObject } = cfProcessing; + + const api = getArrApi(); + + const manageSingle = async (carrId: string) => { + const tr = trashIdToObject.get(carrId); + + if (!tr) { + console.log(`TrashID to manage ${carrId} does not exists`); + return; + } + + const existingCf = serverCfs.get(tr.carrConfig.name!); + + if (existingCf) { + // Update if necessary + const comparison = compareObjectsCarr(existingCf, tr.requestConfig); + + if (!comparison.equal) { + console.log(`Found mismatch for ${tr.requestConfig.name}.`, comparison.changes); + + try { + if (IS_DRY_RUN) { + console.log(`DryRun: Would update CF: ${existingCf.id} - ${existingCf.name}`); + } else { + const updateResult = await api.v3CustomformatUpdate(existingCf.id + "", { + id: existingCf.id, + ...tr.requestConfig, + }); + console.log(`Updated CF ${tr.requestConfig.name}`); + } + } catch (err: any) { + console.log(`Failed updating CF ${tr.requestConfig.name}`, err.response.data); + } + } else { + console.log(`CF ${tr.requestConfig.name} does not need update.`); + } + } else { + // Create + try { + if (IS_DRY_RUN) { + console.log(`Would create CF: ${tr.requestConfig.name}`); + } else { + const createResult = await api.v3CustomformatCreate(tr.requestConfig); + console.log(`Created CF ${tr.requestConfig.name}`); + } + } catch (err: any) { + throw new Error(`Failed creating CF '${tr.requestConfig.name}'. Message: ${err.response.data?.message}`); + } + } + }; + + for (const cf of cfsToManage) { + await manageSingle(cf); + } +}; +export const loadLocalCfs = async (): Promise => { + const config = getConfig(); + + if (config.localCustomFormatsPath == null) { + console.log(`No local custom formats specified. Skipping.`); + return null; + } + + const cfPath = path.resolve(config.localCustomFormatsPath); + + if (!fs.existsSync(cfPath)) { + console.log(`Provided local custom formats path '${config.localCustomFormatsPath}' does not exist.`); + return null; + } + + const files = readdirSync(`${cfPath}`).filter((fn) => fn.endsWith("json")); + const carrIdToObject = new Map(); + const cfNameToCarrObject = new Map(); + + for (const file of files) { + const name = `${cfPath}/${file}`; + const cf: DynamicImportType = await import(`${name}`); + + const cfD = toCarrCF(cf.default); + + carrIdToObject.set(cfD.configarr_id, { + carrConfig: cfD, + requestConfig: mapImportCfToRequestCf(cfD), + }); + + if (cfD.name) { + cfNameToCarrObject.set(cfD.name, cfD); + } + } + + return { + carrIdMapping: carrIdToObject, + cfNameToCarrConfig: cfNameToCarrObject, + }; +}; +export const calculateCFsToManage = (yaml: YamlInput) => { + const cfTrashToManage: Set = new Set(); + + yaml.custom_formats.map((cf) => { + if (cf.trash_ids) { + cf.trash_ids.forEach((tid) => cfTrashToManage.add(tid)); + } + }); + + return cfTrashToManage; +}; +export const mergeCfSources = (listOfCfs: (CFProcessing | null)[]): CFProcessing => { + return listOfCfs.reduce( + (p, c) => { + if (!c) { + return p; + } + + for (const [key, value] of c.carrIdMapping.entries()) { + if (p.carrIdMapping.has(key)) { + console.log(`Overwriting ${key} during CF merge`); + } + p.carrIdMapping.set(key, value); + } + + for (const [key, value] of c.cfNameToCarrConfig.entries()) { + if (p.cfNameToCarrConfig.has(key)) { + console.log(`Overwriting ${key} during CF merge`); + } + p.cfNameToCarrConfig.set(key, value); + } + + return p; + }, + { + carrIdMapping: new Map(), + cfNameToCarrConfig: new Map(), + }, + ); +}; diff --git a/src/example.test.ts b/src/example.test.ts new file mode 100644 index 0000000..4cba084 --- /dev/null +++ b/src/example.test.ts @@ -0,0 +1,270 @@ +import { describe, expect, test } from "vitest"; +import { CustomFormatResource, PrivacyLevel, QualityDefinitionResource, QualitySource } from "./__generated__/generated-sonarr-api"; +import { calculateQualityDefinitionDiff, loadQualityDefinitionFromServer } from "./quality-definitions"; +import { TrashCF, TrashCFSpF, TrashQualityDefintion } from "./types"; +import { compareObjectsCarr, mapImportCfToRequestCf, toCarrCF } from "./util"; + +const exampleCFImplementations = { + name: "TestSpec", + includeCustomFormatWhenRenaming: false, + specifications: [ + { + name: "ReleaseTitleSpec", + implementation: "ReleaseTitleSpecification", + negate: false, + required: false, + fields: { + value: "expres", + }, + }, + { + name: "LanguageUnknown", + implementation: "LanguageSpecification", + negate: false, + required: false, + fields: { + value: 0, + }, + }, + { + name: "LanguageOrgi", + implementation: "LanguageSpecification", + negate: false, + required: false, + fields: { + value: -2, + }, + }, + { + name: "IndexerFlag", + implementation: "IndexerFlagSpecification", + negate: false, + required: false, + fields: { + value: 1, + }, + }, + { + name: "SourceSpec", + implementation: "SourceSpecification", + negate: false, + required: false, + fields: { + value: 6, + }, + }, + { + name: "Resolution", + implementation: "ResolutionSpecification", + negate: false, + required: false, + fields: { + value: 540, + }, + }, + { + name: "ReleaseGroup", + implementation: "ReleaseGroupSpecification", + negate: false, + required: false, + fields: { + value: "regex", + }, + }, + ], +}; + +describe("SizeSpecification", async () => { + const serverResponse: CustomFormatResource = { + id: 103, + name: "Size: Block More 40GB", + includeCustomFormatWhenRenaming: false, + specifications: [ + { + name: "Size", + implementation: "SizeSpecification", + implementationName: "Size", + infoLink: "https://wiki.servarr.com/sonarr/settings#custom-formats-2", + negate: false, + required: false, + fields: [ + { + order: 0, + name: "min", + label: "Minimum Size", + unit: "GB", + helpText: "Release must be greater than this size", + value: 1, + type: "number", + advanced: false, + privacy: PrivacyLevel.Normal, + isFloat: true, + }, + { + order: 1, + name: "max", + label: "Maximum Size", + unit: "GB", + helpText: "Release must be less than or equal to this size", + value: 9, + type: "number", + advanced: false, + privacy: PrivacyLevel.Normal, + isFloat: true, + }, + ], + }, + ], + }; + + const custom: TrashCF = { + trash_id: "custom-size-more-40gb", + trash_scores: { + default: -10000, + }, + trash_description: "Size: Block sizes over 40GB", + name: "Size: Block More 40GB", + includeCustomFormatWhenRenaming: false, + specifications: [ + { + name: "Size", + implementation: "SizeSpecification", + negate: false, + required: false, + fields: { + min: 1, + max: 9, + }, + }, + ], + }; + + test("equal", async () => { + const copied: typeof custom = JSON.parse(JSON.stringify(custom)); + + const result = compareObjectsCarr(serverResponse, mapImportCfToRequestCf(toCarrCF(copied))); + expect(result.equal).toBe(true); + }); + + test("mismatch negate", async () => { + const copied = JSON.parse(JSON.stringify(custom)); + copied.specifications![0].negate = true; + + const result = compareObjectsCarr(serverResponse, mapImportCfToRequestCf(toCarrCF(copied))); + expect(result.equal).toBe(false); + }); + + test("mismatch required", async () => { + const copied: typeof custom = JSON.parse(JSON.stringify(custom)); + copied.specifications![0].required = true; + + const result = compareObjectsCarr(serverResponse, mapImportCfToRequestCf(toCarrCF(copied))); + expect(result.equal).toBe(false); + }); + + test("max differ", async () => { + const copied: typeof custom = JSON.parse(JSON.stringify(custom)); + (copied.specifications![0].fields as TrashCFSpF).max = 100; + + const result = compareObjectsCarr(serverResponse, mapImportCfToRequestCf(toCarrCF(copied))); + expect(result.equal).toBe(false); + }); +}); + +describe("QualityDefinitions", async () => { + const server: QualityDefinitionResource[] = [ + { + quality: { + id: 0, + name: "Unknown", + source: QualitySource.Unknown, + resolution: 0, + }, + title: "Unknown", + weight: 1, + minSize: 1, + maxSize: 199.9, + preferredSize: 194.9, + id: 1, + }, + { + quality: { + id: 1, + name: "SDTV", + source: QualitySource.Television, + resolution: 480, + }, + title: "SDTV", + weight: 2, + minSize: 2, + maxSize: 100, + preferredSize: 95, + id: 2, + }, + ]; + + const client = { + trash_id: "aed34b9f60ee115dfa7918b742336277", + type: "movie", + qualities: [ + { + quality: "SDTV", + min: 2, + preferred: 95, + max: 100, + }, + ], + }; + test("test import", async ({}) => { + const result = await loadQualityDefinitionFromServer(); + + console.log(result); + }); + + test("calculateQualityDefinitionDiff - no diff", async ({}) => { + const result = calculateQualityDefinitionDiff(server, client); + + expect(result.changeMap.size).toBe(0); + expect(result.create).toHaveLength(0); + }); + + test("calculateQualityDefinitionDiff - diff min size", async ({}) => { + const clone: TrashQualityDefintion = JSON.parse(JSON.stringify(client)); + clone.qualities[0].min = 3; + + const result = calculateQualityDefinitionDiff(server, clone); + + expect(result.changeMap.size).toBe(1); + expect(result.create).toHaveLength(0); + }); + + test("calculateQualityDefinitionDiff - diff max size", async ({}) => { + const clone: TrashQualityDefintion = JSON.parse(JSON.stringify(client)); + clone.qualities[0].max = 3; + + const result = calculateQualityDefinitionDiff(server, clone); + + expect(result.changeMap.size).toBe(1); + expect(result.create).toHaveLength(0); + }); + + test("calculateQualityDefinitionDiff - diff preferred size", async ({}) => { + const clone: TrashQualityDefintion = JSON.parse(JSON.stringify(client)); + clone.qualities[0].preferred = 3; + + const result = calculateQualityDefinitionDiff(server, clone); + + expect(result.changeMap.size).toBe(1); + expect(result.create).toHaveLength(0); + }); + + test("calculateQualityDefinitionDiff - create new element", async ({}) => { + const clone: TrashQualityDefintion = JSON.parse(JSON.stringify(client)); + clone.qualities[0].quality = "New"; + + const result = calculateQualityDefinitionDiff(server, clone); + + expect(result.changeMap.size).toBe(0); + expect(result.create).toHaveLength(1); + }); +}); diff --git a/src/quality-definitions.ts b/src/quality-definitions.ts new file mode 100644 index 0000000..3ecb8d6 --- /dev/null +++ b/src/quality-definitions.ts @@ -0,0 +1,63 @@ +import path from "path"; +import { QualityDefinitionResource } from "./__generated__/generated-sonarr-api"; +import { getArrApi } from "./api"; +import { TrashQualityDefintion, TrashQualityDefintionQuality } from "./types"; +import { IS_LOCAL_SAMPLE_MODE } from "./util"; + +export const loadQualityDefinitionFromServer = async (): Promise => { + if (IS_LOCAL_SAMPLE_MODE) { + return (await import(path.resolve("./tests/samples/qualityDefinition.json"))).default; + } + return (await getArrApi().v3QualitydefinitionList()).data as QualityDefinitionResource[]; +}; + +export const calculateQualityDefinitionDiff = (serverQDs: QualityDefinitionResource[], trashQD: TrashQualityDefintion) => { + const serverMap = serverQDs.reduce((p, c) => { + p.set(c.title!, c); + return p; + }, new Map()); + + const changeMap = new Map(); + const create: TrashQualityDefintionQuality[] = []; + + const restData: QualityDefinitionResource[] = []; + + for (const tq of trashQD.qualities) { + const element = serverMap.get(tq.quality); + + if (element) { + const changes: string[] = []; + + if (!element.maxSize) { + console.log(`No maxSize defined: ${element.title}`); + } + + if (element.minSize !== tq.min) { + changes.push(`MinSize diff: ${element.minSize} - ${tq.min}`); + } + if (element.maxSize !== tq.max) { + changes.push(`MaxSize diff: ${element.maxSize} - ${tq.max}`); + } + if (element.preferredSize !== tq.preferred) { + changes.push(`PreferredSize diff: ${element.preferredSize} - ${tq.preferred}`); + } + + if (changes.length > 0) { + changeMap.set(element.title!, changes); + restData.push({ + ...element, + maxSize: tq.max, + minSize: tq.min, + preferredSize: tq.preferred, + }); + } else { + restData.push(element); + } + } else { + // TODO create probably never happens? + create.push(tq); + } + } + + return { changeMap, restData, create }; +}; diff --git a/src/quality-profiles.ts b/src/quality-profiles.ts new file mode 100644 index 0000000..a721e67 --- /dev/null +++ b/src/quality-profiles.ts @@ -0,0 +1,460 @@ +import path from "path"; +import { + CustomFormatResource, + ProfileFormatItemResource, + QualityDefinitionResource, + QualityProfileQualityItemResource, + QualityProfileResource, +} from "./__generated__/generated-sonarr-api"; +import { getArrApi } from "./api"; +import { loadServerCustomFormats } from "./custom-formats"; +import { loadQualityDefinitionFromServer } from "./quality-definitions"; +import { CFProcessing, RecyclarrMergedTemplates, YamlConfigQualityProfile, YamlConfigQualityProfileItems, YamlList } from "./types"; +import { IS_LOCAL_SAMPLE_MODE, notEmpty } from "./util"; + +export const mapQualityProfiles = ({ carrIdMapping }: CFProcessing, customFormats: YamlList[], config: RecyclarrMergedTemplates) => { + // QualityProfile -> (CF Name -> Scoring) + const profileScores = new Map>(); + + const defaultScoringMap = new Map(config.quality_profiles.map((obj) => [obj.name, obj])); + + for (const { trash_ids, quality_profiles } of customFormats) { + if (!trash_ids) { + continue; + } + + for (const profile of quality_profiles) { + for (const trashId of trash_ids) { + const carr = carrIdMapping.get(trashId); + + if (!carr) { + console.log(`Unknown ID for CF. ${trashId}`); + continue; + } + + let selectedProfileMap = profileScores.get(profile.name); + + if (!selectedProfileMap) { + const newMap = new Map(); + profileScores.set(profile.name, newMap); + selectedProfileMap = newMap; + } + + let cfScore = selectedProfileMap.get(carr.carrConfig.name!); + + if (!cfScore) { + const newScore = {}; + selectedProfileMap.set(carr.carrConfig.name!, newScore); + cfScore = newScore; + } + + cfScore.name = carr.carrConfig.name; + + const profileScoreConfig = defaultScoringMap.get(profile.name); + + let score_set: number | undefined; + + if (profileScoreConfig && profileScoreConfig.score_set) { + score_set = carr.carrConfig.configarr_scores?.[profileScoreConfig.score_set]; + } + + // TODO (1): Don't set to 0. If undefined will be handled by reset_unmatched_score. Or should we directly handle this here? + cfScore.score = profile.score ?? score_set ?? carr.carrConfig.configarr_scores?.default; + } + } + } + + return profileScores; +}; + +export const loadQualityProfilesFromServer = async (): Promise => { + if (IS_LOCAL_SAMPLE_MODE) { + return (await import(path.resolve(`./tests/samples/quality_profiles.json`))).default; + } + const api = getArrApi(); + + const qualityProfiles = await api.v3QualityprofileList(); + return qualityProfiles.data as QualityProfileResource[]; +}; + +const mapQualities = (qd: QualityDefinitionResource[], value: YamlConfigQualityProfile) => { + const qdMap = new Map(qd.map((obj) => [obj.title, obj])); + + const allowedQualities: QualityProfileQualityItemResource[] = value.qualities.map((obj, i) => { + if (obj.qualities?.length && obj.qualities.length > 0) { + return { + allowed: true, + id: 1000 + i, + name: obj.name, + items: obj.qualities?.map((obj2) => { + const qd = qdMap.get(obj2); + + const returnObject: QualityProfileQualityItemResource = { + quality: { + id: qd?.quality?.id, + name: obj2, + resolution: qd?.quality?.resolution, + source: qd?.quality?.source, + }, + }; + + qdMap.delete(obj2); + + return returnObject; + }), + }; + } else { + const serverQD = qdMap.get(obj.name); + qdMap.delete(obj.name); + + return { + allowed: true, + items: [], + quality: { + ...serverQD?.quality, + }, + }; + } + }); + + const missingQualities: QualityProfileQualityItemResource[] = []; + + for (const [key, value] of qdMap.entries()) { + missingQualities.push({ + allowed: false, + //id: qualIndex++, // ID not allowed if not enabled + quality: { + id: value.quality?.id, + name: key, + resolution: value.quality?.resolution, + source: value?.quality?.source, + }, + }); + } + + // Ordering of items in the array matters of how they will be displayed. First is last. + // Need to double check if always works as expected also regarding of templates etc. + return [...missingQualities, ...allowedQualities.reverse()]; +}; + +export const compareQualities = (obj1: YamlConfigQualityProfileItems[], obj2: YamlConfigQualityProfileItems[]) => { + function arraysEqual(arr1: string[], arr2: string[]): boolean { + if (arr1.length !== arr2.length) { + return false; + } + + const sortedArr1 = arr1.slice().sort(); + const sortedArr2 = arr2.slice().sort(); + + for (let i = 0; i < sortedArr1.length; i++) { + if (sortedArr1[i] !== sortedArr2[i]) { + return false; + } + } + + return true; + } + + if (obj1.length !== obj2.length) { + return false; + } + + const sorted1 = obj1.sort((a, b) => (a.name < b.name ? -1 : 1)); + const sorted2 = obj2.sort((a, b) => (a.name < b.name ? -1 : 1)); + + for (let index = 0; index < sorted1.length; index++) { + const element1 = sorted1[index]; + const element2 = sorted2[index]; + + if (element1.name !== element2.name) { + return false; + } + + if (!arraysEqual(element1.qualities ?? [], element2.qualities ?? [])) { + return false; + } + } + + return true; +}; + +export const calculateQualityProfilesDiff = async ( + cfManaged: CFProcessing, + qpMerged: Map, + scoring: Map>, + serverQP: QualityProfileResource[], +): Promise<{ + changedQPs: QualityProfileResource[]; + create: QualityProfileResource[]; + noChanges: string[]; +}> => { + const mappedServerQP = serverQP.reduce((p, c) => { + p.set(c.name!, c); + return p; + }, new Map()); + + // TODO can be optimized + const qd = await loadQualityDefinitionFromServer(); + const cfsFromServer = await loadServerCustomFormats(); + const cfsServerMap = new Map(cfsFromServer.map((obj) => [obj.name!, obj])); + + const createQPs: QualityProfileResource[] = []; + const changedQPs: QualityProfileResource[] = []; + const noChangedQPs: string[] = []; + + const changes = new Map(); + + for (const [name, value] of qpMerged.entries()) { + const serverMatch = mappedServerQP.get(name); + const scoringForQP = scoring.get(name); + + const resetScoreExceptions: Map = + value.reset_unmatched_scores?.except?.reduce((p, c) => { + p.set(c, true); + return p; + }, new Map()) ?? new Map(); + + if (!serverMatch) { + console.log(`QualityProfile not found in server. Ignoring: ${name}`); + const mappedQ = mapQualities(qd, value); + + const qualityToId = mappedQ.reduce>((p, c) => { + const id = c.id ?? c.quality?.id; + const qName = c.name ?? c.quality?.name; + + if (id == null || qName == null) { + throw new Error(`No ID (${id}) or name ${qName} found for quality? QP: ${name}`); + } + + p.set(qName, id); + + return p; + }, new Map()); + + const cfs: Map = new Map(JSON.parse(JSON.stringify(Array.from(cfsServerMap)))); + + const customFormatsMapped = Array.from(cfs.values()).map((e) => { + let score = 0; + + if (scoringForQP) { + const providedScore = scoringForQP.get(e.name!); + score = providedScore?.score || 0; + } + + return { + name: e.name, + score: score, + format: e.id, + }; + }); + + createQPs.push({ + name: value.name, + items: mappedQ, + cutoff: qualityToId.get(value.upgrade.until_quality), + cutoffFormatScore: value.upgrade.until_score, + minFormatScore: value.min_format_score, + upgradeAllowed: value.upgrade.allowed, + formatItems: customFormatsMapped, + }); + continue; + } + + const changeList: string[] = []; + changes.set(serverMatch.name!, changeList); + + const updatedServerObject: QualityProfileResource = JSON.parse(JSON.stringify(serverMatch)); + + let diffExist = false; + + const valueQualityMap = new Map(value.qualities.map((obj) => [obj.name, obj])); + + // TODO need to better validate if this quality transforming works as expected in different cases + const serverQualitiesMapped: YamlConfigQualityProfileItems[] = (serverMatch.items || []) + .map((obj): YamlConfigQualityProfileItems | null => { + let qualityName: string; + + if (obj.id) { + qualityName = obj.name!; + } else { + qualityName = obj.quality?.name!; + } + + if (!valueQualityMap.has(qualityName) && !obj.allowed) { + // Only return null if quality not specified and not enabled in arr. If enabled we need to disable it. + return null; + } + + // if ID it is a grouping + if (obj.id) { + return { + name: qualityName, + qualities: (obj.items || []).map((qObj) => { + return qObj.quality!.name!; + }), + }; + } else { + return { + name: qualityName, + qualities: [], + }; + } + }) + .filter(notEmpty); + + // TODO do we want to enforce the whole structure or only match those which are enabled by us? + if (!compareQualities(value.qualities, serverQualitiesMapped)) { + console.log(`QualityProfile Items mismatch will update whole array`); + diffExist = true; + + changeList.push(`QualityProfile items do not match`); + updatedServerObject.items = mapQualities(qd, value); + } else { + // TODO no sure if a useful feature + if (value.quality_sort === "top") { + const length = updatedServerObject.items!.length; + + // TODO sorting + } else { + } + } + + const qualityToId = updatedServerObject.items!.reduce>((p, c) => { + const id = c.id ?? c.quality?.id; + const qName = c.name ?? c.quality?.name; + + if (id == null || qName == null) { + throw new Error(`No ID (${id}) or name ${qName} found for quality? QP: ${name}`); + } + + p.set(qName, id); + + return p; + }, new Map()); + + if (value.min_format_score) { + if (serverMatch.minFormatScore !== value.min_format_score) { + updatedServerObject.minFormatScore = value.min_format_score; + diffExist = true; + changeList.push(`MinFormatScore diff: server: ${serverMatch.minFormatScore} - expected: ${value.min_format_score}`); + } + } + + if (value.upgrade) { + if (serverMatch.upgradeAllowed !== value.upgrade.allowed) { + updatedServerObject.upgradeAllowed = value.upgrade.allowed; + diffExist = true; + + changeList.push(`UpgradeAllowed diff: server: ${serverMatch.upgradeAllowed} - expected: ${value.upgrade.allowed}`); + } + + const upgradeUntil = qualityToId.get(value.upgrade.until_quality); + + if (!upgradeUntil) { + throw new Error(`Did not find expected Quality to upgrade until: ${value.upgrade.until_quality}`); + } + + if (serverMatch.cutoff !== upgradeUntil) { + updatedServerObject.cutoff = upgradeUntil; + diffExist = true; + changeList.push(`Upgrade until quality diff: server: ${serverMatch.cutoff} - expected: ${upgradeUntil}`); + } + + if (serverMatch.cutoffFormatScore !== value.upgrade.until_score) { + updatedServerObject.cutoffFormatScore = value.upgrade.until_score; + diffExist = true; + + changeList.push(`Upgrade until score diff: server: ${serverMatch.cutoffFormatScore} - expected: ${value.upgrade.until_score}`); + } + } + + // CFs matching + const serverCFMap = new Map(serverMatch.formatItems!.map((obj) => [obj.name!, obj])); + + let scoringDiff = false; + + if (scoringForQP) { + const newCFFormats: ProfileFormatItemResource[] = []; + + for (const [scoreKey, scoreValue] of scoringForQP.entries()) { + const serverCF = serverCFMap.get(scoreKey); + serverCFMap.delete(scoreKey); + + // TODO (1): check where best handled + if (scoreValue.score == null) { + if (value.reset_unmatched_scores?.enabled && !resetScoreExceptions.has(scoreKey) && serverCF?.score !== 0) { + scoringDiff = true; + changeList.push(`CF resetting score '${scoreValue.name}': server ${serverCF?.score} - client: 0`); + newCFFormats.push({ ...serverCF, score: 0 }); + } else { + newCFFormats.push({ ...serverCF }); + } + } else { + if (serverCF?.score !== scoreValue.score) { + scoringDiff = true; + changeList.push(`CF diff ${scoreValue.name}: server: ${serverCF?.score} - expected: ${scoreValue.score}`); + newCFFormats.push({ ...serverCF, score: scoreValue.score }); + } else { + newCFFormats.push({ ...serverCF }); + } + } + } + + const missingCfs = Array.from(serverCFMap.values()).reduce((p, c) => { + const cfName = c.name!; + const cfScore = c.score; + + if (value.reset_unmatched_scores?.enabled && !resetScoreExceptions.has(c.name!) && cfScore !== 0) { + scoringDiff = true; + changeList.push(`CF resetting score '${cfName}': server ${cfScore} - client: 0`); + p.push({ ...c, score: 0 }); + } else { + p.push(c); + } + + return p; + }, []); + + newCFFormats.push(...missingCfs); + + updatedServerObject.formatItems = newCFFormats; + } else { + console.log(`No scoring for QualityProfile ${serverMatch.name!} found`); + } + + console.log(`QualityProfile (${value.name}) - CF Changes: ${scoringDiff}, Some other diff: ${diffExist}`); + + if (scoringDiff || diffExist) { + changedQPs.push(updatedServerObject); + } else { + noChangedQPs.push(value.name); + } + + if (changeList.length > 0) { + console.log(`ChangeList for QualityProfile:\n`, changeList); + } else { + console.log(`QualityProfile has no changes.`); + } + } + + return { create: createQPs, changedQPs: changedQPs, noChanges: noChangedQPs }; +}; + +export const filterInvalidQualityProfiles = (profiles: YamlConfigQualityProfile[]): YamlConfigQualityProfile[] => { + return profiles.filter((p) => { + if (p.name == null) { + console.log(`QP filtered because no name provided`); + return false; + } + if (p.qualities == null) { + console.log(`QP ${p.name} filtered because no qualities provided`); + return false; + } + if (p.upgrade == null) { + console.log(`QP ${p.name} filtered because no upgrade definition provided`); + return false; + } + + return true; + }); +}; diff --git a/src/recyclarr-importer.ts b/src/recyclarr-importer.ts new file mode 100644 index 0000000..7579959 --- /dev/null +++ b/src/recyclarr-importer.ts @@ -0,0 +1,51 @@ +import { default as fs } from "fs"; +import simpleGit, { CheckRepoActions } from "simple-git"; +import yaml from "yaml"; +import { getConfig } from "./config"; +import { ArrType, RecyclarrTemplates } from "./types"; +import { recyclarrRepoPaths } from "./util"; + +const DEFAULT_RECYCLARR_GIT_URL = "https://github.com/recyclarr/config-templates"; + +export const cloneRecyclarrTemplateRepo = async () => { + const rootPath = recyclarrRepoPaths.root; + + if (!fs.existsSync(rootPath)) { + fs.mkdirSync(rootPath, { recursive: true }); + } + + const gitClient = simpleGit({ baseDir: rootPath }); + const r = await gitClient.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); + + const applicationConfig = getConfig(); + + if (!r) { + await simpleGit().clone(applicationConfig.recyclarrConfigUrl ?? DEFAULT_RECYCLARR_GIT_URL, rootPath); + } + + await gitClient.checkout(applicationConfig.trashRevision ?? "master"); + + console.log(`Updating Recyclarr repo`); +}; + +export const loadRecyclarrTemplates = (arrType: ArrType) => { + const map = new Map(); + + const fillMap = (path: string) => { + const files = fs.readdirSync(`${path}`).filter((fn) => fn.endsWith("yml")); + + files.forEach((f) => map.set(f.substring(0, f.lastIndexOf(".")), yaml.parse(fs.readFileSync(`${path}/${f}`, "utf8")))); + }; + + if (arrType === "RADARR") { + fillMap(recyclarrRepoPaths.radarrCF); + fillMap(recyclarrRepoPaths.radarrQD); + fillMap(recyclarrRepoPaths.radarrQP); + } else { + fillMap(recyclarrRepoPaths.sonarrCF); + fillMap(recyclarrRepoPaths.sonarrQD); + fillMap(recyclarrRepoPaths.sonarrQP); + } + + return map; +}; diff --git a/src/trash-guide.ts b/src/trash-guide.ts new file mode 100644 index 0000000..37be9d5 --- /dev/null +++ b/src/trash-guide.ts @@ -0,0 +1,107 @@ +import fs from "fs"; +import path from "path"; +import simpleGit, { CheckRepoActions } from "simple-git"; +import { CustomFormatResource } from "./__generated__/generated-sonarr-api"; +import { getConfig } from "./config"; +import { + ArrType, + CFProcessing, + ConfigarrCF, + DynamicImportType, + QualityDefintionsRadarr, + QualityDefintionsSonarr, + TrashCF, + TrashQualityDefintion, +} from "./types"; +import { mapImportCfToRequestCf, toCarrCF, trashRepoPaths } from "./util"; + +const DEFAULT_TRASH_GIT_URL = "https://github.com/TRaSH-Guides/Guides"; + +export const cloneTrashRepo = async () => { + const rootPath = trashRepoPaths.root; + + if (!fs.existsSync(rootPath)) { + fs.mkdirSync(rootPath, { recursive: true }); + } + + const gitClient = simpleGit({ baseDir: rootPath }); + const r = await gitClient.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); + + const applicationConfig = getConfig(); + + if (!r) { + await simpleGit().clone(applicationConfig.trashGuideUrl ?? DEFAULT_TRASH_GIT_URL, rootPath); + } + + await gitClient.checkout(applicationConfig.trashRevision ?? "master"); + + console.log(`Updating TrashGuide repo`); +}; + +export const loadSonarrTrashCFs = async (arrType: ArrType): Promise => { + const trashRepoPath = "./repos/trash-guides"; + const trashJsonDir = "docs/json"; + const trashRadarrPath = `${trashJsonDir}/radarr`; + const trashRadarrCfPath = `${trashRadarrPath}/cf`; + + const trashSonarrPath = `${trashJsonDir}/sonarr`; + const trashSonarrCfPath = `${trashSonarrPath}/cf`; + + const trashIdToObject = new Map(); + const carrIdToObject = new Map(); + const cfNameToCarrObject = new Map(); + + let pathForFiles: string; + + if (arrType === "RADARR") { + pathForFiles = `${trashRepoPath}/${trashRadarrCfPath}`; + } else { + pathForFiles = `${trashRepoPath}/${trashSonarrCfPath}`; + } + + const files = fs.readdirSync(pathForFiles).filter((fn) => fn.endsWith("json")); + + for (const file of files) { + const name = `${pathForFiles}/${file}`; + + const cf: DynamicImportType = await import(path.resolve(name)); + + const carrConfig = toCarrCF(cf.default); + + carrIdToObject.set(carrConfig.configarr_id, { + carrConfig: carrConfig, + requestConfig: mapImportCfToRequestCf(carrConfig), + }); + + if (carrConfig.name) { + cfNameToCarrObject.set(carrConfig.name, carrConfig); + } + } + + console.log(`Trash CFs: ${trashIdToObject.size}`); + + return { + carrIdMapping: carrIdToObject, + cfNameToCarrConfig: cfNameToCarrObject, + }; +}; + +export const loadQualityDefinitionSonarrFromTrash = async ( + qdType: QualityDefintionsSonarr | QualityDefintionsRadarr, + arrType: ArrType, +): Promise => { + let trashPath = arrType === "RADARR" ? trashRepoPaths.radarrQuality : trashRepoPaths.sonarrQuality; + + switch (qdType) { + case "anime": + return (await import(path.resolve(`${trashPath}/anime.json`))).default; + case "series": + return (await import(path.resolve(`${trashPath}/series.json`))).default; + case "movie": + return (await import(path.resolve(`${trashPath}/movie.json`))).default; + case "custom": + throw new Error("Not implemented yet"); + default: + throw new Error(`Unknown QualityDefintion type: ${qdType}`); + } +}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..6976c2b --- /dev/null +++ b/src/types.ts @@ -0,0 +1,155 @@ +import { CustomFormatResource, CustomFormatSpecificationSchema } from "./__generated__/generated-sonarr-api"; + +export type DynamicImportType = { default: T }; + +/** Used in the UI of Sonarr/Radarr to import. Trash JSON are based on that so users can copy&paste stuff */ +export type UserFriendlyField = { + name?: string | null; + value?: any; +} & Pick; + +export type TrashCFSpF = { min: number; max: number }; + +/* +Language values: +0 = Unknown +-2 = Original +*/ +export type CustomFormatImportImplementation = + | "ReleaseTitleSpecification" // Value string + | "LanguageSpecification" // value number + | "SizeSpecification" // special + | "IndexerFlagSpecification" // value number + | "SourceSpecification" // value number + | "ResolutionSpecification" // value number + | "ReleaseGroupSpecification"; // value string + +export type TC1 = Omit & { + implementation: "ReleaseTitleSpecification" | "LanguageSpecification"; + fields?: UserFriendlyField | null; +}; + +export type TC2 = Omit & { + implementation: "SizeSpecification"; + fields?: TrashCFSpF; +}; + +export type TCM = TC1 | TC2; + +export type ImportCF = Omit & { + specifications?: TCM[] | null; +}; + +export type TrashCF = { + trash_id: string; + trash_scores?: { + default?: number; + "anime-sonarr"?: number; + "anime-radarr"?: number; + "sqp-1-1080p"?: number; + "sqp-1-2160p"?: number; + "sqp-2"?: number; + "sqp-3"?: number; + "sqp-4"?: number; + "sqp-5"?: number; + "french-vostfr"?: number; + }; + trash_regex?: string; + trash_description?: string; +} & ImportCF; + +export type ConfigarrCF = { + configarr_id: string; + configarr_scores?: TrashCF["trash_scores"]; +} & ImportCF; + +export type CFProcessing = { + carrIdMapping: Map< + string, + { + carrConfig: ConfigarrCF; + requestConfig: CustomFormatResource; + } + >; + cfNameToCarrConfig: Map; +}; + +export type YamlList = { + trash_ids?: string[]; + quality_profiles: { name: string; score?: number }[]; +}; + +export type YamlInput = { + custom_formats: YamlList[]; +}; + +export type TrashQualityDefintionQuality = { + quality: string; + min: number; + preferred: number; + max: number; +}; + +export type TrashQualityDefintion = { + trash_id: string; + type: string; + qualities: TrashQualityDefintionQuality[]; +}; + +export type YamlConfigInstance = { + base_url: string; + api_key: string; + quality_definition?: { + type: string; + }; + include?: { template: string }[]; + custom_formats: YamlList[]; + quality_profiles: YamlConfigQualityProfile[]; +}; + +export type YamlConfigQualityProfile = { + name: string; + reset_unmatched_scores?: { + enabled: boolean; + except?: string[]; + }; + upgrade: { + allowed: boolean; + until_quality: string; + until_score: number; + }; + min_format_score: number; + score_set: keyof TrashCF["trash_scores"]; + quality_sort: string; + qualities: YamlConfigQualityProfileItems[]; +}; + +export type YamlConfigQualityProfileItems = { + name: string; + qualities?: string[]; +}; + +export type RecyclarrTemplates = Partial< + Pick +>; + +export type RecyclarrMergedTemplates = RecyclarrTemplates & Required>; + +export type YamlConfig = { + trashGuideUrl: string; + trashRevision?: string; + recyclarrConfigUrl: string; + recyclarrRevision?: string; + localCustomFormatsPath?: string; + sonarr: { + [key: string]: YamlConfigInstance; + }; + radarr: { + [key: string]: YamlConfigInstance; + }; +}; + +export type ArrType = "SONARR" | "RADARR"; // anime and series exists in trash guide + +export type QualityDefintionsSonarr = "anime" | "series" | "custom"; +export type QualityDefintionsRadarr = "movie" | "custom"; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..ae88863 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,199 @@ +import path from "path"; +import { CustomFormatResource } from "./__generated__/generated-sonarr-api"; +import { ConfigarrCF, ImportCF, TrashCF, UserFriendlyField } from "./types"; + +export const IS_DRY_RUN = process.env.DRY_RUN === "true"; +export const IS_LOCAL_SAMPLE_MODE = process.env.LOAD_LOCAL_SAMPLES === "true"; +export const DEBUG_CREATE_FILES = process.env.DEBUG_CREATE_FILES === "true"; + +export const repoPath = path.resolve(process.env.CUSTOM_REPO_ROOT || "./repos"); + +const recyclarrConfigPath = `${repoPath}/recyclarr-config`; +const recyclarrSonarrRoot = `${recyclarrConfigPath}/sonarr`; +const recyclarrSonarrCFs = `${recyclarrSonarrRoot}/includes/custom-formats`; +const recyclarrSonarrQDs = `${recyclarrSonarrRoot}/includes/quality-definitions`; +const recyclarrSonarrQPs = `${recyclarrSonarrRoot}/includes/quality-profiles`; + +const recyclarrRadarrRoot = `${recyclarrConfigPath}/radarr`; +const recyclarrRadarrCFs = `${recyclarrRadarrRoot}/includes/custom-formats`; +const recyclarrRadarrQDs = `${recyclarrRadarrRoot}/includes/quality-definitions`; +const recyclarrRadarrQPs = `${recyclarrRadarrRoot}/includes/quality-profiles`; + +const trashRepoPath = "docs/json"; +const trashRepoRoot = `${repoPath}/trash-guides`; +const trashRepoSonarrRoot = `${trashRepoRoot}/${trashRepoPath}/sonarr`; + +const trashRepoRadarrRoot = `${trashRepoRoot}/${trashRepoPath}/radarr`; + +export const trashRepoPaths = { + root: trashRepoRoot, + sonarrCF: `${trashRepoSonarrRoot}/cf`, + sonarrQuality: `${trashRepoSonarrRoot}/quality-size`, + sonarrNaming: `${trashRepoSonarrRoot}/naming`, + radarrCF: `${trashRepoRadarrRoot}/cf`, + radarrQuality: `${trashRepoRadarrRoot}/quality-size`, + radarrNaming: `${trashRepoRadarrRoot}/naming`, +}; + +export const recyclarrRepoPaths = { + root: recyclarrConfigPath, + sonarrCF: `${recyclarrSonarrCFs}`, + sonarrQD: `${recyclarrSonarrQDs}`, + sonarrQP: `${recyclarrSonarrQPs}`, + radarrCF: `${recyclarrRadarrCFs}`, + radarrQD: `${recyclarrRadarrQDs}`, + radarrQP: `${recyclarrRadarrQPs}`, +}; + +export const trashToCarrCF = ({ trash_id, trash_regex, trash_scores, ...rest }: TrashCF): ConfigarrCF => { + return { + ...rest, + configarr_id: trash_id, + configarr_scores: trash_scores, + }; +}; + +export const toCarrCF = (input: TrashCF | ConfigarrCF): ConfigarrCF => { + if ("configarr_id" in input) { + return input; + } + + return trashToCarrCF(input); +}; + +export const mapImportCfToRequestCf = (cf: TrashCF | ConfigarrCF): CustomFormatResource => { + let customId; + let rest: ImportCF; + + if ("trash_id" in cf) { + customId = cf.trash_id; + const { trash_id, trash_scores, ...restCf } = cf; + rest = restCf; + } else { + customId = cf.configarr_id; + const { configarr_id, configarr_scores, ...restCf } = cf; + rest = restCf; + } + + if (!rest.specifications) { + console.log(`ImportCF is wrong ${customId}, ${cf.name}.`); + throw new Error("ImportCF wrong"); + } + + const specs = rest.specifications.map((spec) => { + const newFields: UserFriendlyField[] = []; + + if (!spec.fields) { + console.log(`Spec: ${spec.name} fields is not defined`); + throw new Error(`Spec is not correctly defined: ${spec.name}`); + } + + switch (spec.implementation) { + case "SizeSpecification": + newFields.push({ + name: "min", + value: spec.fields.min, + }); + newFields.push({ + name: "max", + value: spec.fields.max, + }); + break; + case "ReleaseTitleSpecification": + case "LanguageSpecification": + default: + newFields.push({ + value: spec.fields.value, + }); + break; + } + + return { + ...spec, + fields: newFields.map((f) => { + if (f.name) { + return f; + } + + return { ...f, name: "value" }; + }), + }; + }); + + return { ...rest, specifications: specs }; +}; + +export function compareObjectsCarr(object1: any, object2: any): { equal: boolean; changes: string[] } { + const changes: string[] = []; + + for (const key in object2) { + if (Object.prototype.hasOwnProperty.call(object2, key)) { + if (object1.hasOwnProperty(key)) { + const value1 = object1[key]; + let value2 = object2[key]; + + // Todo remove should be already handled + if (key === "fields") { + if (!Array.isArray(value2)) { + value2 = [value2]; + } + } + + if (Array.isArray(value1)) { + if (!Array.isArray(value2)) { + changes.push(`Expected array for key ${key} in object2`); + continue; + } + + if (value1.length !== value2.length) { + changes.push(`Array length mismatch for key ${key}: object1 length ${value1.length}, object2 length ${value2.length}`); + continue; + } + + for (let i = 0; i < value1.length; i++) { + const { equal: isEqual, changes: subChanges } = compareObjectsCarr(value1[i], value2[i]); + // changes.push( + // ...subChanges.map((subChange) => `${key}[${i}].${subChange}`) + // ); + + if (subChanges.length > 0) { + changes.push(`${key}[${i}].${subChanges[0]}`); + } + + if (!isEqual && changes.length <= 0) { + changes.push(`Mismatch found in array element at index ${i} for key ${key}`); + } + } + } else if (typeof value2 === "object" && value2 !== null) { + if (typeof value1 !== "object" || value1 === null) { + changes.push(`Expected object for key ${key} in object1`); + continue; + } + + const { equal: isEqual, changes: subChanges } = compareObjectsCarr(value1, value2); + changes.push(...subChanges.map((subChange) => `${key}.${subChange}`)); + if (!isEqual) { + changes.push(`Mismatch found for key ${key}`); + } + } else { + if (value1 !== value2) { + changes.push(`Mismatch found for key ${key}: server value ${value1}, value to set ${value2}`); + } + } + } else { + console.log(`Ignore unknown key for comparison.`); + } + } + } + + const equal = changes.length === 0; + return { equal, changes }; +} + +export function notEmpty(value: TValue | null | undefined): value is TValue { + if (value === null || value === undefined) return false; + const testDummy: TValue = value; + return true; +} + +export const ROOT_PATH = path.resolve(process.cwd()); diff --git a/tests/e2e/demo-todo-app.spec.ts b/tests/e2e/demo-todo-app.spec.ts new file mode 100644 index 0000000..4c465a9 --- /dev/null +++ b/tests/e2e/demo-todo-app.spec.ts @@ -0,0 +1,416 @@ +import { test, expect, type Page } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("https://demo.playwright.dev/todomvc"); +}); + +const TODO_ITEMS = ["buy some cheese", "feed the cat", "book a doctors appointment"]; + +test.describe("New Todo", () => { + test("should allow me to add todo items", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press("Enter"); + + // Make sure the list only has one todo item. + await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0]]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press("Enter"); + + // Make sure the list now has two todo items. + await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test("should clear text input field when an item is added", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press("Enter"); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test("should append new items to the bottom of the list", async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId("todo-count"); + + // Check test using different methods. + await expect(page.getByText("3 items left")).toBeVisible(); + await expect(todoCount).toHaveText("3 items left"); + await expect(todoCount).toContainText("3"); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId("todo-title")).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe("Mark all as completed", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test("should allow me to mark all items as completed", async ({ page }) => { + // Complete all todos. + await page.getByLabel("Mark all as complete").check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId("todo-item")).toHaveClass(["completed", "completed", "completed"]); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test("should allow me to clear the complete state of all items", async ({ page }) => { + const toggleAll = page.getByLabel("Mark all as complete"); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId("todo-item")).toHaveClass(["", "", ""]); + }); + + test("complete all checkbox should update state when items are completed / cleared", async ({ page }) => { + const toggleAll = page.getByLabel("Mark all as complete"); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId("todo-item").nth(0); + await firstTodo.getByRole("checkbox").uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole("checkbox").check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe("Item", () => { + test("should allow me to mark items as complete", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } + + // Check first item. + const firstTodo = page.getByTestId("todo-item").nth(0); + await firstTodo.getByRole("checkbox").check(); + await expect(firstTodo).toHaveClass("completed"); + + // Check second item. + const secondTodo = page.getByTestId("todo-item").nth(1); + await expect(secondTodo).not.toHaveClass("completed"); + await secondTodo.getByRole("checkbox").check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass("completed"); + await expect(secondTodo).toHaveClass("completed"); + }); + + test("should allow me to un-mark items as complete", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } + + const firstTodo = page.getByTestId("todo-item").nth(0); + const secondTodo = page.getByTestId("todo-item").nth(1); + const firstTodoCheckbox = firstTodo.getByRole("checkbox"); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass("completed"); + await expect(secondTodo).not.toHaveClass("completed"); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass("completed"); + await expect(secondTodo).not.toHaveClass("completed"); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test("should allow me to edit an item", async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId("todo-item"); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole("textbox", { name: "Edit" })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); + await secondTodo.getByRole("textbox", { name: "Edit" }).press("Enter"); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, "buy some sausages"); + }); +}); + +test.describe("Editing", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test("should hide other controls when editing", async ({ page }) => { + const todoItem = page.getByTestId("todo-item").nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole("checkbox")).not.toBeVisible(); + await expect( + todoItem.locator("label", { + hasText: TODO_ITEMS[1], + }), + ).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test("should save edits on blur", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).dispatchEvent("blur"); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, "buy some sausages"); + }); + + test("should trim entered text", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill(" buy some sausages "); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Enter"); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, "buy some sausages"); + }); + + test("should remove the item if an empty text string was entered", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill(""); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Enter"); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test("should cancel edits on escape", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Escape"); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe("Counter", () => { + test("should display the current number of todo items", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // create a todo count locator + const todoCount = page.getByTestId("todo-count"); + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press("Enter"); + + await expect(todoCount).toContainText("1"); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press("Enter"); + await expect(todoCount).toContainText("2"); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe("Clear completed button", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test("should display the correct text", async ({ page }) => { + await page.locator(".todo-list li .toggle").first().check(); + await expect(page.getByRole("button", { name: "Clear completed" })).toBeVisible(); + }); + + test("should remove completed items when clicked", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).getByRole("checkbox").check(); + await page.getByRole("button", { name: "Clear completed" }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test("should be hidden when there are no items that are completed", async ({ page }) => { + await page.locator(".todo-list li .toggle").first().check(); + await page.getByRole("button", { name: "Clear completed" }).click(); + await expect(page.getByRole("button", { name: "Clear completed" })).toBeHidden(); + }); +}); + +test.describe("Persistence", () => { + test("should persist its data", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } + + const todoItems = page.getByTestId("todo-item"); + const firstTodoCheck = todoItems.nth(0).getByRole("checkbox"); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(["completed", ""]); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(["completed", ""]); + }); +}); + +test.describe("Routing", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test("should allow me to display active items", async ({ page }) => { + const todoItem = page.getByTestId("todo-item"); + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole("link", { name: "Active" }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test("should respect the back button", async ({ page }) => { + const todoItem = page.getByTestId("todo-item"); + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step("Showing all items", async () => { + await page.getByRole("link", { name: "All" }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step("Showing active items", async () => { + await page.getByRole("link", { name: "Active" }).click(); + }); + + await test.step("Showing completed items", async () => { + await page.getByRole("link", { name: "Completed" }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test("should allow me to display completed items", async ({ page }) => { + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole("link", { name: "Completed" }).click(); + await expect(page.getByTestId("todo-item")).toHaveCount(1); + }); + + test("should allow me to display all items", async ({ page }) => { + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole("link", { name: "Active" }).click(); + await page.getByRole("link", { name: "Completed" }).click(); + await page.getByRole("link", { name: "All" }).click(); + await expect(page.getByTestId("todo-item")).toHaveCount(3); + }); + + test("should highlight the currently applied filter", async ({ page }) => { + await expect(page.getByRole("link", { name: "All" })).toHaveClass("selected"); + + //create locators for active and completed links + const activeLink = page.getByRole("link", { name: "Active" }); + const completedLink = page.getByRole("link", { name: "Completed" }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass("selected"); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass("selected"); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return JSON.parse(localStorage["react-todos"]).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return JSON.parse(localStorage["react-todos"]).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction((t) => { + return JSON.parse(localStorage["react-todos"]) + .map((todo: any) => todo.title) + .includes(t); + }, title); +} diff --git a/tests/samples/cfs.json b/tests/samples/cfs.json new file mode 100644 index 0000000..7292c28 --- /dev/null +++ b/tests/samples/cfs.json @@ -0,0 +1,25095 @@ +[ + { + "id": 1, + "name": "BR-DISK", + "includeCustomFormatWhenRenaming": false, + "specifications": [ + { + "name": "BR-DISK", + "implementation": "ReleaseTitleSpecification", + "implementationName": "Release Title", + "infoLink": "https://wiki.servarr.com/sonarr/settings#custom-formats-2", + "negate": false, + "required": true, + "fields": [ + { + "order": 0, + "name": "value", + "label": "Regular Expression", + "helpText": "Custom Format RegEx is Case Insensitive", + "value": "^(?!.*\\b((?