diff --git a/.code-workspace b/.code-workspace new file mode 100644 index 0000000..8f0dd66 --- /dev/null +++ b/.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "front" + }, + { + "path": "server" + }, + { + "path": "." + } + ] +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ae52a1e..7b2c89d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,13 +3,17 @@ "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { "ghcr.io/devcontainers/features/node:1": {}, - "ghcr.io/devcontainers/features/go:1": {} + "ghcr.io/devcontainers/features/go:1": {}, + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12" + } }, - "postCreateCommand": "npm install -g nodemon && cd front && npm i && cd ..", - "forwardPorts": [8781, 3000], + "postCreateCommand": "npm install -g nodemon && cd front && npm i && cd ../server && python -m venv .venv", + "forwardPorts": [8765, 3000], "customizations": { "vscode": { "settings": { + "python.testing.pytestArgs": ["--no-cov"], "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": true, @@ -17,9 +21,17 @@ "[go]": { "editor.defaultFormatter": "golang.go" }, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff" + }, "files.trimTrailingWhitespace": true }, - "extensions": ["esbenp.prettier-vscode", "golang.go"] + "extensions": [ + "esbenp.prettier-vscode", + "golang.go", + "charliermarsh.ruff", + "ms-python.python" + ] } } -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index d64e8c7..3348923 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,15 @@ test .env .DS_Store data -server/build -server/main +server_go front/.eslintcache front/node_modules /public/ front/build -!front/.env \ No newline at end of file +!front/.env + + +server/.venv +__pycache__ \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 8c3e5d1..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "Run server", - "type": "shell", - "command": "nodemon --watch . --watch ../data -e go,yaml --exec go run main.go --signal SIGTERM", - "options": { - "cwd": "${workspaceFolder}/server" - }, - "group": { - "kind": "test", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Run front ", - "type": "shell", - "command": "npm run start", - "options": { - "cwd": "${workspaceFolder}/front" - }, - "group": { - "kind": "test", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - } - ] -} diff --git a/Dockerfile b/Dockerfile index 6eee884..74502e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,6 @@ -# Server build -FROM golang:1.20-alpine as server-builder - -RUN apk add --no-cache \ - alpine-sdk \ - ca-certificates \ - tzdata - -# Force the go compiler to use modules -ENV GO111MODULE=on - -ADD . /app -WORKDIR /app/server -RUN CGO_ENABLED=0 GOOS=linux go build -a -o holerr . - # Front build -FROM node:14-alpine as front-builder -RUN apk add --no-cache --virtual python3 py3-pip make g++ -RUN apk add tzdata +FROM node:20-alpine as front-builder +RUN apk add --no-cache python3 py3-pip make g++ ADD . /app WORKDIR /app/front @@ -24,20 +8,19 @@ RUN npm ci install RUN CI=false GENERATE_SOURCEMAP=false npm run build:docker # Final image -FROM scratch +FROM python:3.12-alpine ARG APP_VERSION -ENV IS_IN_DOCKER=1 ENV APP_VERSION=${APP_VERSION} # copy front files COPY --from=front-builder /app/public /app/public # copy server files -COPY --from=server-builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt -COPY --from=server-builder /usr/share/zoneinfo /usr/share/zoneinfo -COPY --from=server-builder /app/server/holerr /app/server/holerr +COPY ./server /app/server -ENTRYPOINT ["/app/server/holerr"] +WORKDIR /app/server +RUN pip install -r requirements.txt +CMD ["python", "-m", "holerr"] -EXPOSE 8781 \ No newline at end of file +EXPOSE 8765 \ No newline at end of file diff --git a/README.md b/README.md index 24e9fcf..5c3ace7 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ [tippin_svg]: https://img.shields.io/badge/donate-BuyMeACoffee-ffdd00?logo=buymeacoffee&style=flat [tippin_url]: https://www.buymeacoffee.com/bemble -> Torrent -> Real Debrid -> Downloader +> Torrent & magnet -> Debrider -> Downloader ## Running ### Docker ```bash -docker run -v "/local/data:/app/data" -p8781:8781 ghcr.io/bemble/holerr:latest +docker run -v "/local/data:/app/data" -p8765:8765 ghcr.io/bemble/holerr:latest ``` ### Docker compose @@ -26,46 +26,66 @@ holerr: container_name: holerr restart: unless-stopped ports: - - ${PORT_HOLERR}:8781 + - ${PORT_HOLERR}:8765 volumes: - "${PWD}/holerr/data:/app/data" - /usr/share/zoneinfo:/usr/share/zoneinfo:ro - /etc/localtime:/etc/localtime:ro ``` +## Supported debriders & downloaders + +**Debriders:** + +- Real-Debrid + +**Downloaders:** + +- Synology Download Station + +## Migrate from v1 + +The main file to migrate is the configuration, but holerr will migrate your configuration file from v1 to v2 after its first starts. +:warning: Also note that default port has changed, it's now `8765` + ## Configuration -Create a `config.json` file in the data directory. - -```typescript -type Configuration = { - // Set holerr in debug (default: false) - debug: boolean; - // If necessary, the base_path to fetch the front [optional, default: ""] example: "/holerr" - base_path: string; - // Debriders, providers that will download the torrent - debriders: { - // Real-Debrid - real_debrid: { - // Real-Debrid private API token: https://real-debrid.com/apitoken - api_key: string; - }; - }; - // Downloaders, providers that will download the files downloaded by the debrider - downloaders: { - // Synology - synology_download_station: { - // Your Synology endpoint (example: "http://192.168.1.1:5000") - endpoint: string; - // DSM username (this user must not have 2FA) - username: string; - // DSM password - password: string; - }; - }; - // Presets, see in Data structure - presets: Preset[]; -}; +Create a `config.yaml` file in the data directory. + +```yaml +# Select the logger to set in debug +debug: # optional, string[] + - holerr.* +# If necessary, project base path (root path), when run behind a proxy fo example +base_path: /holerr # optional, string +# Debrider configuration +debrider: + real_debrid: + # Real-Debrid private API token: https://real-debrid.com/apitoken + api_key: your key # string +# Downloader configuration +downloader: + # Synology + synology_download_station: + # Your Synology endpoint (example: "http://192.168.1.1:5000") + endpoint: synology endpoint # string + # DSM username (this user must not have 2FA) + username: dsm user # string + # DSM password + password: use password # string +# Presets list +presets: + - name: Downloads # string + # Directory that holerr will watch + watch_dir: holes/downloads # string + # Downloader output directory + output_dir: Downloads # string + # Whether the file should be downloaded in a subdoctory or not + create_sub_dir: false # optional, boolean + # Restrict the extensions to download, default no-restriction + file_extensions: # optional, string[] + # Restrict the size of the files to download, default no restriction + min_file_size: # human readbale string, example: 3.0B, 12KB, 432.2MB, 4.5GB, 1TB ``` ### Synology specifics @@ -90,276 +110,33 @@ Before being able to start a download in Download Station you **must**: - Add a configuration file - :warning: while developing, you should not use a `base_path` -### Running the front and the server - -Open VS Code, start the dev container. There's 2 tasks: - -- Run front -- Run server - -**Note:** if you set an API key in your configuration, you must set it when running front: `REACT_APP_API_KEY= npm run start` - -## API documentation - -### Websocket - -A websocket connection will push events when concerned. +### Prepare environment -- **Endpoint:** `/api/ws` -- **Protocol:** `ws` +#### Front -Example: `ws://192.168.1.1:8781/api/ws` - -#### Messages +```bash +cd front +npm i +``` -Messages are JSON objects stringified, with the following shape: +#### Server -```typescript -type WebsocketInputMessage = { - // Action to perform - action: string; - // Payload, if concerned - payload?: any; -}; -``` +Create a virtual environment: -##### Actions - -- **`downloads/new`:** new download found, payload is the new download - ```typescript - type WebsocketDownloadsNewInputMessage = { - action: "downloads/new"; - payload: Download; - }; - ``` -- **`downloads/update`:** download has been updated, payload is the updated download - ```typescript - type WebsocketDownloadsUpdateInputMessage = { - action: "downloads/update"; - payload: Download; - }; - ``` -- **`downloads/delete`:** download has been deleted, payload is the deleted download (also send - on `/api/downloads/clean_up`) - ```typescript - type WebsocketDownloadsUpdateInputMessage = { - action: "downloads/delete"; - payload: Download; - }; - ``` - -### HTTP - -#### Configuration - -- **List:** `[GET] /api/configuration` get the configuration (passwords and API keys are obfuscated) -- **Update:** `[PATCH] /api/configuration` update configuration parts - -#### Status - -- **Get:** `[GET] /api/status` get the status and other information - -#### Constants - -- **List:** `[GET] /api/constants` get the list of used constants - -#### Presets - -- **List:** `[GET] /api/presets` get the list of presets -- **Add:** `[POST] /api/presets` add the given preset, replies with the preset added -- **Update:** `[PATCH] /api/presets/:name` update the preset given by its name, replies with the updated preset -- **Delete:** `[DELETE] /api/presets/:name` delete the preset given by its name - -#### Downloads - -- **List:** `[GET] /api/downloads` get the list of downloads -- **Add:** `[POST] /api/downloads` add a new torrent to holerr - - Data are sent using FormData. - - _Body structure:_ - - ```typescript - type PostTorrentBody = { - // Preset name - preset: string; - // Torrent file - torrent_file: File; - }; - ``` - - _Example:_ - - ```javascript - const body = new FormData(); - body.append("preset", "Film"); - body.append("torrent_file", fileInput.files[0], "myFile.torrent"); - - fetch("/api/downloads", { method: "POST", body }) - .then((response) => response.text()) - .then((result) => console.log(result)) - .catch((error) => console.log("error", error)); - ``` - -- **Delete:** `[DELETE] /api/downloads/:id` abort the current download (torrent on debrider and download task on - downloader will be deleted) and delete download from database -- **Clean:** `[POST] /api/downloads/clean_up` remove downloads sent to downloader or in error from database - -## Data structure - -### Configuration - -```typescript -type Configuration = { - // API key used to communicate with the server [default: ""] - api_key: string; - // Set holerr in debug (default: false) - debug: boolean; - // If necessary, the base_path to fetch the front [optional, default: "/"] example: "/holerr" - base_path: string; - // Debriders, providers that will download the torrent - debriders?: Debriders; - // Downloaders, providers that will download the files downloaded by the debrider - downloaders?: Downloaders; - // Download presets - presets?: Preset[]; -}; - -type Debriders = { - // Real-Debrid - real_debrid: RealDebrid; -}; - -type RealDebrid = { - // Real-Debrid private API token: https://real-debrid.com/apitoken - api_key: string; -}; - -type Downloaders = { - // Synology - synology_download_station: SynologyDownloadStation; -}; - -type SynologyDownloadStation = { - // Your Synology endpoint (example: "http://192.168.1.1:5000") - endpoint: string; - // DSM username (this user must not have 2FA) - username: string; - // DSM password - password: string; -}; -``` +- In VSCode, open command palette (`Shift+CMD+P`) > `Python: Create Environment...` +- Select `Venv` +- Select `server` workspace +- Check `requirements.txt` -### Status +### Running the front and the server -```typescript -type Status = { - // Is connected to debrider - debrider_connected: bool; - // Is connected to downloader - downloader_connected: bool; -}; -``` +Open VS Code, start the dev container. There's 2 tasks: -### Constants +- Run front +- Run server -```typescript -type Constants = { - // Global download task status - download_status: Record; - // Torrent status from debrider - torrent_status: Record; -}; -``` +Database migrations will be run before the server starts. -### Presets - -```typescript -type Preset = { - // Human readable name - name: string; - // Where holerr will watch torrents, relative to data dir - watch_dir: string; - // Downloader output directory [optional] (in Synology, must start with a shared folder) - output_dir: string; - // If true, create a sub directory, into output_dir, per download task and download files in it [optional, default: false] - create_sub_dir: bool; - // Accepted file extensions [optional] - file_extensions: string[] | null; - // Minimum file size to download [optional, default: 0] - min_file_size: number; - // Whether downloader should download in subdir - create_sub_dir?: boolean; -}; -``` +## API documentation -### Downloads - -```typescript -type Download = { - // Internal download id - id: string; - // Download title (computed from torrent file name) - title: string; - // Name of the preset used - preset: string; - // Global downlaod task status - status: DownloadStatus; - // Human readable status - status_details: string; - // Debrider torrent info (might have more data, according to the debrider used) - torrent_info: TorrentInfo | null; - // Downloader downloads info - download_info: DownloadInfo | null; - // Download task creation date - created_at: string; - // Download task last update date - updated_at: string; -}; - -type TorrentInfo = { - // Debrider torrent ID - id: string; - // Torrent filename - filename: string; - // Size of selected files only - bytes: int; - // Download progress [0...100] - progress: int; - // Current status of the torrent - status: string; - // List of file available in the torrent - files: DownloadFile[]; - // When torrent is downloaded, list of files - links: string[] | null; -}; - -type DownloadFile = { - // Debrider file ID - id: int; - // Path to the file inside the torrent, starting with "/" - path: string; - // Size of the file - bytes: int; - // Whether the file is selected for download or not [0,1] - selected: int; -}; - -type DownloadInfo = { - // Download progress [0...100] - progress: number; - // Total files size - bytes: number; - // Key are links - tasks: Record; -}; - -type DownloadInfoTask = { - // Downloader ID - id: string; - // Tasks status - status: number; - // Downloaded - bytes_downloaded: number; -}; -``` +Simply navigate to http://localhost:8765/docs or http://localhost:8765/redoc. diff --git a/front/.vscode/tasks.json b/front/.vscode/tasks.json new file mode 100644 index 0000000..e2a1c5b --- /dev/null +++ b/front/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Run", + "type": "shell", + "command": "npm run start", + "options": {}, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + } + ] +} diff --git a/front/package-lock.json b/front/package-lock.json index ce96636..9c5e083 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -32,7 +32,6 @@ "i18next": "^21.2.0", "i18next-browser-languagedetector": "^6.1.2", "luxon": "^2.0.2", - "node-sass": "^7.0.1", "pretty-bytes": "^5.6.0", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -43,6 +42,7 @@ "react-router-dom": "^5.3.0", "react-scripts": "^5.0.1", "redux-persist": "^6.0.0", + "sass": "^1.76.0", "typescript": "^4.6.3" } }, @@ -2185,7 +2185,9 @@ "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==" + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true, + "peer": true }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", @@ -2767,6 +2769,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "peer": true, "dependencies": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" @@ -2776,6 +2780,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "optional": true, + "peer": true, "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" @@ -3503,7 +3509,9 @@ "node_modules/@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "optional": true, + "peer": true }, "node_modules/@types/node": { "version": "16.11.27", @@ -3513,7 +3521,9 @@ "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "optional": true, + "peer": true }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -4051,7 +4061,9 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true, + "peer": true }, "node_modules/accepts": { "version": "1.3.8", @@ -4176,6 +4188,8 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "peer": true, "dependencies": { "debug": "^4.1.0", "depd": "^1.1.2", @@ -4189,6 +4203,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "peer": true, "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -4329,12 +4345,16 @@ "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true, + "peer": true }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "peer": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -4433,6 +4453,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4446,6 +4468,8 @@ "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "optional": true, + "peer": true, "dependencies": { "safer-buffer": "~2.1.0" } @@ -4454,6 +4478,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "optional": true, + "peer": true, "engines": { "node": ">=0.8" } @@ -4475,6 +4501,8 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "optional": true, + "peer": true, "engines": { "node": "*" } @@ -4547,6 +4575,8 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "optional": true, + "peer": true, "engines": { "node": "*" } @@ -4554,7 +4584,9 @@ "node_modules/aws4": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "optional": true, + "peer": true }, "node_modules/axe-core": { "version": "4.4.1", @@ -4815,6 +4847,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "peer": true, "dependencies": { "tweetnacl": "^0.14.3" } @@ -5022,6 +5056,8 @@ "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "peer": true, "dependencies": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", @@ -5095,6 +5131,8 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "optional": true, + "peer": true, "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", @@ -5144,7 +5182,9 @@ "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "optional": true, + "peer": true }, "node_modules/chalk": { "version": "4.1.2", @@ -5223,6 +5263,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "peer": true, "engines": { "node": ">=10" } @@ -5265,6 +5307,8 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -5398,6 +5442,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "peer": true, "bin": { "color-support": "bin.js" } @@ -5511,7 +5557,9 @@ "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true, + "peer": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -6043,6 +6091,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "optional": true, + "peer": true, "dependencies": { "assert-plus": "^1.0.0" }, @@ -6083,6 +6133,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6091,6 +6143,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "optional": true, + "peer": true, "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" @@ -6103,6 +6157,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6188,7 +6244,9 @@ "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "optional": true, + "peer": true }, "node_modules/depd": { "version": "1.1.2", @@ -6445,6 +6503,8 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "peer": true, "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -6511,6 +6571,7 @@ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "optional": true, + "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -6539,6 +6600,8 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -6546,7 +6609,9 @@ "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true, + "peer": true }, "node_modules/error-ex": { "version": "1.3.2", @@ -7443,7 +7508,9 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true, + "peer": true }, "node_modules/extsprintf": { "version": "1.3.0", @@ -7451,7 +7518,9 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "engines": [ "node >=0.6.0" - ] + ], + "optional": true, + "peer": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -7690,6 +7759,8 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "optional": true, + "peer": true, "engines": { "node": "*" } @@ -7844,6 +7915,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -7896,6 +7969,8 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "peer": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", @@ -7915,6 +7990,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "optional": true, + "peer": true, "dependencies": { "globule": "^1.0.0" }, @@ -7968,6 +8045,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8002,6 +8081,8 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "optional": true, + "peer": true, "dependencies": { "assert-plus": "^1.0.0" } @@ -8107,6 +8188,8 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.3.tgz", "integrity": "sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg==", + "optional": true, + "peer": true, "dependencies": { "glob": "~7.1.1", "lodash": "~4.17.10", @@ -8120,6 +8203,8 @@ "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "optional": true, + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8139,6 +8224,8 @@ "version": "3.0.8", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "optional": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -8174,6 +8261,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "optional": true, + "peer": true, "engines": { "node": ">=4" } @@ -8183,6 +8272,8 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "deprecated": "this library is no longer supported", + "optional": true, + "peer": true, "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -8195,6 +8286,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -8270,7 +8363,9 @@ "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "optional": true, + "peer": true }, "node_modules/he": { "version": "1.2.0", @@ -8318,6 +8413,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "optional": true, + "peer": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -8455,7 +8552,9 @@ "node_modules/http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "optional": true, + "peer": true }, "node_modules/http-deceiver": { "version": "1.2.7", @@ -8535,6 +8634,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "optional": true, + "peer": true, "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -8569,6 +8670,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "optional": true, + "peer": true, "dependencies": { "ms": "^2.0.0" } @@ -8663,6 +8766,11 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8723,7 +8831,9 @@ "node_modules/infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true, + "peer": true }, "node_modules/inflight": { "version": "1.0.6", @@ -8768,7 +8878,9 @@ "node_modules/ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "optional": true, + "peer": true }, "node_modules/ipaddr.js": { "version": "2.0.1", @@ -8913,7 +9025,9 @@ "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=" + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "optional": true, + "peer": true }, "node_modules/is-module": { "version": "1.0.0", @@ -9098,7 +9212,9 @@ "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "optional": true, + "peer": true }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -9855,7 +9971,9 @@ "node_modules/js-base64": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", - "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "optional": true, + "peer": true }, "node_modules/js-tokens": { "version": "4.0.0", @@ -9877,7 +9995,9 @@ "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true, + "peer": true }, "node_modules/jsdom": { "version": "16.7.0", @@ -9963,7 +10083,9 @@ "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "optional": true, + "peer": true }, "node_modules/json5": { "version": "2.2.1", @@ -9999,6 +10121,8 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "optional": true, + "peer": true, "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -10323,6 +10447,8 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "peer": true, "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", @@ -10357,6 +10483,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "optional": true, + "peer": true, "engines": { "node": ">=8" }, @@ -10392,6 +10520,8 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "optional": true, + "peer": true, "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", @@ -10602,6 +10732,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "optional": true, + "peer": true, "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", @@ -10615,6 +10747,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10623,6 +10757,8 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "optional": true, + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10634,6 +10770,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -10645,6 +10783,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.1.0", "minipass-sized": "^1.0.3", @@ -10661,6 +10801,8 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -10672,6 +10814,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -10683,6 +10827,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0" }, @@ -10694,6 +10840,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -10706,6 +10854,8 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "peer": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -10733,7 +10883,9 @@ "node_modules/nan": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "optional": true, + "peer": true }, "node_modules/nanoid": { "version": "3.3.3", @@ -10785,6 +10937,8 @@ "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "peer": true, "dependencies": { "env-paths": "^2.2.0", "glob": "^7.1.4", @@ -10808,6 +10962,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz", "integrity": "sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw==", + "optional": true, + "peer": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -10820,6 +10976,8 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "peer": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", @@ -10838,6 +10996,8 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.1.tgz", "integrity": "sha512-BTHDvY6nrRHuRfyjt1MAufLxYdVXZfd099H4+i1f0lPywNQyI4foeNXJRObB/uy+TYqUW0vAD9gbdSOXPst7Eg==", + "optional": true, + "peer": true, "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", @@ -10863,6 +11023,8 @@ "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-7.0.1.tgz", "integrity": "sha512-uMy+Xt29NlqKCFdFRZyXKOTqGt+QaKHexv9STj2WeLottnlqZEEWx6Bj0MXNthmFRRdM/YwyNo/8Tr46TOM0jQ==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "async-foreach": "^0.1.3", "chalk": "^4.1.2", @@ -10891,6 +11053,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "peer": true, "dependencies": { "abbrev": "1" }, @@ -10905,6 +11069,8 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "optional": true, + "peer": true, "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", @@ -10957,6 +11123,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "peer": true, "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", @@ -10984,6 +11152,8 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "optional": true, + "peer": true, "engines": { "node": "*" } @@ -11220,6 +11390,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "peer": true, "dependencies": { "aggregate-error": "^3.0.0" }, @@ -12686,12 +12858,16 @@ "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "optional": true, + "peer": true }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "peer": true, "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -12773,6 +12949,8 @@ "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "optional": true, + "peer": true, "engines": { "node": ">=0.6" } @@ -12800,6 +12978,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -13189,6 +13369,8 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "optional": true, + "peer": true, "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", @@ -13203,6 +13385,8 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "optional": true, + "peer": true, "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", @@ -13219,6 +13403,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "optional": true, + "peer": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -13231,6 +13417,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "optional": true, + "peer": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -13242,6 +13430,8 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "optional": true, + "peer": true, "dependencies": { "p-try": "^2.0.0" }, @@ -13256,6 +13446,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "optional": true, + "peer": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -13267,6 +13459,8 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -13274,12 +13468,16 @@ "node_modules/read-pkg/node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "optional": true, + "peer": true }, "node_modules/read-pkg/node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "optional": true, + "peer": true, "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -13291,6 +13489,8 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true, + "peer": true, "bin": { "semver": "bin/semver" } @@ -13299,6 +13499,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -13511,6 +13713,8 @@ "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "optional": true, + "peer": true, "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -13541,6 +13745,8 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "optional": true, + "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -13554,6 +13760,8 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "optional": true, + "peer": true, "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -13688,6 +13896,8 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "optional": true, + "peer": true, "engines": { "node": ">= 4" } @@ -13801,10 +14011,28 @@ "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" }, + "node_modules/sass": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.76.0.tgz", + "integrity": "sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/sass-graph": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.0.tgz", "integrity": "sha512-WSO/MfXqKH7/TS8RdkCX3lVkPFQzCgbqdGsmSKq6tlPU+GpGEsa/5aW18JqItnqh+lPtcjifqdZ/VmiILkKckQ==", + "optional": true, + "peer": true, "dependencies": { "glob": "^7.0.0", "lodash": "^4.17.11", @@ -13822,6 +14050,8 @@ "version": "17.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", "integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==", + "optional": true, + "peer": true, "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -13839,6 +14069,8 @@ "version": "21.0.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -13926,6 +14158,8 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.3.0.tgz", "integrity": "sha512-14Zl9GcbBvOT9057ZKjpz5yPOyUWG2ojd9D5io28wHRYsOrs7U95Q+KNL87+32p8rc+LvDpbu/i9ZYjM9Q+FsQ==", + "optional": true, + "peer": true, "dependencies": { "js-base64": "^2.4.3", "source-map": "^0.7.1" @@ -13935,6 +14169,8 @@ "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "optional": true, + "peer": true, "engines": { "node": ">= 8" } @@ -14089,7 +14325,9 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true, + "peer": true }, "node_modules/setprototypeof": { "version": "1.2.0", @@ -14160,6 +14398,8 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "optional": true, + "peer": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -14187,6 +14427,8 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", + "optional": true, + "peer": true, "dependencies": { "ip": "^1.1.5", "smart-buffer": "^4.2.0" @@ -14200,6 +14442,8 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.0.tgz", "integrity": "sha512-wWqJhjb32Q6GsrUqzuFkukxb/zzide5quXYcMVpIjxalDBBYy2nqKCFQ/9+Ie4dvOYSQdOk3hUlZSdzZOd3zMQ==", + "optional": true, + "peer": true, "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", @@ -14278,6 +14522,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "optional": true, + "peer": true, "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -14286,12 +14532,16 @@ "node_modules/spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "optional": true, + "peer": true }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "optional": true, + "peer": true, "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -14300,7 +14550,9 @@ "node_modules/spdx-license-ids": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==" + "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", + "optional": true, + "peer": true }, "node_modules/spdy": { "version": "4.0.2", @@ -14339,6 +14591,8 @@ "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "optional": true, + "peer": true, "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -14363,6 +14617,8 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "peer": true, "dependencies": { "minipass": "^3.1.1" }, @@ -14411,6 +14667,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "optional": true, + "peer": true, "dependencies": { "readable-stream": "^2.0.1" } @@ -14418,12 +14676,16 @@ "node_modules/stdout-stream/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "optional": true, + "peer": true }, "node_modules/stdout-stream/node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "optional": true, + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -14438,6 +14700,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -14906,6 +15170,8 @@ "version": "6.1.11", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "optional": true, + "peer": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -15138,6 +15404,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -15146,6 +15414,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "optional": true, + "peer": true, "dependencies": { "glob": "^7.1.2" } @@ -15213,6 +15483,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "optional": true, + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -15223,7 +15495,9 @@ "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true, + "peer": true }, "node_modules/type-check": { "version": "0.4.0", @@ -15248,6 +15522,8 @@ "version": "0.18.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -15341,6 +15617,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "peer": true, "dependencies": { "unique-slug": "^2.0.0" } @@ -15349,6 +15627,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "peer": true, "dependencies": { "imurmurhash": "^0.1.4" } @@ -15439,6 +15719,8 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "optional": true, + "peer": true, "bin": { "uuid": "bin/uuid" } @@ -15473,6 +15755,8 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "optional": true, + "peer": true, "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -15498,6 +15782,8 @@ "engines": [ "node >=0.6.0" ], + "optional": true, + "peer": true, "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -15954,6 +16240,8 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "peer": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -17818,7 +18106,9 @@ "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==" + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true, + "peer": true }, "@humanwhocodes/config-array": { "version": "0.9.5", @@ -18224,6 +18514,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "peer": true, "requires": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" @@ -18233,6 +18525,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "optional": true, + "peer": true, "requires": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" @@ -18756,7 +19050,9 @@ "@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "optional": true, + "peer": true }, "@types/node": { "version": "16.11.27", @@ -18766,7 +19062,9 @@ "@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "optional": true, + "peer": true }, "@types/parse-json": { "version": "4.0.0", @@ -19203,7 +19501,9 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true, + "peer": true }, "accepts": { "version": "1.3.8", @@ -19295,6 +19595,8 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "peer": true, "requires": { "debug": "^4.1.0", "depd": "^1.1.2", @@ -19305,6 +19607,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "peer": true, "requires": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -19398,12 +19702,16 @@ "aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true, + "peer": true }, "are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "peer": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -19474,7 +19782,9 @@ "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "optional": true, + "peer": true }, "asap": { "version": "2.0.6", @@ -19485,6 +19795,8 @@ "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "optional": true, + "peer": true, "requires": { "safer-buffer": "~2.1.0" } @@ -19492,7 +19804,9 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "optional": true, + "peer": true }, "ast-types-flow": { "version": "0.0.7", @@ -19510,7 +19824,9 @@ "async-foreach": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "optional": true, + "peer": true }, "asynckit": { "version": "0.4.0", @@ -19548,12 +19864,16 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "optional": true, + "peer": true }, "aws4": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "optional": true, + "peer": true }, "axe-core": { "version": "4.4.1", @@ -19762,6 +20082,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "peer": true, "requires": { "tweetnacl": "^0.14.3" } @@ -19919,6 +20241,8 @@ "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "peer": true, "requires": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", @@ -19977,6 +20301,8 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "optional": true, + "peer": true, "requires": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", @@ -20007,7 +20333,9 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "optional": true, + "peer": true }, "chalk": { "version": "4.1.2", @@ -20061,7 +20389,9 @@ "chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "peer": true }, "chrome-trace-event": { "version": "1.0.3", @@ -20094,7 +20424,9 @@ "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "peer": true }, "cliui": { "version": "7.0.4", @@ -20198,7 +20530,9 @@ "color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "peer": true }, "colord": { "version": "2.9.2", @@ -20293,7 +20627,9 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true, + "peer": true }, "content-disposition": { "version": "0.5.4", @@ -20659,6 +20995,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "optional": true, + "peer": true, "requires": { "assert-plus": "^1.0.0" } @@ -20684,12 +21022,16 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "optional": true, + "peer": true }, "decamelize-keys": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "optional": true, + "peer": true, "requires": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" @@ -20698,7 +21040,9 @@ "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "optional": true, + "peer": true } } }, @@ -20762,7 +21106,9 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "optional": true, + "peer": true }, "depd": { "version": "1.1.2", @@ -20967,6 +21313,8 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "peer": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -21015,6 +21363,7 @@ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "optional": true, + "peer": true, "requires": { "iconv-lite": "^0.6.2" } @@ -21036,12 +21385,16 @@ "env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "peer": true }, "err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true, + "peer": true }, "error-ex": { "version": "1.3.2", @@ -21703,12 +22056,16 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true, + "peer": true }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "optional": true, + "peer": true }, "fast-deep-equal": { "version": "3.1.3", @@ -21887,7 +22244,9 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "optional": true, + "peer": true }, "fork-ts-checker-webpack-plugin": { "version": "6.5.1", @@ -21988,6 +22347,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "peer": true, "requires": { "minipass": "^3.0.0" } @@ -22027,6 +22388,8 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "peer": true, "requires": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", @@ -22043,6 +22406,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "optional": true, + "peer": true, "requires": { "globule": "^1.0.0" } @@ -22080,7 +22445,9 @@ "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "optional": true, + "peer": true }, "get-stream": { "version": "6.0.1", @@ -22100,6 +22467,8 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "optional": true, + "peer": true, "requires": { "assert-plus": "^1.0.0" } @@ -22180,6 +22549,8 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.3.tgz", "integrity": "sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg==", + "optional": true, + "peer": true, "requires": { "glob": "~7.1.1", "lodash": "~4.17.10", @@ -22190,6 +22561,8 @@ "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "optional": true, + "peer": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -22203,6 +22576,8 @@ "version": "3.0.8", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "optional": true, + "peer": true, "requires": { "brace-expansion": "^1.1.7" } @@ -22230,12 +22605,16 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "optional": true, + "peer": true }, "har-validator": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "optional": true, + "peer": true, "requires": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -22244,7 +22623,9 @@ "hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==" + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "optional": true, + "peer": true }, "harmony-reflect": { "version": "1.6.2", @@ -22293,7 +22674,9 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "optional": true, + "peer": true }, "he": { "version": "1.2.0", @@ -22337,6 +22720,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "optional": true, + "peer": true, "requires": { "lru-cache": "^6.0.0" } @@ -22447,7 +22832,9 @@ "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "optional": true, + "peer": true }, "http-deceiver": { "version": "1.2.7", @@ -22507,6 +22894,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "optional": true, + "peer": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -22531,6 +22920,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "optional": true, + "peer": true, "requires": { "ms": "^2.0.0" } @@ -22593,6 +22984,11 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz", "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==" }, + "immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -22631,7 +23027,9 @@ "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true, + "peer": true }, "inflight": { "version": "1.0.6", @@ -22673,7 +23071,9 @@ "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "optional": true, + "peer": true }, "ipaddr.js": { "version": "2.0.1", @@ -22767,7 +23167,9 @@ "is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=" + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "optional": true, + "peer": true }, "is-module": { "version": "1.0.0", @@ -22889,7 +23291,9 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "optional": true, + "peer": true }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -23461,7 +23865,9 @@ "js-base64": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", - "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "optional": true, + "peer": true }, "js-tokens": { "version": "4.0.0", @@ -23480,7 +23886,9 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true, + "peer": true }, "jsdom": { "version": "16.7.0", @@ -23549,7 +23957,9 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "optional": true, + "peer": true }, "json5": { "version": "2.2.1", @@ -23574,6 +23984,8 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "optional": true, + "peer": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -23841,6 +24253,8 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "peer": true, "requires": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", @@ -23871,7 +24285,9 @@ "map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==" + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "optional": true, + "peer": true }, "mdn-data": { "version": "2.0.4", @@ -23895,6 +24311,8 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "optional": true, + "peer": true, "requires": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", @@ -24043,6 +24461,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "optional": true, + "peer": true, "requires": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", @@ -24052,7 +24472,9 @@ "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "optional": true, + "peer": true } } }, @@ -24060,6 +24482,8 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "optional": true, + "peer": true, "requires": { "yallist": "^4.0.0" } @@ -24068,6 +24492,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "peer": true, "requires": { "minipass": "^3.0.0" } @@ -24076,6 +24502,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "peer": true, "requires": { "encoding": "^0.1.12", "minipass": "^3.1.0", @@ -24087,6 +24515,8 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "peer": true, "requires": { "minipass": "^3.0.0" } @@ -24095,6 +24525,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "peer": true, "requires": { "minipass": "^3.0.0" } @@ -24103,6 +24535,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "peer": true, "requires": { "minipass": "^3.0.0" } @@ -24111,6 +24545,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "peer": true, "requires": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -24119,7 +24555,9 @@ "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "peer": true }, "ms": { "version": "2.1.2", @@ -24138,7 +24576,9 @@ "nan": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "optional": true, + "peer": true }, "nanoid": { "version": "3.3.3", @@ -24178,6 +24618,8 @@ "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "peer": true, "requires": { "env-paths": "^2.2.0", "glob": "^7.1.4", @@ -24195,6 +24637,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz", "integrity": "sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw==", + "optional": true, + "peer": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -24204,6 +24648,8 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "peer": true, "requires": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", @@ -24219,6 +24665,8 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.1.tgz", "integrity": "sha512-BTHDvY6nrRHuRfyjt1MAufLxYdVXZfd099H4+i1f0lPywNQyI4foeNXJRObB/uy+TYqUW0vAD9gbdSOXPst7Eg==", + "optional": true, + "peer": true, "requires": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", @@ -24242,6 +24690,8 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-7.0.1.tgz", "integrity": "sha512-uMy+Xt29NlqKCFdFRZyXKOTqGt+QaKHexv9STj2WeLottnlqZEEWx6Bj0MXNthmFRRdM/YwyNo/8Tr46TOM0jQ==", + "optional": true, + "peer": true, "requires": { "async-foreach": "^0.1.3", "chalk": "^4.1.2", @@ -24264,6 +24714,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "peer": true, "requires": { "abbrev": "1" } @@ -24272,6 +24724,8 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "optional": true, + "peer": true, "requires": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", @@ -24306,6 +24760,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "peer": true, "requires": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", @@ -24329,7 +24785,9 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "optional": true, + "peer": true }, "object-assign": { "version": "4.1.1", @@ -24488,6 +24946,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "peer": true, "requires": { "aggregate-error": "^3.0.0" } @@ -25415,12 +25875,16 @@ "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "optional": true, + "peer": true }, "promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "peer": true, "requires": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -25486,7 +25950,9 @@ "qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "optional": true, + "peer": true }, "queue-microtask": { "version": "1.2.3", @@ -25496,7 +25962,9 @@ "quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==" + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "optional": true, + "peer": true }, "raf": { "version": "3.4.1", @@ -25795,6 +26263,8 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "optional": true, + "peer": true, "requires": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", @@ -25805,12 +26275,16 @@ "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "optional": true, + "peer": true }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "optional": true, + "peer": true, "requires": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -25821,12 +26295,16 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true, + "peer": true }, "type-fest": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "optional": true, + "peer": true } } }, @@ -25834,6 +26312,8 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "optional": true, + "peer": true, "requires": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", @@ -25844,6 +26324,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "optional": true, + "peer": true, "requires": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -25853,6 +26335,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "optional": true, + "peer": true, "requires": { "p-locate": "^4.1.0" } @@ -25861,6 +26345,8 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "optional": true, + "peer": true, "requires": { "p-try": "^2.0.0" } @@ -25869,6 +26355,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "optional": true, + "peer": true, "requires": { "p-limit": "^2.2.0" } @@ -25876,7 +26364,9 @@ "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "optional": true, + "peer": true } } }, @@ -26045,6 +26535,8 @@ "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "optional": true, + "peer": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -26072,6 +26564,8 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "optional": true, + "peer": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -26082,6 +26576,8 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "optional": true, + "peer": true, "requires": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -26173,7 +26669,9 @@ "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "optional": true, + "peer": true }, "reusify": { "version": "1.0.4", @@ -26250,10 +26748,22 @@ "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" }, + "sass": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.76.0.tgz", + "integrity": "sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==", + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, "sass-graph": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.0.tgz", "integrity": "sha512-WSO/MfXqKH7/TS8RdkCX3lVkPFQzCgbqdGsmSKq6tlPU+GpGEsa/5aW18JqItnqh+lPtcjifqdZ/VmiILkKckQ==", + "optional": true, + "peer": true, "requires": { "glob": "^7.0.0", "lodash": "^4.17.11", @@ -26265,6 +26775,8 @@ "version": "17.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", "integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==", + "optional": true, + "peer": true, "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -26278,7 +26790,9 @@ "yargs-parser": { "version": "21.0.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==" + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "optional": true, + "peer": true } } }, @@ -26327,6 +26841,8 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.3.0.tgz", "integrity": "sha512-14Zl9GcbBvOT9057ZKjpz5yPOyUWG2ojd9D5io28wHRYsOrs7U95Q+KNL87+32p8rc+LvDpbu/i9ZYjM9Q+FsQ==", + "optional": true, + "peer": true, "requires": { "js-base64": "^2.4.3", "source-map": "^0.7.1" @@ -26335,7 +26851,9 @@ "source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "optional": true, + "peer": true } } }, @@ -26474,7 +26992,9 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true, + "peer": true }, "setprototypeof": { "version": "1.2.0", @@ -26532,7 +27052,9 @@ "smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "optional": true, + "peer": true }, "sockjs": { "version": "0.3.24", @@ -26555,6 +27077,8 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", + "optional": true, + "peer": true, "requires": { "ip": "^1.1.5", "smart-buffer": "^4.2.0" @@ -26564,6 +27088,8 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.0.tgz", "integrity": "sha512-wWqJhjb32Q6GsrUqzuFkukxb/zzide5quXYcMVpIjxalDBBYy2nqKCFQ/9+Ie4dvOYSQdOk3hUlZSdzZOd3zMQ==", + "optional": true, + "peer": true, "requires": { "agent-base": "^6.0.2", "debug": "^4.3.3", @@ -26622,6 +27148,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "optional": true, + "peer": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -26630,12 +27158,16 @@ "spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "optional": true, + "peer": true }, "spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "optional": true, + "peer": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -26644,7 +27176,9 @@ "spdx-license-ids": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==" + "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", + "optional": true, + "peer": true }, "spdy": { "version": "4.0.2", @@ -26680,6 +27214,8 @@ "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "optional": true, + "peer": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -26696,6 +27232,8 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "peer": true, "requires": { "minipass": "^3.1.1" } @@ -26734,6 +27272,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "optional": true, + "peer": true, "requires": { "readable-stream": "^2.0.1" }, @@ -26741,12 +27281,16 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "optional": true, + "peer": true }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "optional": true, + "peer": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -26761,6 +27305,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "peer": true, "requires": { "safe-buffer": "~5.1.0" } @@ -27111,6 +27657,8 @@ "version": "6.1.11", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "optional": true, + "peer": true, "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -27273,12 +27821,16 @@ "trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==" + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "optional": true, + "peer": true }, "true-case-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "optional": true, + "peer": true, "requires": { "glob": "^7.1.2" } @@ -27338,6 +27890,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "optional": true, + "peer": true, "requires": { "safe-buffer": "^5.0.1" } @@ -27345,7 +27899,9 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true, + "peer": true }, "type-check": { "version": "0.4.0", @@ -27363,7 +27919,9 @@ "type-fest": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==" + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "optional": true, + "peer": true }, "type-is": { "version": "1.6.18", @@ -27426,6 +27984,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "peer": true, "requires": { "unique-slug": "^2.0.0" } @@ -27434,6 +27994,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "peer": true, "requires": { "imurmurhash": "^0.1.4" } @@ -27503,7 +28065,9 @@ "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "optional": true, + "peer": true }, "v8-compile-cache": { "version": "2.3.0", @@ -27531,6 +28095,8 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "optional": true, + "peer": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -27550,6 +28116,8 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "optional": true, + "peer": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -27884,6 +28452,8 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "peer": true, "requires": { "string-width": "^1.0.2 || 2 || 3 || 4" } diff --git a/front/package.json b/front/package.json index 3c0da2d..57e4411 100644 --- a/front/package.json +++ b/front/package.json @@ -2,7 +2,7 @@ "name": "holerr-front", "version": "0.1.0", "private": true, - "homepage": "%holerr-base-path-placeholder%", + "homepage": ".", "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/free-solid-svg-icons": "^5.15.4", @@ -28,7 +28,6 @@ "i18next": "^21.2.0", "i18next-browser-languagedetector": "^6.1.2", "luxon": "^2.0.2", - "node-sass": "^7.0.1", "pretty-bytes": "^5.6.0", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -39,10 +38,11 @@ "react-router-dom": "^5.3.0", "react-scripts": "^5.0.1", "redux-persist": "^6.0.0", + "sass": "^1.76.0", "typescript": "^4.6.3" }, "scripts": { - "start": "PUBLIC_URL=. react-scripts start", + "start": "react-scripts start", "build": "react-scripts build", "build:docker": "INLINE_RUNTIME_CHUNK=false react-scripts build", "postbuild:docker": "rm -rf ../public && mv build ../public", @@ -61,4 +61,4 @@ "last 1 safari version" ] } -} +} \ No newline at end of file diff --git a/front/public/browserconfig.xml b/front/public/browserconfig.xml index 3fbdb51..f2429ad 100644 --- a/front/public/browserconfig.xml +++ b/front/public/browserconfig.xml @@ -2,7 +2,7 @@ - + #da532c diff --git a/front/public/index.html b/front/public/index.html index 13e70b3..a4c74d1 100644 --- a/front/public/index.html +++ b/front/public/index.html @@ -1,19 +1,46 @@ + - - - - - - - + + + + + + + Holerr - + diff --git a/front/public/site.webmanifest b/front/public/site.webmanifest index 2e48366..84cd8c9 100644 --- a/front/public/site.webmanifest +++ b/front/public/site.webmanifest @@ -3,12 +3,12 @@ "short_name": "Holerr", "icons": [ { - "src": "%holerr-base-path-placeholder%/android-chrome-192x192.png", + "src": "./android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "%holerr-base-path-placeholder%/android-chrome-512x512.png", + "src": "./android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } @@ -16,4 +16,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} +} \ No newline at end of file diff --git a/front/src/Main.tsx b/front/src/Main.tsx index af1884f..6252fcd 100644 --- a/front/src/Main.tsx +++ b/front/src/Main.tsx @@ -11,7 +11,7 @@ import { Download } from "./models/downloads.type"; import webSocket from "./api/websocket"; import useSocketMessage from "./hooks/useSocketMessage"; import AppBottomBar from "./layouts/AppBottomBar"; -import {Downloads, Presets, Settings, Status} from "./pages"; +import { Downloads, Presets, Settings, Status } from "./pages"; import { addDownload, @@ -54,7 +54,7 @@ const Main = () => { return (
- + diff --git a/front/src/api/http.ts b/front/src/api/http.ts index 9ab5ba4..69cfdac 100644 --- a/front/src/api/http.ts +++ b/front/src/api/http.ts @@ -1,24 +1,14 @@ import axios from "axios"; -import store from "../store"; -let baseURL = process.env.PUBLIC_URL; +let baseURL = (window as any).base_path; if (baseURL === "/") { - baseURL = ""; + baseURL = ""; } baseURL += "/api"; const httpApi = axios.create({ - baseURL, - headers: {'Accept': 'application/json'} + baseURL, + headers: { Accept: "application/json" }, }); -httpApi.interceptors.request.use(config => { - const apiKey = store.getState().appConfig.apiKey; - if (apiKey && apiKey.length && !apiKey.startsWith("%holerr-api-key-")) { - config.headers = config.headers || {}; - config.headers['X-Api-Key'] = apiKey; - } - return config; -}, (error) => error); - export default httpApi; diff --git a/front/src/api/websocket.ts b/front/src/api/websocket.ts index 84c4786..1ae9ef8 100644 --- a/front/src/api/websocket.ts +++ b/front/src/api/websocket.ts @@ -1,82 +1,78 @@ import store from "../store"; class Socket { - path: string; - ws?: WebSocket; - onMessageStack: Record any)[]>; - callStack: string[]; + path: string; + ws?: WebSocket; + onMessageStack: Record any)[]>; + callStack: string[]; - constructor(path: string) { - this.path = path; - this.callStack = []; - this.onMessageStack = {}; - } + constructor(path: string) { + this.path = path; + this.callStack = []; + this.onMessageStack = {}; + } - isConnected() { - return this.ws && this.ws.readyState === 1; - } + isConnected() { + return this.ws && this.ws.readyState === 1; + } - connect() { - let url = window.location.origin.replace("http", "ws") + this.path; - const apiKey = store.getState().appConfig.apiKey; - if (apiKey && apiKey.length && !apiKey.startsWith("%holerr-api-key-")) { - url += `?x-api-key=${apiKey}` - } - this.ws = new WebSocket(url); + connect() { + let url = window.location.origin.replace("http", "ws") + this.path; + this.ws = new WebSocket(url); - this.ws.onopen = () => { - while (this.callStack.length) { - this.ws && this.ws.send(this.callStack[0]); - this.callStack.shift(); - } - }; + this.ws.onopen = () => { + while (this.callStack.length) { + this.ws && this.ws.send(this.callStack[0]); + this.callStack.shift(); + } + }; - this.ws.onmessage = (evt: MessageEvent) => { - const data = JSON.parse(evt.data) as { action: string; payload: any }; - if (data.action && this.onMessageStack[data.action]) { - this.onMessageStack[data.action].forEach((c) => c(data.payload)); - } - }; + this.ws.onmessage = (evt: MessageEvent) => { + const data = JSON.parse(evt.data) as { action: string; payload: any }; + if (data.action && this.onMessageStack[data.action]) { + this.onMessageStack[data.action].forEach((c) => c(data.payload)); + } + }; - this.ws.onclose = () => { - setTimeout(() => this.connect(), 500); - }; + this.ws.onclose = () => { + setTimeout(() => this.connect(), 500); + }; - this.ws.onerror = () => { - this.ws && this.ws.close(); - }; - } + this.ws.onerror = () => { + this.ws && this.ws.close(); + }; + } - subscribe(action: string, callback: (data: T) => any) { - this.on(action, callback); - return () => this.off(action, callback); - } + subscribe(action: string, callback: (data: T) => any) { + this.on(action, callback); + return () => this.off(action, callback); + } - on(action: string, callback: (data: T) => any) { - if (!this.onMessageStack[action]) { - this.onMessageStack[action] = []; - } - this.onMessageStack[action].push(callback); + on(action: string, callback: (data: T) => any) { + if (!this.onMessageStack[action]) { + this.onMessageStack[action] = []; } + this.onMessageStack[action].push(callback); + } - off(action: string, callback: (data: T) => any) { - const index = this.onMessageStack[action].indexOf(callback); - this.onMessageStack[action].splice(index, 1); - } + off(action: string, callback: (data: T) => any) { + const index = this.onMessageStack[action].indexOf(callback); + this.onMessageStack[action].splice(index, 1); + } - send(data: any) { - const body = JSON.stringify(data); - if (!this.isConnected()) { - this.callStack.push(body); - } else { - this.ws && this.ws.send(body); - } + send(data: any) { + const body = JSON.stringify(data); + if (!this.isConnected()) { + this.callStack.push(body); + } else { + this.ws && this.ws.send(body); } + } } -let baseURL = process.env.PUBLIC_URL; +let baseURL = (window as any).base_path; if (baseURL === "/") { - baseURL = ""; + baseURL = ""; } baseURL += "/api"; const webSocket = new Socket(`${baseURL}/ws`); diff --git a/front/src/components/DropZone.module.scss b/front/src/components/DropZone.module.scss index 9bda2b6..70f08be 100644 --- a/front/src/components/DropZone.module.scss +++ b/front/src/components/DropZone.module.scss @@ -1,4 +1,4 @@ -@import "src/theme"; +@use "theme" as *; .root { height: 200px; diff --git a/front/src/components/StateProgress.module.scss b/front/src/components/StateProgress.module.scss index 3906e5a..a460b17 100644 --- a/front/src/components/StateProgress.module.scss +++ b/front/src/components/StateProgress.module.scss @@ -1,4 +1,4 @@ -@import "src/theme"; +@use "theme"; .root { width: 32px; @@ -11,18 +11,18 @@ .torrent, .download { - color: $grey-300 !important; + color: theme.$grey-300 !important; &.progress { - color: $blue-500 !important; + color: theme.$blue-500 !important; } &.done { - color: $green-200 !important; + color: theme.$green-200 !important; } &.error { - color: $red-500 !important; + color: theme.$red-500 !important; } } @@ -44,7 +44,7 @@ .torrentBottom, .downloadBottom { - color: $grey-200 !important; + color: theme.$grey-200 !important; } .icon { @@ -53,17 +53,17 @@ position: absolute; top: 11px; left: 11px; - color: $grey-400 !important; + color: theme.$grey-400 !important; &.done { - color: $green-200 !important; + color: theme.$green-200 !important; } &.error { - color: $red-500 !important; + color: theme.$red-500 !important; } &.animated { - @include flicker; + @include theme.flicker; } } diff --git a/front/src/components/StateProgress.tsx b/front/src/components/StateProgress.tsx index 510ef88..a27b07d 100644 --- a/front/src/components/StateProgress.tsx +++ b/front/src/components/StateProgress.tsx @@ -1,60 +1,79 @@ import classes from "./StateProgress.module.scss"; -import {CircularProgress} from "@material-ui/core"; +import { CircularProgress } from "@material-ui/core"; import classnames from "classnames"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faCheck, faClock, faCloudUploadAlt, faExclamation, faFileDownload} from "@fortawesome/free-solid-svg-icons"; -import {DownloadStep, DownloadStepStatus} from "../models/downloads.utils"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faCheck, + faClock, + faCloudUploadAlt, + faExclamation, + faFileDownload, +} from "@fortawesome/free-solid-svg-icons"; +import { DownloadStep, DownloadStepStatus } from "../models/downloads.utils"; type StateProgressProps = { - status?: DownloadStepStatus; - step?: DownloadStep; - torrentProgress: number, - downloadProgress: number + status?: DownloadStepStatus; + step?: DownloadStep; + progress: number; }; -const StateProgress: React.FC = ({status, step, torrentProgress, downloadProgress}) => { - let icon = faClock; - let animateIcon = true; - let iconColor = undefined; - let torrentClass = classes.progress; - let downloadClass = classes.progress; +const StateProgress: React.FC = ({ + status, + step, + progress, +}) => { + let icon = faClock; + let animateIcon = true; + let iconColor = undefined; + let torrentClass = classes.progress; + let downloadClass = classes.progress; - if (status === DownloadStepStatus.FAILURE) { - icon = faExclamation; - iconColor = classes.error; - if (step === DownloadStep.DEBRIDER) { - torrentClass = classes.error; - } else if (step === DownloadStep.DOWNLOADER) { - torrentClass = classes.done; - downloadClass = classes.error; - } - } else if (status === DownloadStepStatus.SUCCESS) { - icon = faCheck; - animateIcon = false; - iconColor = classes.done; - torrentClass = classes.done; - downloadClass = classes.done; - } else if (step === DownloadStep.DEBRIDER) { - torrentClass = classes.progress; - icon = faCloudUploadAlt; + if (status === DownloadStepStatus.FAILURE) { + icon = faExclamation; + iconColor = classes.error; + if (step === DownloadStep.DEBRIDER) { + torrentClass = classes.error; } else if (step === DownloadStep.DOWNLOADER) { - torrentClass = classes.done; - downloadClass = classes.progress; - icon = faFileDownload; + torrentClass = classes.done; + downloadClass = classes.error; } + } else if (status === DownloadStepStatus.SUCCESS) { + icon = faCheck; + animateIcon = false; + iconColor = classes.done; + torrentClass = classes.done; + downloadClass = classes.done; + } else if (step === DownloadStep.DEBRIDER) { + torrentClass = classes.progress; + icon = faCloudUploadAlt; + } else if (step === DownloadStep.DOWNLOADER) { + torrentClass = classes.done; + downloadClass = classes.progress; + icon = faFileDownload; + } - return
- - - - - -
; + return ( +
+ + + +
+ ); }; -export default StateProgress; \ No newline at end of file +export default StateProgress; diff --git a/front/src/components/StatusDot.module.scss b/front/src/components/StatusDot.module.scss index 8bf3203..674645a 100644 --- a/front/src/components/StatusDot.module.scss +++ b/front/src/components/StatusDot.module.scss @@ -1,22 +1,22 @@ -@import "src/theme"; +@use "theme"; .root { display: inline-block; width: 12px; height: 12px; border-radius: 6px; - background: $grey-500; + background: theme.$grey-500; transition: background-color 250ms ease-in-out; } .success { - background-color: $green-400; + background-color: theme.$green-400; } .error { - background-color: $red-400; + background-color: theme.$red-400; } .pending { - @include pending; + @include theme.pending; } diff --git a/front/src/i18n/en.json b/front/src/i18n/en.json index fb8d9bf..918c7df 100644 --- a/front/src/i18n/en.json +++ b/front/src/i18n/en.json @@ -13,6 +13,8 @@ "downloads_refresh": "Refresh", "downloads_clear_all": "Clear all", "downloads_delete": "Delete", + "downloads_preset": "Preset", + "downloads_magnets": "Magnet links", "downloads_add_torrents": "Add torrents", "downloads_add_torrents_title": "Add torrents", "downloads_add_torrents_action": "Download", @@ -50,7 +52,6 @@ "configuration_subtitle": "Configuration", "configuration_debug": "Debug", "configuration_base_path": "Base path", - "configuration_api_key": "API key", "retrieve_real_debrid_token": "Retrieve your API key here:", "real_debrid_website": "Real-debrid website", "configuration_endpoint": "Endpoint", diff --git a/front/src/i18n/fr.json b/front/src/i18n/fr.json index 3ec70cc..982e843 100644 --- a/front/src/i18n/fr.json +++ b/front/src/i18n/fr.json @@ -13,6 +13,7 @@ "downloads_clear_all": "Nettoyer complétés", "downloads_delete": "Supprimer", "downloads_preset": "Préréglage", + "downloads_magnets": "Liens Magnet", "downloads_add_torrents": "Ajouter des torrents", "downloads_add_torrents_title": "Ajouter des torrents", "downloads_add_torrents_action": "Télécharger", @@ -50,7 +51,6 @@ "configuration_subtitle": "Configuration", "configuration_debug": "Debug", "configuration_base_path": "Chemin de base", - "configuration_api_key": "Clé API", "retrieve_real_debrid_token": "Retrouvez votre clé API ici :", "real_debrid_website": "site de Real-debrid", "configuration_endpoint": "Endpoint", diff --git a/front/src/models/configuration.type.ts b/front/src/models/configuration.type.ts index c22a7ce..67181b7 100644 --- a/front/src/models/configuration.type.ts +++ b/front/src/models/configuration.type.ts @@ -1,38 +1,32 @@ import { Preset } from "./presets.type"; export type Configuration = { - // API key used to communicate with the server [default: ""] - api_key: string; - // Set holerr in debug (default: false) - debug: boolean; - // Current app version - app_version: string; - // Wether holerr is running using docker or not - is_in_docker: boolean; - // If necessary, the base_path to fetch the front [optional, default: "/"] example: "/holerr" - base_path: string; - // Debriders, providers that will download the torrent - debriders?: Debriders; - // Downloaders, providers that will download the files downloaded by the debrider - downloaders?: Downloaders; - // Download presets - presets?: Preset[]; + // Current app version + app_version: string; + // If necessary, the base_path to fetch the front [optional, default: "/"] example: "/holerr" + base_path: string; + // Debriders, providers that will download the torrent + debrider?: Debrider; + // Downloaders, providers that will download the files downloaded by the debrider + downloader?: Downloader; + // Download presets + presets?: Preset[]; }; -export type Debriders = { - real_debrid: RealDebrid -} +export type Debrider = { + real_debrid: RealDebrid; +}; export type RealDebrid = { - api_key: string -} + api_key: string; +}; -export type Downloaders = { - synology_download_station: SynologyDownloadStation +export type Downloader = { + synology_download_station: SynologyDownloadStation; }; export type SynologyDownloadStation = { - endpoint: string, - username: string, - password: string -}; \ No newline at end of file + endpoint: string; + username: string; + password: string; +}; diff --git a/front/src/models/downloads.type.ts b/front/src/models/downloads.type.ts index 8cbaa80..5429297 100644 --- a/front/src/models/downloads.type.ts +++ b/front/src/models/downloads.type.ts @@ -1,64 +1,27 @@ export enum DownloadStatus { TORRENT_FOUND = 0, - TORRENT_SENT_TO_DEBRIDER = 1, - DEBRIDER_DOWNLOADING = 2, - DEBRIDER_DOWNLOADED = 3, - SENT_TO_DOWNLOADER = 4, - DOWNLOADER_DOWNLOADING = 5, - DOWNLOADER_DOWNLOADED = 6, + TORRENT_SENT_TO_DEBRIDER = 10, + DEBRIDER_DOWNLOADING = 11, + DEBRIDER_POST_DOWNLOAD = 12, + DEBRIDER_DOWNLOADED = 13, + SENT_TO_DOWNLOADER = 20, + DOWNLOADER_DOWNLOADING = 21, + DOWNLOADER_DOWNLOADED = 22, + DOWNLOADED = 30, ERROR_NO_FILES_FOUND = 100, ERROR_DEBRIDER = 101, - ERROR_DOWNLOADER= 102, - STOPPED = 200, + ERROR_DOWNLOADER = 102, + ERROR_DELETED_ON_DEBRIDER = 103, } export type Download = { - // Internal download id id: string; - // Download title (computed from torrent file name) title: string; - // Name of the preset used preset: string; - // Global downlaod task status status: DownloadStatus; - // Human readable status - status_details: string; - // Debrider torrent info (might have more data, according to the debrider used) - torrent_info: TorrentInfo | null; - // Downloader downloads info - download_info: DownloadInfo | null; - // Download task creation date + total_bytes: number; + total_progress: number; + to_delete: boolean; created_at: string; - // Download task last update date updated_at: string; }; - -export type TorrentInfo = { - id: string; - filename: string; - bytes: number; - progress: number; - status: string; - files: DownloadFile[] | null; - links: string[] | null; -}; - -export type DownloadFile = { - id: number; - path: string; - bytes: number; - selected: number; -}; - -export type DownloadInfo = { - progress: number; - // Key are links, values are downloader id - tasks: Record -}; - -export type DownloadInfoTask = { - id: string; - status: number; - bytes: number; - bytes_downloaded: number; -}; \ No newline at end of file diff --git a/front/src/models/downloads.utils.ts b/front/src/models/downloads.utils.ts index f45d048..bc297c8 100644 --- a/front/src/models/downloads.utils.ts +++ b/front/src/models/downloads.utils.ts @@ -1,103 +1,119 @@ -import {DownloadStatus} from "./downloads.type"; +import { DownloadStatus } from "./downloads.type"; export enum DownloadStepStatus { - WAITING = "waiting", - PROGRESS = "progress", - SUCCESS = "success", - FAILURE = "failure" -}; + WAITING = "waiting", + PROGRESS = "progress", + SUCCESS = "success", + FAILURE = "failure", +} export enum DownloadStep { - UNKNOWN, - DEBRIDER, - DOWNLOADER -}; + UNKNOWN, + DEBRIDER, + DOWNLOADER, + GENERAL, +} type DownloadConfig = { - status: DownloadStepStatus; - step: DownloadStep; + status: DownloadStepStatus; + step: DownloadStep; }; export const DEFAULT_STATUS_CONFIG: DownloadConfig = { - status: DownloadStepStatus.WAITING, - step: DownloadStep.UNKNOWN, + status: DownloadStepStatus.WAITING, + step: DownloadStep.UNKNOWN, }; -export const downloadStatusConfig: Map = new Map([ +export const downloadStatusConfig: Map = + new Map([ + [ + DownloadStatus.TORRENT_FOUND, + { + status: DownloadStepStatus.PROGRESS, + step: DownloadStep.GENERAL, + }, + ], + [ + DownloadStatus.TORRENT_SENT_TO_DEBRIDER, + { + status: DownloadStepStatus.PROGRESS, + step: DownloadStep.DEBRIDER, + }, + ], [ - DownloadStatus.TORRENT_FOUND, - { - status: DownloadStepStatus.PROGRESS, - step: DownloadStep.DEBRIDER - }, + DownloadStatus.DEBRIDER_DOWNLOADING, + { + status: DownloadStepStatus.PROGRESS, + step: DownloadStep.DEBRIDER, + }, ], [ - DownloadStatus.TORRENT_SENT_TO_DEBRIDER, - { - status: DownloadStepStatus.PROGRESS, - step: DownloadStep.DEBRIDER - }, + DownloadStatus.DEBRIDER_POST_DOWNLOAD, + { + status: DownloadStepStatus.PROGRESS, + step: DownloadStep.DEBRIDER, + }, ], [ - DownloadStatus.DEBRIDER_DOWNLOADING, - { - status: DownloadStepStatus.PROGRESS, - step: DownloadStep.DEBRIDER - }, + DownloadStatus.DEBRIDER_DOWNLOADED, + { + status: DownloadStepStatus.PROGRESS, + step: DownloadStep.DEBRIDER, + }, ], [ - DownloadStatus.DEBRIDER_DOWNLOADED, - { - status: DownloadStepStatus.PROGRESS, - step: DownloadStep.DEBRIDER - }, + DownloadStatus.SENT_TO_DOWNLOADER, + { + status: DownloadStepStatus.PROGRESS, + step: DownloadStep.DOWNLOADER, + }, ], [ - DownloadStatus.SENT_TO_DOWNLOADER, - { - status: DownloadStepStatus.PROGRESS, - step: DownloadStep.DOWNLOADER - }, + DownloadStatus.DOWNLOADER_DOWNLOADING, + { + status: DownloadStepStatus.PROGRESS, + step: DownloadStep.DOWNLOADER, + }, ], [ - DownloadStatus.DOWNLOADER_DOWNLOADING, - { - status: DownloadStepStatus.PROGRESS, - step: DownloadStep.DOWNLOADER - }, + DownloadStatus.DOWNLOADER_DOWNLOADED, + { + status: DownloadStepStatus.SUCCESS, + step: DownloadStep.DOWNLOADER, + }, ], [ - DownloadStatus.DOWNLOADER_DOWNLOADED, - { - status: DownloadStepStatus.SUCCESS, - step: DownloadStep.DOWNLOADER - }, + DownloadStatus.DOWNLOADED, + { + status: DownloadStepStatus.SUCCESS, + step: DownloadStep.GENERAL, + }, ], [ - DownloadStatus.ERROR_NO_FILES_FOUND, - { - status: DownloadStepStatus.FAILURE, - step: DownloadStep.DEBRIDER - }, + DownloadStatus.ERROR_NO_FILES_FOUND, + { + status: DownloadStepStatus.FAILURE, + step: DownloadStep.DEBRIDER, + }, ], [ - DownloadStatus.ERROR_DEBRIDER, - { - status: DownloadStepStatus.FAILURE, - step: DownloadStep.DEBRIDER - }, + DownloadStatus.ERROR_DEBRIDER, + { + status: DownloadStepStatus.FAILURE, + step: DownloadStep.DEBRIDER, + }, ], [ - DownloadStatus.ERROR_DOWNLOADER, - { - status: DownloadStepStatus.FAILURE, - step: DownloadStep.DOWNLOADER - }, + DownloadStatus.ERROR_DOWNLOADER, + { + status: DownloadStepStatus.FAILURE, + step: DownloadStep.DOWNLOADER, + }, ], [ - DownloadStatus.STOPPED, - { - status: DownloadStepStatus.FAILURE, - step: DownloadStep.UNKNOWN - }, + DownloadStatus.ERROR_DELETED_ON_DEBRIDER, + { + status: DownloadStepStatus.FAILURE, + step: DownloadStep.DEBRIDER, + }, ], -]); + ]); diff --git a/front/src/models/presets.type.ts b/front/src/models/presets.type.ts index 285bc0f..b811ad0 100644 --- a/front/src/models/presets.type.ts +++ b/front/src/models/presets.type.ts @@ -8,7 +8,7 @@ export type Preset = { // Accepted file extensions [optional] file_extensions: string[] | null; // Minimum file size to download [optional, default: 0] - min_file_size: number; + min_file_size: string | null; // Whether downloader should download in subdir create_sub_dir?: boolean; }; diff --git a/front/src/models/status.type.ts b/front/src/models/status.type.ts index 53468b6..801a2c6 100644 --- a/front/src/models/status.type.ts +++ b/front/src/models/status.type.ts @@ -1,4 +1,15 @@ export type Status = { - debrider_connected: boolean, - downloader_connected: boolean -}; \ No newline at end of file + app: { + version: string; + }; + debrider: { + id: string; + name: string; + connected: boolean; + }; + downloader: { + id: string; + name: string; + connected: boolean; + }; +}; diff --git a/front/src/pages/Downloads/AddDownloadsDialog.tsx b/front/src/pages/Downloads/AddDownloadsDialog.tsx index 573d0cf..89cdeef 100644 --- a/front/src/pages/Downloads/AddDownloadsDialog.tsx +++ b/front/src/pages/Downloads/AddDownloadsDialog.tsx @@ -17,6 +17,7 @@ import { Typography, useMediaQuery, useTheme, + TextField, } from "@material-ui/core"; import { TransitionProps } from "@material-ui/core/transitions/transition"; import ClearIcon from "@material-ui/icons/Clear"; @@ -27,7 +28,7 @@ import { FC, forwardRef, useEffect, useState } from "react"; import { useDropzone } from "react-dropzone"; import { useTranslation } from "react-i18next"; import httpApi from "../../api/http"; -import {DropZone, FilesInput} from "../../components"; +import { DropZone, FilesInput } from "../../components"; import { useAppSelector } from "../../store"; import { presetsSelector } from "../../store/presets/presets.selectors"; @@ -86,9 +87,9 @@ const uploadTorrent = async (file: File, preset: string) => { if (file && preset) { const body = new FormData(); body.append("preset", preset); - body.append("torrent_file", file, file.name); + body.append("file", file, file.name); - await httpApi.post("/downloads", body, { + await httpApi.post("/actions/add_torrent", body, { headers: { "Content-Type": "multipart/form-data", }, @@ -96,6 +97,12 @@ const uploadTorrent = async (file: File, preset: string) => { } }; +const addMagnet = async (magnet: string, preset: string) => { + if (magnet && preset) { + await httpApi.post("/actions/add_magnet", { uri: magnet, preset }); + } +}; + type AddDownloadsDialogProps = { open: boolean; onClose: () => any; @@ -134,6 +141,9 @@ const AddDownloadsDialog: FC = ({ open, onClose }) => { setPresetName(allPresets[0]?.name); }, [allPresets]); + // Magnets + const [magnets, setMagnets] = useState(); + // Drop actions const { getInputProps, getRootProps, isDragActive } = useDropzone({ onDrop: addFiles, @@ -143,7 +153,8 @@ const AddDownloadsDialog: FC = ({ open, onClose }) => { }); // Submit - const isValid = presetName && files.length > 0; + const isValid = + presetName && (files.length > 0 || (magnets?.split("\n").length || 0) > 0); const [isLoading, setLoading] = useState(false); const handleSubmit = async (event: React.FormEvent) => { @@ -152,9 +163,13 @@ const AddDownloadsDialog: FC = ({ open, onClose }) => { for (let f of files) { await uploadTorrent(f, presetName as string); } + for (let magnet of magnets?.split("\n") || []) { + await addMagnet(magnet, presetName as string); + } onClose(); setFiles([]); setLoading(false); + setMagnets(""); }; return ( @@ -213,6 +228,15 @@ const AddDownloadsDialog: FC = ({ open, onClose }) => { {t("downloads_add_torrents_browse")} + + setMagnets(e.currentTarget.value as string)} + label={t("downloads_magnets")} + > + {files.length > 0 && ( {files.map((f, index) => ( @@ -229,7 +253,9 @@ const AddDownloadsDialog: FC = ({ open, onClose }) => { variant="contained" disabled={!isValid || isLoading} > - {t("downloads_add_torrents_action", { count: files.length })} + {t("downloads_add_torrents_action", { + count: files.length + (magnets?.split("\n").length || 0), + })}
diff --git a/front/src/pages/Downloads/DownloadItem.tsx b/front/src/pages/Downloads/DownloadItem.tsx index a9b4a62..90ee6d1 100644 --- a/front/src/pages/Downloads/DownloadItem.tsx +++ b/front/src/pages/Downloads/DownloadItem.tsx @@ -1,61 +1,75 @@ -import {Card, CardContent, Grid, IconButton, makeStyles,} from "@material-ui/core"; +import { + Card, + CardContent, + Grid, + IconButton, + makeStyles, +} from "@material-ui/core"; import DeleteIcon from "@material-ui/icons/DeleteOutlined"; -import {useDispatch} from "react-redux"; -import {Download} from "../../models/downloads.type"; -import {DEFAULT_STATUS_CONFIG, downloadStatusConfig} from "../../models/downloads.utils"; -import {deleteDownload} from "../../store/downloads/downloads.thunk"; +import { useDispatch } from "react-redux"; +import { Download } from "../../models/downloads.type"; +import { + DEFAULT_STATUS_CONFIG, + downloadStatusConfig, +} from "../../models/downloads.utils"; +import { deleteDownload } from "../../store/downloads/downloads.thunk"; import DownloadItemChips from "./DownloadItemChips"; -import {StateProgress} from "../../components"; -import {blue} from "@material-ui/core/colors"; -import {FunctionComponent} from "react"; +import { StateProgress } from "../../components"; +import { blue } from "@material-ui/core/colors"; +import { FunctionComponent } from "react"; type DownloadItemProps = { - item: Download; + item: Download; }; -const useStyles = makeStyles(({palette, spacing}) => ({ - content: { - padding: `${spacing(2)}px !important` - }, - status: { - paddingRight: spacing(2) - }, - title: { - flex: 1, - wordBreak: "break-all", - color: palette.type === "light" ? blue[900] :blue[300] - }, +const useStyles = makeStyles(({ palette, spacing }) => ({ + content: { + padding: `${spacing(2)}px !important`, + }, + status: { + paddingRight: spacing(2), + }, + title: { + flex: 1, + wordBreak: "break-all", + color: palette.type === "light" ? blue[900] : blue[300], + }, })); -const DownloadItem: FunctionComponent = ({item}) => { - const {status, step} = downloadStatusConfig.get(item.status) || DEFAULT_STATUS_CONFIG; - const dispatch = useDispatch(); - const classes = useStyles(); +const DownloadItem: FunctionComponent = ({ item }) => { + const { status, step } = + downloadStatusConfig.get(item.status) || DEFAULT_STATUS_CONFIG; + const dispatch = useDispatch(); + const classes = useStyles(); - const handleDeleteClick = () => { - dispatch(deleteDownload(item.id)); - }; + const handleDeleteClick = () => { + dispatch(deleteDownload(item.id)); + }; - const torrentProgress = item.torrent_info?.progress || 0; - const downloadProgress = item.download_info?.progress || 0; - - return - - - - - - - {item.title} - - - - - - - - ; + return ( + + + + + + + + {item.title} + + + + + + + + + + + ); }; export default DownloadItem; diff --git a/front/src/pages/Downloads/DownloadItemChips.tsx b/front/src/pages/Downloads/DownloadItemChips.tsx index e2d3096..2958ed9 100644 --- a/front/src/pages/Downloads/DownloadItemChips.tsx +++ b/front/src/pages/Downloads/DownloadItemChips.tsx @@ -1,95 +1,99 @@ -import {Chip, Grid, makeStyles} from "@material-ui/core"; -import React, {FC, useEffect, useMemo, useState} from "react"; -import {Download, TorrentInfo} from "../../models/downloads.type"; -import {DateTime} from "luxon"; +import { Chip, Grid, makeStyles } from "@material-ui/core"; +import React, { FC, useEffect, useState } from "react"; +import { Download } from "../../models/downloads.type"; +import { DateTime } from "luxon"; import TimeIcon from "@material-ui/icons/AccessTimeOutlined"; import SettingsIcon from "@material-ui/icons/SettingsOutlined"; import prettyBytes from "pretty-bytes"; -import {useTranslation} from "react-i18next"; -import {grey} from "@material-ui/core/colors"; +import { useTranslation } from "react-i18next"; +import { grey } from "@material-ui/core/colors"; const useStyles = makeStyles((theme) => ({ - labels: { - marginTop: 4, - overflowY: "auto", - "&::-webkit-scrollbar": { - display: "none", - }, - "& .MuiGrid-root": { - marginLeft: 4, - }, - "& .MuiChip-root": { - color: grey[400], - borderColor: grey[400], - fontFamily: "monospace" - }, - "& .MuiChip-icon": { - fill: grey[400] - } + labels: { + marginTop: 4, + overflowY: "auto", + "&::-webkit-scrollbar": { + display: "none", }, + "& .MuiGrid-root": { + marginLeft: 4, + }, + "& .MuiChip-root": { + color: grey[400], + borderColor: grey[400], + fontFamily: "monospace", + }, + "& .MuiChip-icon": { + fill: grey[400], + }, + }, })); type DownloadItemProps = { - item: Download; -}; -const DownloadItemChips: FC = ({item}) => { - const classes = useStyles(); - - return - } - label={item.preset} - variant="outlined" - /> - - {item.torrent_info && } - ; + item: Download; }; +const DownloadItemChips: FC = ({ item }) => { + const classes = useStyles(); -type SizeChipProps = { torrentInfo: TorrentInfo }; -const SizeChip = React.memo(({torrentInfo}) => { - const size = useMemo(() => { - return (torrentInfo.files || []) - .filter((file) => file.selected) - .reduce((acc, file) => acc + file.bytes, 0); - }, [torrentInfo]); - - return size ? ( + return ( + + } - label={prettyBytes(size)} - variant="outlined" + size="small" + icon={} + label={item.preset} + variant="outlined" /> - ) : null; + + + + + {item.total_bytes ? ( + + + + ) : null} + + ); +}; + +type SizeChipProps = { size: number }; +const SizeChip = React.memo(({ size }) => { + return size ? ( + } + label={prettyBytes(size)} + variant="outlined" + /> + ) : null; }); const computeSince = (dateTime: DateTime, language: string = "en"): string => - dateTime.toRelative({locale: language}) as string; + dateTime.toRelative({ locale: language }) as string; type TimeChipProps = { date: string }; -const TimeChip = React.memo(({date}) => { - const dateTime = DateTime.fromISO(date); +const TimeChip = React.memo(({ date }) => { + const dateTime = DateTime.fromISO(date); - const {i18n} = useTranslation(); + const { i18n } = useTranslation(); - const [since, setSince] = useState( - computeSince(dateTime, i18n.language) - ); + const [since, setSince] = useState( + computeSince(dateTime, i18n.language) + ); - useEffect(() => { - const interval = setInterval(() => { - const newSince = computeSince(dateTime, i18n.language); - if (newSince !== since) setSince(newSince); - }, 1000); - return () => { - clearInterval(interval); - }; - }, [dateTime]); + useEffect(() => { + const interval = setInterval(() => { + const newSince = computeSince(dateTime, i18n.language); + if (newSince !== since) setSince(newSince); + }, 1000); + return () => { + clearInterval(interval); + }; + }, [dateTime]); - return ( - } label={since} variant="outlined"/> - ); + return ( + } label={since} variant="outlined" /> + ); }); export default DownloadItemChips; diff --git a/front/src/pages/Presets/Preset.module.scss b/front/src/pages/Presets/Preset.module.scss index 11998f0..a457c4b 100644 --- a/front/src/pages/Presets/Preset.module.scss +++ b/front/src/pages/Presets/Preset.module.scss @@ -1,14 +1,14 @@ -@import "src/theme"; +@use "theme"; .cardContent { & > *:not(:first-child) { - margin-top: spacing(4); + margin-top: theme.spacing(4); } } .chipsContainer { - padding: spacing(1, 0); + padding: theme.spacing(1, 0); & > *:not(:first-child) { - margin-left: spacing(2); + margin-left: theme.spacing(2); } } diff --git a/front/src/pages/Presets/PresetForm.tsx b/front/src/pages/Presets/PresetForm.tsx index 43fa96c..248f440 100644 --- a/front/src/pages/Presets/PresetForm.tsx +++ b/front/src/pages/Presets/PresetForm.tsx @@ -1,139 +1,175 @@ import { ChangeEvent, useEffect, useRef, useState } from "react"; -import { Checkbox, Chip, FormControl, FormHelperText, Input, InputAdornment, InputLabel, MenuItem, Select, TextField } from "@material-ui/core"; +import { + Checkbox, + Chip, + FormControl, + FormHelperText, + Input, + InputAdornment, + InputLabel, + MenuItem, + Select, + TextField, +} from "@material-ui/core"; import { useTranslation } from "react-i18next"; import { Preset } from "../../models/presets.type"; import { Autocomplete } from "@material-ui/lab"; type PresetProps = { - preset: Preset, - onUpdate: (p:Preset) => void + preset: Preset; + onUpdate: (p: Preset) => void; }; const ALL_EXTENSIONS = "___all___"; -const PresetForm:React.FC= ({preset, onUpdate}) => { - const {t} = useTranslation(); - const didMount = useRef(false); - const [name, setName] = useState(preset.name || ""); - const [watchDir, setWatchDir] = useState(preset.watch_dir || ""); - const [outputDir, setOutputDir] = useState(preset.output_dir || ""); - const [minFileSizeStr, setMinFileSizeStr] = useState(""+(preset.min_file_size || 0)); - const [minFileSizeUnit, setMinFileSizeUnit] = useState(1); - const [minFileSize, setMinFileSize] = useState(preset.min_file_size || 0); - const [createSubDir, setCreateSubDir] = useState(preset.create_sub_dir || false); - const [extensions, setExtensions] = useState(preset.file_extensions ? [...preset.file_extensions] : [ALL_EXTENSIONS]); +const PresetForm: React.FC = ({ preset, onUpdate }) => { + const { t } = useTranslation(); + const didMount = useRef(false); + const [name, setName] = useState(preset.name || ""); + const [watchDir, setWatchDir] = useState(preset.watch_dir || ""); + const [outputDir, setOutputDir] = useState(preset.output_dir || ""); + const [minFileSizeStr, setMinFileSizeStr] = useState( + "" + (preset.min_file_size || 0) + ); + const [minFileSizeUnit, setMinFileSizeUnit] = useState("B"); + const [minFileSize, setMinFileSize] = useState(null); + const [createSubDir, setCreateSubDir] = useState( + preset.create_sub_dir || false + ); + const [extensions, setExtensions] = useState( + preset.file_extensions ? [...preset.file_extensions] : [ALL_EXTENSIONS] + ); - const cleanMinFileSize = () => { - const minFileSize = minFileSizeStr.length > 0 ? parseInt(minFileSizeStr, 10) : 0; - if (minFileSize > 0) { - let tmpFileSize = minFileSize * minFileSizeUnit; - let curUnit = 1e12; - while(!Number.isInteger(tmpFileSize/curUnit)) { - curUnit /= 1e3; - } - setMinFileSizeStr(""+(tmpFileSize/curUnit)); - setMinFileSizeUnit(curUnit); - } else { - setMinFileSizeUnit(1); - } - - if( minFileSizeStr.length === 0) { - setMinFileSizeStr("0"); - } - }; + const cleanMinFileSize = () => { + setMinFileSizeStr("" + (parseFloat(preset.min_file_size || "") || 0)); + const unit = (preset.min_file_size || "0").replace(/[0-9.]/g, "") || "B"; + setMinFileSizeUnit(unit); + }; - useEffect(cleanMinFileSize, [preset.min_file_size]); + useEffect(cleanMinFileSize, [preset.min_file_size]); - useEffect(() => { - const newMinFileSize = (minFileSizeStr.length > 0 ? parseInt(minFileSizeStr, 10) : 0)*minFileSizeUnit; - setMinFileSize(newMinFileSize); - }, [minFileSizeStr, minFileSizeUnit]); + useEffect(() => { + if (minFileSizeStr === "" || minFileSizeStr === "0") { + setMinFileSize(null); + } else { + setMinFileSize(`${minFileSizeStr}${minFileSizeUnit}`); + } + }, [minFileSizeStr, minFileSizeUnit]); - useEffect(() => { - if (didMount.current) { - const exts = [...extensions]; - if (exts.indexOf(ALL_EXTENSIONS) >= 0) { - exts.splice(exts.indexOf(ALL_EXTENSIONS), 1); - } + useEffect(() => { + if (didMount.current) { + const exts = [...extensions]; + if (exts.indexOf(ALL_EXTENSIONS) >= 0) { + exts.splice(exts.indexOf(ALL_EXTENSIONS), 1); + } - const newPreset = { - ...preset, - name, - watch_dir: watchDir, - output_dir: outputDir, - min_file_size: minFileSize, - create_sub_dir: createSubDir, - file_extensions: exts.length ? exts : null - }; - cleanMinFileSize(); - onUpdate(newPreset); - } else { - didMount.current = true; - } - }, [name, watchDir, outputDir, minFileSize, createSubDir, extensions]); + const newPreset = { + ...preset, + name, + watch_dir: watchDir, + output_dir: outputDir, + min_file_size: minFileSize, + create_sub_dir: createSubDir, + file_extensions: exts.length ? exts : null, + }; + onUpdate(newPreset); + } else { + didMount.current = true; + } + }, [name, watchDir, outputDir, minFileSize, createSubDir, extensions]); + const handleChangeMinFileSizeUnit = (e: ChangeEvent<{ value: unknown }>) => { + setMinFileSizeUnit(e.target.value as string); + }; - const handleChangeMinFileSizeUnit = (e:ChangeEvent<{ value: unknown }>) => { - setMinFileSizeUnit(parseInt(e.target.value as string, 10)); + const handleUpdateExtensions = (e: ChangeEvent<{}>, value: string[]) => { + if (value.indexOf(ALL_EXTENSIONS) >= 0) { + value.splice(value.indexOf(ALL_EXTENSIONS), 1); } + setExtensions(value.length === 0 ? [ALL_EXTENSIONS] : value); + }; - const handleUpdateExtensions = (e:ChangeEvent<{ }>, value: string[]) => { - if(value.indexOf(ALL_EXTENSIONS) >= 0) { - value.splice(value.indexOf(ALL_EXTENSIONS), 1); + return ( + <> + setName(e.target.value)} + fullWidth={true} + /> + setWatchDir(e.target.value)} + fullWidth={true} + /> + setOutputDir(e.target.value)} + fullWidth={true} + /> + + value.map((option: string, index: number) => + option === ALL_EXTENSIONS ? ( + + ) : ( + + ) + ) } - setExtensions(value.length === 0 ? [ALL_EXTENSIONS] : value); - }; - - return <> - setName(e.target.value)} - fullWidth={true} /> - setWatchDir(e.target.value)} - fullWidth={true} /> - setOutputDir(e.target.value)} - fullWidth={true} /> - - value.map((option: string, index: number) => option === ALL_EXTENSIONS ? : ) - } - renderInput={(params) => ( - - )} - /> - - {t("presets.min_file_size")} - setMinFileSizeStr(e.target.value)} - endAdornment={ - - - } + renderInput={(params) => ( + - {t("presets.min_file_size_hint")} - - - setCreateSubDir(e.target.checked)} /> {t("presets.create_sub_dir")} + )} + /> + + + {t("presets.min_file_size")} - ; -} + setMinFileSizeStr(e.target.value)} + endAdornment={ + + + + } + /> + {t("presets.min_file_size_hint")} + + + setCreateSubDir(e.target.checked)} + />{" "} + {t("presets.create_sub_dir")} + + + ); +}; -export default PresetForm; \ No newline at end of file +export default PresetForm; diff --git a/front/src/pages/Presets/Presets.module.scss b/front/src/pages/Presets/Presets.module.scss index 362f586..a7673ad 100644 --- a/front/src/pages/Presets/Presets.module.scss +++ b/front/src/pages/Presets/Presets.module.scss @@ -1,12 +1,12 @@ -@import "src/theme"; +@use "theme"; .root { - padding: spacing(2); + padding: theme.spacing(2); margin-bottom: 56px !important; } .floatingButton { position: absolute !important; - bottom: spacing(2); - right: spacing(2); + bottom: theme.spacing(2); + right: theme.spacing(2); } diff --git a/front/src/pages/Settings/Settings.module.scss b/front/src/pages/Settings/Settings.module.scss index 3a34084..7ea00f1 100644 --- a/front/src/pages/Settings/Settings.module.scss +++ b/front/src/pages/Settings/Settings.module.scss @@ -1,24 +1,24 @@ -@import "src/theme"; +@use "theme"; .root { - padding: spacing(4); + padding: theme.spacing(4); } .formControl { - margin: spacing(1); + margin: theme.spacing(1); } .spacer { - height: spacing(4); + height: theme.spacing(4); width: 1px; } .link { @media (prefers-color-scheme: dark) { - color: $primary-light; + color: theme.$primary-light; } @media (prefers-color-scheme: light) { - color: $primary-dark; + color: theme.$primary-dark; } } diff --git a/front/src/pages/Settings/Settings.tsx b/front/src/pages/Settings/Settings.tsx index e54932c..8616849 100644 --- a/front/src/pages/Settings/Settings.tsx +++ b/front/src/pages/Settings/Settings.tsx @@ -1,242 +1,286 @@ import classes from "./Settings.module.scss"; import { - Button, - Card, - CardContent, - Checkbox, - FormControl, - Grid, - InputLabel, - Select, - TextField + Button, + Card, + CardContent, + FormControl, + Grid, + InputLabel, + Select, + TextField, } from "@material-ui/core"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import AppContent from "../../layouts/AppContent"; import AppTopBar from "../../layouts/AppTopBar"; -import {ChangeEvent, useEffect, useState} from "react"; +import { ChangeEvent, useEffect, useState } from "react"; import httpApi from "../../api/http"; -import {Configuration} from "../../models/configuration.type"; -import {useAppDispatch} from "../../store"; +import { Configuration } from "../../models/configuration.type"; const Settings = () => { - const [isLoading, setIsLoading] = useState(false); - const [config, setConfig] = useState(); - // Needed to not override api key and password if no change - const [newConfig, setNewConfig] = useState({}); - const [hasChange, setHasChange] = useState(false); - const [restartRequired, setRestartRequired] = useState(false); - const {t, i18n} = useTranslation(); - const languages = (i18n.options.supportedLngs || []).filter(l => l !== "cimode"); - - const dispatch = useAppDispatch(); - - useEffect(() => { - (async () => { - setIsLoading(true); - const {data} = await httpApi.get("/configuration"); - setConfig(data); - setIsLoading(false); - })(); - }, []); - - useEffect(() => { - if (newConfig && Object.keys(newConfig).length) { - setHasChange(true); - } - }, [newConfig]); - - const handleChangeDebug = ({target}: ChangeEvent) => { - if (config) { - const tmpConf = Object.assign({}, config); - tmpConf.debug = target.checked; - - const tmpNewConfig = Object.assign({}, newConfig); - tmpNewConfig.debug = tmpConf.debug; - - setRestartRequired(true); - setConfig(tmpConf); - setNewConfig(tmpNewConfig); - } - }; - - const handleChangeApiKey = ({target}: ChangeEvent) => { - if (config) { - const tmpConf = Object.assign({}, config); - tmpConf.api_key = target.value; - - const tmpNewConfig = Object.assign({}, newConfig); - tmpNewConfig.api_key = tmpConf.api_key; - - setConfig(tmpConf); - setNewConfig(tmpNewConfig); - } - }; - - const handleChangeBasePath = ({target}: ChangeEvent) => { - if (config) { - const tmpConf = Object.assign({}, config); - tmpConf.base_path = target.value; - - const tmpNewConfig = Object.assign({}, newConfig); - tmpNewConfig.base_path = tmpConf.base_path; - - setRestartRequired(true); - setConfig(tmpConf); - setNewConfig(tmpNewConfig); - } - }; - - const handleChangeRealDebridApiKey = ({target}: ChangeEvent) => { - if (config) { - const tmpConf = Object.assign({}, config); - if (!tmpConf.debriders) { - tmpConf.debriders = {real_debrid: {api_key: ""}}; - } - tmpConf.debriders.real_debrid.api_key = target.value; - - const tmpNewConfig = Object.assign({}, newConfig); - tmpNewConfig.debriders = tmpConf.debriders; - - setConfig(tmpConf); - setNewConfig(tmpNewConfig); - } - }; - - const handleChangeSynoEndpoint = ({target}: ChangeEvent) => { - if (config) { - const tmpConf = Object.assign({}, config); - if (!tmpConf.downloaders) { - tmpConf.downloaders = {synology_download_station: {endpoint: "", username: "", password: ""}}; - } - tmpConf.downloaders.synology_download_station.endpoint = target.value; - - const tmpNewConfig = Object.assign({}, newConfig); - if (!tmpNewConfig.downloaders) { - tmpNewConfig.downloaders = {synology_download_station: {}}; - } - tmpNewConfig.downloaders.synology_download_station.endpoint = tmpConf.downloaders.synology_download_station.endpoint; - - setConfig(tmpConf); - setNewConfig(tmpNewConfig); - } - }; - - const handleChangeSynoUsername = ({target}: ChangeEvent) => { - if (config) { - const tmpConf = Object.assign({}, config); - if (!tmpConf.downloaders) { - tmpConf.downloaders = {synology_download_station: {endpoint: "", username: "", password: ""}}; - } - tmpConf.downloaders.synology_download_station.username = target.value; - - const tmpNewConfig = Object.assign({}, newConfig); - if (!tmpNewConfig.downloaders) { - tmpNewConfig.downloaders = {synology_download_station: {}}; - } - tmpNewConfig.downloaders.synology_download_station.username = tmpConf.downloaders.synology_download_station.username; - - setConfig(tmpConf); - setNewConfig(tmpNewConfig); - } - }; - - const handleChangeSynoPassword = ({target}: ChangeEvent) => { - if (config) { - const tmpConf = Object.assign({}, config); - if (!tmpConf.downloaders) { - tmpConf.downloaders = {synology_download_station: {endpoint: "", username: "", password: ""}}; - } - tmpConf.downloaders.synology_download_station.password = target.value; - - const tmpNewConfig = Object.assign({}, newConfig); - if (!tmpNewConfig.downloaders) { - tmpNewConfig.downloaders = {synology_download_station: {}}; - } - tmpNewConfig.downloaders.synology_download_station.password = tmpConf.downloaders.synology_download_station.password; - - setConfig(tmpConf); - setNewConfig(tmpNewConfig); - } - }; - - const handleSave = async (e:React.FormEvent) => { - e.preventDefault(); - - setIsLoading(true); - const {data} = await httpApi.patch(`/configuration`, newConfig); - setConfig(data); - if (data.api_key.length) { - dispatch({type: "appConfig/set", payload: {apiKey: data.api_key}}); - } - setIsLoading(false); - }; - - return <> - - - - - - -
- - - {t("settings.language")} - - -

{t("settings.configuration_subtitle")}

- {config ? <> - {t("settings.configuration_debug")} - - - -

Real-Debrid

- -
- {t("settings.retrieve_real_debrid_token")} {t("settings.real_debrid_website")} -
-

Synology Download Station

- - - -
- - {restartRequired ?

{t("settings.configuration_restart_required")}

: null} - : null} - - - - - - - - ; + const [isLoading, setIsLoading] = useState(false); + const [config, setConfig] = useState(); + // Needed to not override api key and password if no change + const [newConfig, setNewConfig] = useState({}); + const [hasChange, setHasChange] = useState(false); + const [restartRequired, setRestartRequired] = useState(false); + const { t, i18n } = useTranslation(); + const languages = (i18n.options.supportedLngs || []).filter( + (l) => l !== "cimode" + ); + + useEffect(() => { + (async () => { + setIsLoading(true); + const { data } = await httpApi.get("/configuration"); + setConfig(data); + setIsLoading(false); + })(); + }, []); + + useEffect(() => { + if (newConfig && Object.keys(newConfig).length) { + setHasChange(true); + } + }, [newConfig]); + + const handleChangeBasePath = ({ target }: ChangeEvent) => { + if (config) { + const tmpConf = Object.assign({}, config); + tmpConf.base_path = target.value; + + const tmpNewConfig = Object.assign({}, newConfig); + tmpNewConfig.base_path = tmpConf.base_path; + + setRestartRequired(true); + setConfig(tmpConf); + setNewConfig(tmpNewConfig); + } + }; + + const handleChangeRealDebridApiKey = ({ + target, + }: ChangeEvent) => { + if (config) { + const tmpConf = Object.assign({}, config); + if (!tmpConf.debrider) { + tmpConf.debrider = { real_debrid: { api_key: "" } }; + } + tmpConf.debrider.real_debrid.api_key = target.value; + + const tmpNewConfig = Object.assign({}, newConfig); + tmpNewConfig.debrider = tmpConf.debrider; + + setConfig(tmpConf); + setNewConfig(tmpNewConfig); + } + }; + + const handleChangeSynoEndpoint = ({ + target, + }: ChangeEvent) => { + if (config) { + const tmpConf = Object.assign({}, config); + if (!tmpConf.downloader) { + tmpConf.downloader = { + synology_download_station: { + endpoint: "", + username: "", + password: "", + }, + }; + } + tmpConf.downloader.synology_download_station.endpoint = target.value; + + const tmpNewConfig = Object.assign({}, newConfig); + if (!tmpNewConfig.downloader) { + tmpNewConfig.downloader = { synology_download_station: {} }; + } + tmpNewConfig.downloader.synology_download_station.endpoint = + tmpConf.downloader.synology_download_station.endpoint; + + setConfig(tmpConf); + setNewConfig(tmpNewConfig); + } + }; + + const handleChangeSynoUsername = ({ + target, + }: ChangeEvent) => { + if (config) { + const tmpConf = Object.assign({}, config); + if (!tmpConf.downloader) { + tmpConf.downloader = { + synology_download_station: { + endpoint: "", + username: "", + password: "", + }, + }; + } + tmpConf.downloader.synology_download_station.username = target.value; + + const tmpNewConfig = Object.assign({}, newConfig); + if (!tmpNewConfig.downloader) { + tmpNewConfig.downloader = { synology_download_station: {} }; + } + tmpNewConfig.downloader.synology_download_station.username = + tmpConf.downloader.synology_download_station.username; + + setConfig(tmpConf); + setNewConfig(tmpNewConfig); + } + }; + + const handleChangeSynoPassword = ({ + target, + }: ChangeEvent) => { + if (config) { + const tmpConf = Object.assign({}, config); + if (!tmpConf.downloader) { + tmpConf.downloader = { + synology_download_station: { + endpoint: "", + username: "", + password: "", + }, + }; + } + tmpConf.downloader.synology_download_station.password = target.value; + + const tmpNewConfig = Object.assign({}, newConfig); + if (!tmpNewConfig.downloader) { + tmpNewConfig.downloader = { synology_download_station: {} }; + } + tmpNewConfig.downloader.synology_download_station.password = + tmpConf.downloader.synology_download_station.password; + + setConfig(tmpConf); + setNewConfig(tmpNewConfig); + } + }; + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + + setIsLoading(true); + const { data } = await httpApi.patch( + `/configuration`, + newConfig + ); + setConfig(data); + setIsLoading(false); + }; + + return ( + <> + + + + + + +
+ + + + {t("settings.language")} + + + +

{t("settings.configuration_subtitle")}

+ {config ? ( + <> + +

Real-Debrid

+ +
+ {t("settings.retrieve_real_debrid_token")}{" "} + + {t("settings.real_debrid_website")} + +
+

Synology Download Station

+ + + +
+ + {restartRequired ? ( +

{t("settings.configuration_restart_required")}

+ ) : null} + + ) : null} + + + + + + + + + ); }; export default Settings; diff --git a/front/src/pages/Status/Status.module.scss b/front/src/pages/Status/Status.module.scss index 2ad5116..76d9372 100644 --- a/front/src/pages/Status/Status.module.scss +++ b/front/src/pages/Status/Status.module.scss @@ -1,19 +1,19 @@ -@import "src/theme"; +@use "theme"; .root { - padding: spacing(4); + padding: theme.spacing(4); } .spacer { - height: spacing(4); + height: theme.spacing(4); width: 100%; } .separator { border: none; - border-top: 1px solid $grey-400; + border-top: 1px solid theme.$grey-400; } .pending { - @include pending; + @include theme.pending; } diff --git a/front/src/pages/Status/Status.tsx b/front/src/pages/Status/Status.tsx index e11ac2e..24845d8 100644 --- a/front/src/pages/Status/Status.tsx +++ b/front/src/pages/Status/Status.tsx @@ -1,92 +1,107 @@ import classes from "./Status.module.scss"; -import { - Button, - Card, - CardContent, - Grid, -} from "@material-ui/core"; -import {useTranslation} from "react-i18next"; +import { Card, CardContent, Grid } from "@material-ui/core"; +import { useTranslation } from "react-i18next"; import AppContent from "../../layouts/AppContent"; import AppTopBar from "../../layouts/AppTopBar"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import httpApi from "../../api/http"; -import {Status as StatusType} from "../../models/status.type"; +import { Status as StatusType } from "../../models/status.type"; import webSocket from "../../api/websocket"; -import { Configuration } from "../../models/configuration.type"; -import {StatusDot} from "../../components"; -import {Status as StatusDotType} from "../../components/StatusDot"; +import { StatusDot } from "../../components"; +import { Status as StatusDotType } from "../../components/StatusDot"; import classNames from "classnames"; const Status = () => { - const [isLoading, setIsLoading] = useState(false); - const [isInDocker, setIsInDocker] = useState(false); - const [appVersion, setAppVersion] = useState(""); - const [status, setStatus] = useState(); - const {t} = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [status, setStatus] = useState(); + const { t } = useTranslation(); - // TODO: watch webSocket connection status - // TODO: reload status when websocket is connected - useEffect(() => { - (async () => { - setIsLoading(true); - const {data} = await httpApi.get("/status"); - const {data: {is_in_docker, app_version}} = await httpApi.get("/configuration"); - setIsInDocker(is_in_docker); - setAppVersion(app_version); - setStatus(data); - setIsLoading(false); - })(); - }, []); + // TODO: watch webSocket connection status + // TODO: reload status when websocket is connected + useEffect(() => { + (async () => { + setIsLoading(true); + const { data } = await httpApi.get("/status"); + setStatus(data); + setIsLoading(false); + })(); + }, []); - const handleRestart = () => { - void httpApi.post("/server/restart"); - }; + let debriderStatus: StatusDotType = "pending"; + let downloaderStatus: StatusDotType = "pending"; + if (!isLoading) { + debriderStatus = status?.debrider.connected ? "success" : "error"; + downloaderStatus = status?.downloader.connected ? "success" : "error"; + } - let debriderStatus:StatusDotType = "pending"; - let downloaderStatus:StatusDotType = "pending"; - if(!isLoading) { - debriderStatus = status?.debrider_connected ? "success" : "error"; - downloaderStatus = status?.downloader_connected ? "success" : "error"; - } - - return <> - - - - - - -
{t("status.app_version")} {isLoading ? t("loading") : appVersion}
-
-
-
- - - {t("status.websocket")} - - - {t("status.debrider")} - - - {t("status.downloader")} - - - {isInDocker ? <> -
-
-
- - -
- {t("status.restart_information")} - - : null} - - + return ( + <> + + + + + + +
+ {t("status.app_version")}{" "} + + {isLoading ? t("loading") : status?.app.version} + +
+
+
+
+ + + {" "} + {t("status.websocket")} + + + + {t("status.debrider")} + {status?.debrider ? ( + ({status?.debrider.name}) + ) : null} + + + + {t("status.downloader")} + {status?.downloader ? ( + ({status?.downloader.name}) + ) : null} + - - - ; + + + + + + + ); }; export default Status; diff --git a/front/src/setupProxy.js b/front/src/setupProxy.js index 28efd05..15c779d 100644 --- a/front/src/setupProxy.js +++ b/front/src/setupProxy.js @@ -3,7 +3,7 @@ const { createProxyMiddleware } = require("http-proxy-middleware"); module.exports = function (app) { app.use( createProxyMiddleware("/api", { - target: "http://localhost:8781", + target: "http://localhost:8765", ws: true, }) ); diff --git a/front/src/store/appConfig/index.ts b/front/src/store/appConfig/index.ts deleted file mode 100644 index 968e461..0000000 --- a/front/src/store/appConfig/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {createAction, createReducer} from "@reduxjs/toolkit"; - -type AppConfigState = { - apiKey: string -} - -const initialState: AppConfigState = { - apiKey: process.env.REACT_APP_API_KEY || "%holerr-api-key-placeholder%" -}; - -const set = createAction>("appConfig/set"); - -export default createReducer(initialState, (builder) => { - builder.addCase(set, (state, {payload}) => { - Object.assign(state, payload); - }); -}); \ No newline at end of file diff --git a/front/src/store/downloads/downloads.thunk.ts b/front/src/store/downloads/downloads.thunk.ts index 62c4bb8..7bbf8db 100644 --- a/front/src/store/downloads/downloads.thunk.ts +++ b/front/src/store/downloads/downloads.thunk.ts @@ -1,17 +1,19 @@ -import {AppThunk} from ".."; -import {Download} from "../../models/downloads.type"; +import { AppThunk } from ".."; +import { Download } from "../../models/downloads.type"; import httpApi from "../../api/http"; -import {setAllDownload} from "./downloads.actions"; +import { setAllDownload } from "./downloads.actions"; export const fetchDownloads = (): AppThunk => async (dispatch) => { - const { data: downloads } = await httpApi.get("/downloads"); - dispatch(setAllDownload(downloads as Download[])); + const { data: downloads } = await httpApi.get("/downloads"); + dispatch(setAllDownload(downloads as Download[])); }; -export const deleteDownload = (id: string): AppThunk => async (dispatch) => { +export const deleteDownload = + (id: string): AppThunk => + async (dispatch) => { await httpApi.delete(`/downloads/${id}`); -}; + }; export const cleanUpDownload = (): AppThunk => async (dispatch) => { - await httpApi.post("/downloads/clean_up"); + await httpApi.post("/actions/clean_downloaded"); }; diff --git a/front/src/store/reducers.ts b/front/src/store/reducers.ts index f94c170..152ebdf 100644 --- a/front/src/store/reducers.ts +++ b/front/src/store/reducers.ts @@ -1,12 +1,10 @@ import { combineReducers } from "@reduxjs/toolkit"; import downloads from "./downloads/downloads.slice"; import presets from "./presets/presets.slice"; -import appConfig from "./appConfig"; const reducers = combineReducers({ downloads: downloads.reducer, presets: presets.reducer, - appConfig }); export default reducers; diff --git a/front/src/theme/_colors.scss b/front/src/theme/_colors.scss index 874d750..d04b82e 100644 --- a/front/src/theme/_colors.scss +++ b/front/src/theme/_colors.scss @@ -1,37 +1,50 @@ -@import "node_modules/@material/theme/color-palette"; +@use "~@material/theme/color-palette" as material; -$primary-50: $orange-50; -$primary-100: $orange-100; -$primary-200: $orange-200; -$primary-300: $orange-300; -$primary-400: $orange-400; -$primary-500: $orange-500; -$primary-600: $orange-600; -$primary-700: $orange-700; -$primary-800: $orange-800; -$primary-900: $orange-900; -$primary-a100: $orange-a100; -$primary-a200: $orange-a200; -$primary-a400: $orange-a400; -$primary-a700: $orange-a700; +$grey-200: material.$grey-200; +$grey-300: material.$grey-300; +$grey-400: material.$grey-400; +$grey-500: material.$grey-500; + +$green-200: material.$green-200; +$green-400: material.$green-400; + +$red-400: material.$red-400; +$red-500: material.$red-500; + +$blue-500: material.$blue-500; + +$primary-50: material.$orange-50; +$primary-100: material.$orange-100; +$primary-200: material.$orange-200; +$primary-300: material.$orange-300; +$primary-400: material.$orange-400; +$primary-500: material.$orange-500; +$primary-600: material.$orange-600; +$primary-700: material.$orange-700; +$primary-800: material.$orange-800; +$primary-900: material.$orange-900; +$primary-a100: material.$orange-a100; +$primary-a200: material.$orange-a200; +$primary-a400: material.$orange-a400; +$primary-a700: material.$orange-a700; $primary-light: $primary-400; $primary-main: $primary-700; $primary-dark: $primary-800; -$secondary-50: $light-blue-50; -$secondary-100: $light-blue-100; -$secondary-200: $light-blue-200; -$secondary-300: $light-blue-300; -$secondary-400: $light-blue-400; -$secondary-500: $light-blue-500; -$secondary-600: $light-blue-600; -$secondary-700: $light-blue-700; -$secondary-800: $light-blue-800; -$secondary-900: $light-blue-900; -$secondary-a100: $light-blue-a100; -$secondary-a200: $light-blue-a200; -$secondary-a400: $light-blue-a400; -$secondary-a700: $light-blue-a700; +$secondary-50: material.$light-blue-50; +$secondary-100: material.$light-blue-100; +$secondary-200: material.$light-blue-200; +$secondary-300: material.$light-blue-300; +$secondary-400: material.$light-blue-400; +$secondary-500: material.$light-blue-500; +$secondary-600: material.$light-blue-600; +$secondary-700: material.$light-blue-700; +$secondary-800: material.$light-blue-800; +$secondary-900: material.$light-blue-900; +$secondary-a100: material.$light-blue-a100; +$secondary-a200: material.$light-blue-a200; +$secondary-a400: material.$light-blue-a400; +$secondary-a700: material.$light-blue-a700; $secondary-light: $secondary-400; $secondary-main: $secondary-700; $secondary-dark: $secondary-800; diff --git a/front/src/theme/_index.scss b/front/src/theme/_index.scss new file mode 100644 index 0000000..f13da85 --- /dev/null +++ b/front/src/theme/_index.scss @@ -0,0 +1,5 @@ +@forward "colors"; +@forward "spacing"; +@forward "animations"; + +$border-radius: 8px; diff --git a/front/src/theme/index.scss b/front/src/theme/index.scss deleted file mode 100644 index 320d338..0000000 --- a/front/src/theme/index.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import "./colors"; -@import "./spacing"; -@import "./animations"; - -$border-radius: 8px; diff --git a/server/.vscode/settings.json b/server/.vscode/settings.json new file mode 100644 index 0000000..2d9a081 --- /dev/null +++ b/server/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "files.exclude": { + "**/__pycache__": true + } +} diff --git a/server/.vscode/tasks.json b/server/.vscode/tasks.json new file mode 100644 index 0000000..3016367 --- /dev/null +++ b/server/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Migrate DB", + "type": "shell", + "command": "source .venv/bin/activate && alembic upgrade head", + "options": {}, + "presentation": { + "reveal": "never", + "panel": "dedicated" + }, + "problemMatcher": [] + }, + { + "label": "Run", + "type": "shell", + "command": "source .venv/bin/activate && nodemon --watch holerr/ -e py --exec python -m holerr --signal SIGTERM", + "dependsOn": ["Migrate DB"], + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + } + ] +} diff --git a/server/alembic.ini b/server/alembic.ini new file mode 100644 index 0000000..3db3ce7 --- /dev/null +++ b/server/alembic.ini @@ -0,0 +1,116 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = sqlite:///../data/db.sqlite3 + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/server/alembic/README b/server/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/server/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/server/alembic/env.py b/server/alembic/env.py new file mode 100644 index 0000000..3f2d4f8 --- /dev/null +++ b/server/alembic/env.py @@ -0,0 +1,78 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from holerr.database.models import Base + +target_metadata = [Base.metadata] + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/server/alembic/script.py.mako b/server/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/server/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/server/alembic/versions/6b54a1283344_initial_version.py b/server/alembic/versions/6b54a1283344_initial_version.py new file mode 100644 index 0000000..0c2eafc --- /dev/null +++ b/server/alembic/versions/6b54a1283344_initial_version.py @@ -0,0 +1,90 @@ +"""Initial version + +Revision ID: 6b54a1283344 +Revises: +Create Date: 2024-05-04 17:21:50.905044 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '6b54a1283344' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('download', + sa.Column('id', sa.String(), nullable=False), + sa.Column('magnet', sa.String(), nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('preset', sa.String(), nullable=False), + sa.Column('status', sa.Integer(), nullable=False), + sa.Column('total_bytes', sa.Integer(), nullable=False), + sa.Column('total_progress', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('debrider_file', + sa.Column('id', sa.String(), nullable=False), + sa.Column('download_id', sa.String(), nullable=False), + sa.Column('path', sa.String(), nullable=False), + sa.Column('bytes', sa.Integer(), nullable=False), + sa.Column('selected', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['download_id'], ['download.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('debrider_info', + sa.Column('id', sa.String(), nullable=False), + sa.Column('download_id', sa.String(), nullable=False), + sa.Column('filename', sa.String(), nullable=False), + sa.Column('bytes', sa.Integer(), nullable=False), + sa.Column('progress', sa.Integer(), nullable=False), + sa.Column('status', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['download_id'], ['download.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('debrider_link', + sa.Column('link', sa.String(), nullable=False), + sa.Column('download_id', sa.String(), nullable=False), + sa.Column('is_unrestricted', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['download_id'], ['download.id'], ), + sa.PrimaryKeyConstraint('link') + ) + op.create_table('downloader_info', + sa.Column('download_id', sa.String(), nullable=False), + sa.Column('progress', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['download_id'], ['download.id'], ), + sa.PrimaryKeyConstraint('download_id') + ) + op.create_table('downloader_task', + sa.Column('id', sa.String(), nullable=False), + sa.Column('download_id', sa.String(), nullable=False), + sa.Column('status', sa.Integer(), nullable=False), + sa.Column('bytes_downloaded', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['download_id'], ['download.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('downloader_task') + op.drop_table('downloader_info') + op.drop_table('debrider_link') + op.drop_table('debrider_info') + op.drop_table('debrider_file') + op.drop_table('download') + # ### end Alembic commands ### diff --git a/server/alembic/versions/f0b87f1198a2_add_to_delete_field_to_download_table.py b/server/alembic/versions/f0b87f1198a2_add_to_delete_field_to_download_table.py new file mode 100644 index 0000000..ebbb070 --- /dev/null +++ b/server/alembic/versions/f0b87f1198a2_add_to_delete_field_to_download_table.py @@ -0,0 +1,30 @@ +"""Add to_delete field to download table + +Revision ID: f0b87f1198a2 +Revises: 6b54a1283344 +Create Date: 2024-05-04 17:23:29.795675 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f0b87f1198a2' +down_revision: Union[str, None] = '6b54a1283344' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('download', sa.Column('to_delete', sa.Boolean(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('download', 'to_delete') + # ### end Alembic commands ### diff --git a/server/api/api.go b/server/api/api.go deleted file mode 100644 index 847ae9c..0000000 --- a/server/api/api.go +++ /dev/null @@ -1,31 +0,0 @@ -package api - -import ( - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" -) - -func Router(r chi.Router) { - r.Use(middleware.SetHeader("Content-Type", "application/json")) - r.Use(CheckApiKey) - - r.Get(`/status`, StatusList) - r.Post(`/server/restart`, ServerRestart) - - r.Get(`/configuration`, ConfigList) - r.Patch(`/configuration`, ConfigUpdate) - - r.Get(`/downloads`, DownloadsList) - r.Post(`/downloads`, DownloadsAdd) - r.Delete(`/downloads/{id}`, DownloadsDelete) - r.Post(`/downloads/clean_up`, DownloadsCleanUp) - - r.Get(`/ws`, Websocket) - - r.Get(`/presets`, PresetsList) - r.Post(`/presets`, PresetsAdd) - r.Patch(`/presets/{name}`, PresetsUpdate) - r.Delete(`/presets/{name}`, PresetsDelete) - - r.Get(`/constants`, ConstantsList) -} diff --git a/server/api/config.go b/server/api/config.go deleted file mode 100644 index 9b11483..0000000 --- a/server/api/config.go +++ /dev/null @@ -1,62 +0,0 @@ -package api - -import ( - "encoding/json" - "holerr/core/config" - "log" - "net/http" - "strings" - - "github.com/spf13/viper" -) - -func hideSecret(s string) string { - return s[0:1] + strings.Repeat("*", len(s)-2) + s[len(s)-1:] -} - -func ConfigList(w http.ResponseWriter, r *http.Request) { - list := map[string]interface{}{ - config.ConfKeyDebug: config.IsDebug(), - config.ConfKeyAppVersion: config.AppVersion(), - config.ConfKeyIsInDocker: config.IsInDocker(), - config.ConfKeyApiKey: config.GetApiKey(), - config.ConfKeyBasePath: config.GetBasePath(), - } - - debriders, debridersErr := config.GetDebriders() - if debridersErr == nil { - if debriders.RealDebrid.ApiKey != "" { - debriders.RealDebrid.ApiKey = hideSecret(debriders.RealDebrid.ApiKey) - } - list[config.ConfKeyDebriders] = debriders - } - - downloaders, downloadersErr := config.GetDownloaders() - if downloadersErr == nil { - if downloaders.SynologyDownloadStation.Password != "" { - downloaders.SynologyDownloadStation.Password = hideSecret(downloaders.SynologyDownloadStation.Password) - } - list[config.ConfKeyDownloaders] = downloaders - } - - body, _ := json.Marshal(list) - w.Write(body) -} - -func ConfigUpdate(w http.ResponseWriter, r *http.Request) { - decodeError := viper.MergeConfig(r.Body) - if decodeError != nil { - log.Println(decodeError) - w.WriteHeader(http.StatusInternalServerError) - content := map[string]error{ - "message": decodeError, - } - body, _ := json.Marshal(content) - w.Write(body) - } else { - viper.WriteConfig() - config.SetBasePath(config.GetBasePath()) - config.SetApiKey(config.GetApiKey()) - ConfigList(w, r) - } -} diff --git a/server/api/constants.go b/server/api/constants.go deleted file mode 100644 index b362d60..0000000 --- a/server/api/constants.go +++ /dev/null @@ -1,17 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "holerr/core/db" - "holerr/debriders/debrider" -) - -func ConstantsList(w http.ResponseWriter, r *http.Request) { - var list = map[string]interface{}{ - "download_status": db.DownloadStatus, - "torrent_status": debrider.TorrentStatus, - } - body, _ := json.Marshal(list) - w.Write(body) -} diff --git a/server/api/downloads.go b/server/api/downloads.go deleted file mode 100644 index 61f207f..0000000 --- a/server/api/downloads.go +++ /dev/null @@ -1,194 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "holerr/core/config" - "holerr/core/db" - "holerr/core/log" - "holerr/debriders" - "holerr/downloaders" - "io" - "net/http" - "os" - "path/filepath" - "reflect" - "strings" - - "github.com/go-chi/chi" -) - -func DownloadsList(w http.ResponseWriter, r *http.Request) { - dbi := db.Get() - records, err := dbi.ReadAll("downloads") - if err != nil { - log.Error(err) - } - - downloads := []db.Download{} - for _, f := range records { - down := db.Download{} - if err := json.Unmarshal([]byte(f), &down); err != nil { - log.Error(err) - } - downloads = append(downloads, down) - } - - body, _ := json.Marshal(downloads) - w.Write(body) -} - -func DownloadsAdd(w http.ResponseWriter, r *http.Request) { - r.ParseMultipartForm(32 << 20) - - // Read form fields - presetName := r.FormValue("preset") - preset, err := config.GetPresetByName(presetName) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": err.Error()}) - w.Write(body) - return - } - - // Source - file, handler, err := r.FormFile("torrent_file") - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": err.Error()}) - w.Write(body) - return - } - defer file.Close() - - // Destination - outputFilePath := filepath.Clean(fmt.Sprintf(`%s/%s/%s`, config.GetDataDir(), preset.WatchDir, handler.Filename)) - dst, err := os.Create(outputFilePath) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": err.Error()}) - w.Write(body) - return - } - defer dst.Close() - - // Copy - if _, err = io.Copy(dst, file); err != nil { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": err.Error()}) - w.Write(body) - return - } - - body, _ := json.Marshal(map[string]string{}) - w.Write(body) -} - -func DownloadsDelete(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - dbi := db.Get() - down := db.Download{} - dbi.Read("downloads", id, &down) - if reflect.DeepEqual(down, db.Download{}) { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": "Download not found"}) - w.Write(body) - return - } - - // Torrent found - if down.Status == db.DownloadStatus["TORRENT_FOUND"] { - // Do nothing, too early, torrent is added just after - } - - // Debrider - if down.Status >= db.DownloadStatus["TORRENT_SENT_TO_DEBRIDER"] && down.Status <= db.DownloadStatus["DEBRIDER_DOWNLOADED"] { - debrider := debriders.Get() - if debrider != nil { - err := debrider.DeleteTorrent(down.TorrentInfo.Id) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": "Could not delete torrent on debrider"}) - w.Write(body) - return - } - } else { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": "No debrider set"}) - w.Write(body) - } - } - - // Downloader - if down.Status >= db.DownloadStatus["SENT_TO_DOWNLOADER"] && down.Status <= db.DownloadStatus["DOWNLOADER_DOWNLOADED"] { - ids := make([]string, 0, len(down.DownloadInfo.Tasks)) - for _, task := range down.DownloadInfo.Tasks { - ids = append(ids, task.Id) - } - - if len(ids) == 0 { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": "No download task to delete"}) - w.Write(body) - return - } - - downloader := downloaders.Get() - if downloader != nil { - err := downloader.DeleteDownload(strings.Join(ids, ",")) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": "Could not delete download task on downloader"}) - w.Write(body) - return - } - } else { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": "No downloader set"}) - w.Write(body) - } - } - - if writeErr := dbi.Delete("downloads", id); writeErr != nil { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": "Could not delete download in database"}) - w.Write(body) - return - } - - WebsocketBroadcast("downloads/delete", down) - - body, _ := json.Marshal(map[string]string{}) - w.Write(body) -} - -func DownloadsCleanUp(w http.ResponseWriter, r *http.Request) { - dbi := db.Get() - records, err := dbi.ReadAll("downloads") - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - body, _ := json.Marshal(map[string]string{"message": err.Error()}) - w.Write(body) - return - } - - downloads := []db.Download{} - for _, f := range records { - down := db.Download{} - if err := json.Unmarshal([]byte(f), &down); err != nil { - log.Error(err) - } - if down.Status >= db.DownloadStatus["DOWNLOADER_DOWNLOADED"] { - errRemove := dbi.Delete("downloads", down.Id) - if errRemove != nil { - log.Error(errRemove) - } - downloads = append(downloads, down) - - WebsocketBroadcast("downloads/delete", down) - } - } - - body, _ := json.Marshal(downloads) - w.Write(body) -} diff --git a/server/api/middlewares.go b/server/api/middlewares.go deleted file mode 100644 index 72abafa..0000000 --- a/server/api/middlewares.go +++ /dev/null @@ -1,32 +0,0 @@ -package api - -import ( - "encoding/json" - "holerr/core/config" - "net/http" -) - -func CheckApiKey(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - apiKey := config.GetApiKey() - - if apiKey != "" { - userApiKey := r.Header.Get("X-Api-Key") - if userApiKey == "" { - userApiKey = r.URL.Query().Get("x-api-key") - } - - if userApiKey != apiKey { - w.WriteHeader(http.StatusForbidden) - content := map[string]string { - "message" : "Forbidden", - } - body, _ := json.Marshal(content) - w.Write(body) - return - } - } - - next.ServeHTTP(w, r) - }) -} diff --git a/server/api/presets.go b/server/api/presets.go deleted file mode 100644 index 4736341..0000000 --- a/server/api/presets.go +++ /dev/null @@ -1,84 +0,0 @@ -package api - -import ( - "encoding/json" - "holerr/core/config" - "log" - "net/http" - "strings" - - "github.com/go-chi/chi" -) - -func PresetsList(w http.ResponseWriter, r *http.Request) { - presets, _ := config.GetPresets() - - body, _ := json.Marshal(presets) - w.Write(body) -} - -func PresetsAdd(w http.ResponseWriter, r *http.Request) { - preset := config.Preset{} - decodeErr := json.NewDecoder(r.Body).Decode(&preset) - - if decodeErr != nil { - w.WriteHeader(http.StatusInternalServerError) - - content := map[string]string{ - "message": "Decode error: " + decodeErr.Error(), - } - body, _ := json.Marshal(content) - w.Write(body) - return - } - - addErr := config.AddPreset(preset) - if addErr != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(addErr.Error())) - return - } - - preset, _ = config.GetPresetByName(preset.Name) - body, _ := json.Marshal(preset) - w.Write(body) -} - -func PresetsUpdate(w http.ResponseWriter, r *http.Request) { - name := strings.Replace(chi.URLParam(r, "name"), "%2F", "/", -1) - - preset := config.Preset{} - decodeErr := json.NewDecoder(r.Body).Decode(&preset) - - if decodeErr != nil { - w.WriteHeader(http.StatusInternalServerError) - content := map[string]string{ - "message": "Decode error: " + decodeErr.Error(), - } - body, _ := json.Marshal(content) - w.Write(body) - return - } - - updateErr := config.UpdatePreset(name, preset) - if updateErr != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(updateErr.Error())) - return - } - - newName := name - if preset.Name != "" { - newName = preset.Name - } - preset, _ = config.GetPresetByName(newName) - body, _ := json.Marshal(preset) - w.Write(body) -} - -func PresetsDelete(w http.ResponseWriter, r *http.Request) { - name := strings.Replace(chi.URLParam(r, "name"), "%2F", "/", -1) - log.Println(name) - config.RemovePreset(name) - w.WriteHeader(http.StatusNoContent) -} diff --git a/server/api/server.go b/server/api/server.go deleted file mode 100644 index f2a9c18..0000000 --- a/server/api/server.go +++ /dev/null @@ -1,13 +0,0 @@ -package api - -import ( - "net/http" - "os" - "time" -) - -func ServerRestart(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNoContent) - time.Sleep(time.Millisecond * 200) - os.Exit(0) -} diff --git a/server/api/status.go b/server/api/status.go deleted file mode 100644 index 0e0f18f..0000000 --- a/server/api/status.go +++ /dev/null @@ -1,29 +0,0 @@ -package api - -import ( - "encoding/json" - "holerr/debriders" - "holerr/downloaders" - "net/http" -) - -func StatusList(w http.ResponseWriter, r *http.Request) { - debriderStatus := false - debrider := debriders.Get() - if debrider != nil { - debriderStatus = debrider.IsConnected() - } - - downloaderStatus := false - downloader := downloaders.Get() - if downloader != nil { - downloaderStatus = downloader.IsConnected() - } - - var list = map[string]interface{}{ - "debrider_connected": debriderStatus, - "downloader_connected": downloaderStatus, - } - body, _ := json.Marshal(list) - w.Write(body) -} diff --git a/server/api/websocket.go b/server/api/websocket.go deleted file mode 100644 index fd98c7e..0000000 --- a/server/api/websocket.go +++ /dev/null @@ -1,57 +0,0 @@ -package api - -import ( - "encoding/json" - "github.com/gorilla/websocket" - "net/http" - "sync" - "holerr/core/log" -) - -var connectionPool = struct { - sync.RWMutex - connections map[*websocket.Conn]struct{} -}{ - connections: make(map[*websocket.Conn]struct{}), -} - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - -func Websocket(w http.ResponseWriter, req *http.Request) { - upgrader.CheckOrigin = func(r *http.Request) bool { return true } - con, err := upgrader.Upgrade(w, req, nil) - if err != nil { - log.Error(err) - } - connectionPool.Lock() - connectionPool.connections[con] = struct{}{} - connectionPool.Unlock() -} - -func WebsocketBroadcast(action string, payload interface{}) { - message, _ := json.Marshal(map[string]interface{}{ - "action": action, - "payload": payload, - }) - - connectionPool.Lock() - defer connectionPool.Unlock() - for connection := range connectionPool.connections { - if err := connection.WriteMessage(websocket.TextMessage, message); err != nil { - delete(connectionPool.connections, connection) - log.Error(err) - } - } -} - -func contains(s []*websocket.Conn, e *websocket.Conn) bool { - for _, ws := range s { - if ws == e { - return true - } - } - return false -} diff --git a/server/core/config/apikey.go b/server/core/config/apikey.go deleted file mode 100644 index e8feaa8..0000000 --- a/server/core/config/apikey.go +++ /dev/null @@ -1,26 +0,0 @@ -package config - -import ( - "github.com/spf13/viper" - "holerr/core/tools/placeholder" -) - -const ApiKeyPlaceholder = "%holerr-api-key-placeholder%" - -func GetApiKey() string { - return viper.GetString(ConfKeyApiKey) -} - -func SetApiKey(apiKey string) { - viper.Set(ConfKeyApiKey, apiKey) - viper.WriteConfig() - - replacer := placeholder.Replacer{ - Placeholder: ApiKeyPlaceholder, - Replacement: apiKey, - IsURLPath: false, - } - placeholder.SetReplacer(replacer) - - placeholder.ReplaceInFiles(GetPublicDir()) -} diff --git a/server/core/config/appVersion.go b/server/core/config/appVersion.go deleted file mode 100644 index 8a98060..0000000 --- a/server/core/config/appVersion.go +++ /dev/null @@ -1,10 +0,0 @@ -package config - -import "os" - -func AppVersion() string { - if os.Getenv("APP_VERSION") == "" { - return "local" - } - return os.Getenv("APP_VERSION") -} diff --git a/server/core/config/basepath.go b/server/core/config/basepath.go deleted file mode 100644 index e354f85..0000000 --- a/server/core/config/basepath.go +++ /dev/null @@ -1,26 +0,0 @@ -package config - -import ( - "github.com/spf13/viper" - "holerr/core/tools/placeholder" -) - -const BasePathPlaceholder = "%holerr-base-path-placeholder%" - -func GetBasePath() string { - return viper.GetString(ConfKeyBasePath) -} - -func SetBasePath(base_path string) { - viper.Set(ConfKeyBasePath, base_path) - viper.WriteConfig() - - replacer := placeholder.Replacer{ - Placeholder: BasePathPlaceholder, - Replacement: base_path, - IsURLPath: true, - } - placeholder.SetReplacer(replacer) - - placeholder.ReplaceInFiles(GetPublicDir()) -} diff --git a/server/core/config/config.go b/server/core/config/config.go deleted file mode 100644 index a5192e6..0000000 --- a/server/core/config/config.go +++ /dev/null @@ -1,84 +0,0 @@ -package config - -import ( - "fmt" - "holerr/core/tools/placeholder" - "log" - "os" - "path" - "path/filepath" - "runtime" - - "github.com/spf13/viper" -) - -const ConfKeyApiKey = "api_key" -const ConfKeyBasePath = "base_path" -const ConfKeyDebriders = "debriders" -const ConfKeyDebug = "debug" -const ConfKeyAppVersion = "app_version" -const ConfKeyIsInDocker = "is_in_docker" -const ConfKeyDownloaders = "downloaders" -const ConfKeyPresets = "presets" - -func InitFromFile() { - log.Println("Reading configuration") - - confFile := fmt.Sprintf(`%s/config.json`, GetDataDir()) - _, err := os.Stat(confFile) - if os.IsNotExist(err) { - log.Println("Configuration file does not exists, creating an empty one") - file, err := os.Create(confFile) - if err != nil { - log.Fatal(err) - } - file.WriteString("{}") - defer file.Close() - } - - viper.SetConfigFile(confFile) - - viper.SetDefault(ConfKeyBasePath, "/") - - err = viper.ReadInConfig() - if err != nil { - panic(err) - } - - publicDir := GetPublicDir() - os.MkdirAll(publicDir, os.ModePerm) - - basePathReplacer := placeholder.Replacer{ - Placeholder: BasePathPlaceholder, - Replacement: GetBasePath(), - IsURLPath: true, - } - placeholder.SetReplacer(basePathReplacer) - - apiKeyReplacer := placeholder.Replacer{ - Placeholder: ApiKeyPlaceholder, - Replacement: GetApiKey(), - IsURLPath: false, - } - placeholder.SetReplacer(apiKeyReplacer) - - placeholder.ReplaceInFiles(publicDir) - - createPresetDirs() -} - -func GetServerDir() (string, error) { - _, currentFilePath, _, _ := runtime.Caller(0) - currentFileDir := path.Dir(currentFilePath) - return filepath.Abs(fmt.Sprintf(`%s/../..`, currentFileDir)) -} - -func GetPublicDir() string { - dir, _ := GetServerDir() - return filepath.Clean(fmt.Sprintf(`%s/../public`, dir)) -} - -func GetDataDir() string { - dir, _ := GetServerDir() - return filepath.Clean(fmt.Sprintf(`%s/../data`, dir)) -} diff --git a/server/core/config/debriders.go b/server/core/config/debriders.go deleted file mode 100644 index 85a075c..0000000 --- a/server/core/config/debriders.go +++ /dev/null @@ -1,26 +0,0 @@ -package config - -import ( - "errors" - "github.com/spf13/viper" - "reflect" -) - -func GetDebriders() (Debriders, error) { - if viper.IsSet(ConfKeyDebriders) { - debriders := Debriders{} - err := viper.UnmarshalKey(ConfKeyDebriders, &debriders) - if err == nil { - return debriders, nil - } - } - return Debriders{}, errors.New("No debrider set") -} - -func GetRealDebrid() (RealDebrid, error) { - debriders, err := GetDebriders() - if err == nil && !(reflect.DeepEqual(debriders.RealDebrid, RealDebrid{})) { - return debriders.RealDebrid, nil - } - return RealDebrid{}, errors.New("Real-Debrid not set") -} diff --git a/server/core/config/debug.go b/server/core/config/debug.go deleted file mode 100644 index b9a43c1..0000000 --- a/server/core/config/debug.go +++ /dev/null @@ -1,9 +0,0 @@ -package config - -import ( - "github.com/spf13/viper" -) - -func IsDebug() bool { - return viper.GetBool(ConfKeyDebug) -} diff --git a/server/core/config/downloaders.go b/server/core/config/downloaders.go deleted file mode 100644 index 44afe63..0000000 --- a/server/core/config/downloaders.go +++ /dev/null @@ -1,26 +0,0 @@ -package config - -import ( - "errors" - "github.com/spf13/viper" - "reflect" -) - -func GetDownloaders() (Downloaders, error) { - if viper.IsSet(ConfKeyDownloaders) { - downloaders := Downloaders{} - err := viper.UnmarshalKey(ConfKeyDownloaders, &downloaders) - if err == nil { - return downloaders, nil - } - } - return Downloaders{}, errors.New("No downloader set") -} - -func GetSynologyDownloadStation() (SynologyDownloadStation, error) { - downloaders, err := GetDownloaders() - if err == nil && !(reflect.DeepEqual(downloaders.SynologyDownloadStation, SynologyDownloadStation{})) { - return downloaders.SynologyDownloadStation, nil - } - return SynologyDownloadStation{}, errors.New("Synology downloader not set") -} diff --git a/server/core/config/isInDocker.go b/server/core/config/isInDocker.go deleted file mode 100644 index be6d819..0000000 --- a/server/core/config/isInDocker.go +++ /dev/null @@ -1,7 +0,0 @@ -package config - -import "os" - -func IsInDocker() bool { - return os.Getenv("IS_IN_DOCKER") == "1" -} diff --git a/server/core/config/model.go b/server/core/config/model.go deleted file mode 100644 index edfbd81..0000000 --- a/server/core/config/model.go +++ /dev/null @@ -1,37 +0,0 @@ -package config - -type Config struct { - Debug *bool `mapstructure:"debug"json:"debug"` - ApiKey string `mapstructure:"api_key"json:"api_key"` - BasePath string `mapstructure:"base_path"json:"base_path"` - Debriders Debriders `mapstructure:"debriders"json:"debriders"` - Downloaders Downloaders `mapstructure:"downloaders"json:"downloaders"` - Presets []Preset `mapstructure:"presets"json:"downloaders"` -} - -type Debriders struct { - RealDebrid RealDebrid `mapstructure:"real_debrid"json:"real_debrid"` -} - -type RealDebrid struct { - ApiKey string `mapstructure:"api_key"json:"api_key"` -} - -type Downloaders struct { - SynologyDownloadStation SynologyDownloadStation `mapstructure:"synology_download_station"json:"synology_download_station"` -} - -type SynologyDownloadStation struct { - Endpoint string `mapstructure:"endpoint"json:"endpoint"` - Username string `mapstructure:"username"json:"username"` - Password string `mapstructure:"password"json:"password"` -} - -type Preset struct { - Name string `mapstructure:"name"json:"name"` - WatchDir string `mapstructure:"watch_dir"json:"watch_dir"` - OutputDir string `mapstructure:"output_dir"json:"output_dir"` - CreateSubDir *bool `mapstructure:"create_sub_dir"json:"create_sub_dir"` - FileExtensions []string `mapstructure:"file_extensions"json:"file_extensions"` - MinFileSize *int `mapstructure:"min_file_size"json:"min_file_size"` -} diff --git a/server/core/config/presets.go b/server/core/config/presets.go deleted file mode 100644 index cc03743..0000000 --- a/server/core/config/presets.go +++ /dev/null @@ -1,113 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "os" - "path/filepath" - - "github.com/imdario/mergo" - "github.com/spf13/viper" -) - -func GetPresets() ([]Preset, bool) { - if viper.IsSet(ConfKeyPresets) { - presets := []Preset{} - err := viper.UnmarshalKey(ConfKeyPresets, &presets) - if err == nil { - return presets, false - } - } - return []Preset{}, true -} - -func GetPresetByPath(filePath string) (Preset, error) { - torrentDir := filepath.Dir(filePath) - presets, _ := GetPresets() - dataDir := GetDataDir() - for _, a := range presets { - presetDir := filepath.Clean(fmt.Sprintf(`%s/../data/%s`, dataDir, a.WatchDir)) - if presetDir == torrentDir { - return a, nil - } - } - return Preset{}, errors.New("No preset found for path " + filePath) -} - -func GetPresetByName(name string) (Preset, error) { - presets, _ := GetPresets() - for _, a := range presets { - if a.Name == name { - return a, nil - } - } - return Preset{}, errors.New("No preset found for name " + name) -} - -func AddPreset(preset Preset) error { - _, err := GetPresetByName(preset.Name) - if err == nil { - return errors.New("Preset " + preset.Name + " already exists") - } - - presets, _ := GetPresets() - presets = append(presets, preset) - viper.Set(ConfKeyPresets, presets) - createPresetDirs() - viper.WriteConfig() - return nil -} - -func UpdatePreset(name string, preset Preset) error { - _, err := GetPresetByName(name) - if err != nil { - return err - } - - presets, _ := GetPresets() - for i, a := range presets { - if a.Name == name { - mergo.Merge(&presets[i], preset, mergo.WithOverride) - // Fix falsy values with mergo - if preset.CreateSubDir != nil { - presets[i].CreateSubDir = preset.CreateSubDir - } - if preset.MinFileSize != nil { - presets[i].MinFileSize = preset.MinFileSize - } - if preset.FileExtensions == nil { - presets[i].FileExtensions = nil - } - } - } - - viper.Set(ConfKeyPresets, presets) - createPresetDirs() - viper.WriteConfig() - return nil -} - -func RemovePreset(name string) { - presets, _ := GetPresets() - index := -1 - for i, a := range presets { - if a.Name == name { - index = i - } - } - - if index >= 0 { - presets = append(presets[:index], presets[index+1:]...) - viper.Set(ConfKeyPresets, presets) - viper.WriteConfig() - } -} - -func createPresetDirs() { - presets, _ := GetPresets() - dataDir := GetDataDir() - for _, a := range presets { - presetDir := filepath.Clean(fmt.Sprintf(`%s/../data/%s`, dataDir, a.WatchDir)) - os.MkdirAll(presetDir, os.ModePerm) - } -} diff --git a/server/core/db/db.go b/server/core/db/db.go deleted file mode 100644 index c1546d3..0000000 --- a/server/core/db/db.go +++ /dev/null @@ -1,39 +0,0 @@ -package db - -import ( - "fmt" - scribble "github.com/nanobox-io/golang-scribble" - "os" - "path/filepath" - "holerr/core/config" - "holerr/core/log" -) - -var db *scribble.Driver -var inited bool = false - -func Get() *scribble.Driver { - if !isInited() { - log.Info("Initing database...") - createDbDirs() - - dbDir := fmt.Sprintf(`%s/db`, config.GetDataDir()) - ndb, err := scribble.New(dbDir, nil) - if err != nil { - log.Fatal(err) - } - db = ndb - inited = true - } - return db -} - -func isInited() bool { - return inited -} - -func createDbDirs() { - dbDir := fmt.Sprintf(`%s/db`, config.GetDataDir()) - newpath := filepath.Join(dbDir, "downloads") - os.MkdirAll(newpath, os.ModePerm) -} diff --git a/server/core/db/model.go b/server/core/db/model.go deleted file mode 100644 index 622401d..0000000 --- a/server/core/db/model.go +++ /dev/null @@ -1,56 +0,0 @@ -package db - -import ( - "time" - "holerr/debriders/debrider" -) - -var DownloadStatus = map[string]int{ - "TORRENT_FOUND": 0, - "TORRENT_SENT_TO_DEBRIDER": 1, - "DEBRIDER_DOWNLOADING": 2, - "DEBRIDER_DOWNLOADED": 3, - "SENT_TO_DOWNLOADER": 4, - "DOWNLOADER_DOWNLOADING": 5, - "DOWNLOADER_DOWNLOADED": 6, - "ERROR_NO_FILES_FOUND": 100, - "ERROR_DEBRIDER": 101, - "ERROR_DOWNLOADER": 102, -} - -var DownloadStatusDetail = map[int]string{ - DownloadStatus["TORRENT_FOUND"]: "Torrent file found on drive", - DownloadStatus["TORRENT_SENT_TO_DEBRIDER"]: "Torrent sent to debrider for download", - DownloadStatus["DEBRIDER_DOWNLOADING"]: "Debrider is downloading files, check on debrider", - DownloadStatus["DEBRIDER_DOWNLOADED"]: "Debrider download is terminated", - DownloadStatus["SENT_TO_DOWNLOADER"]: "Debrided files sent to downloader", - DownloadStatus["DOWNLOADER_DOWNLOADING"]: "Downloader is downloading the files", - DownloadStatus["DOWNLOADER_DOWNLOADED"]: "Downloader task is terminated", - DownloadStatus["ERROR_NO_FILES_FOUND"]: "No files found", - DownloadStatus["ERROR_DEBRIDER"]: "Debrider error", - DownloadStatus["ERROR_DOWNLOADER"]: "Downloader error", -} - -type Download struct { - Id string `json:"id"` - Title string `json:"title"` - Preset string `json:"preset"` - Status int `json:"status"` - StatusDetails string `json:"status_details"` - TorrentInfo debrider.TorrentInfo `json:"torrent_info"` - DownloadInfo DownloadInfo `json:"download_info"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type DownloadInfo struct { - Progress int `json:"progress"` - Bytes int `json:"bytes"` - Tasks map[string]DownloadInfoTask `json:"tasks"` -} - -type DownloadInfoTask struct { - Id string `json:"id"` - Status int `json:"status"` - BytesDownloaded int `json:"bytes_downloaded"` -} diff --git a/server/core/log/log.go b/server/core/log/log.go deleted file mode 100644 index e1be81b..0000000 --- a/server/core/log/log.go +++ /dev/null @@ -1,24 +0,0 @@ -package log - -import ( - "holerr/core/config" - "log" -) - -func Info(msg ...interface{}) { - if config.IsDebug() { - log.Println(msg) - } -} - -func Error(msg ...interface{}) { - log.Println(msg) -} - -func Fatal(msg ...interface{}) { - log.Fatalln(msg) -} - -func Panic(msg ...interface{}) { - panic(msg) -} diff --git a/server/core/tools/file/file.go b/server/core/tools/file/file.go deleted file mode 100644 index 51773b0..0000000 --- a/server/core/tools/file/file.go +++ /dev/null @@ -1,22 +0,0 @@ -package file - -import ( - "io" - "os" -) - -func Copy(src string, dst string) (int64, error) { - source, err := os.Open(src) - if err != nil { - return 0, err - } - defer source.Close() - - destination, err := os.Create(dst) - if err != nil { - return 0, err - } - defer destination.Close() - nBytes, err := io.Copy(destination, source) - return nBytes, err -} diff --git a/server/core/tools/placeholder/model.go b/server/core/tools/placeholder/model.go deleted file mode 100644 index f5b9753..0000000 --- a/server/core/tools/placeholder/model.go +++ /dev/null @@ -1,7 +0,0 @@ -package placeholder - -type Replacer struct { - Placeholder string `mapstructure:"placeholder"` - Replacement string `mapstructure:"replacement"` - IsURLPath bool `mapstructure:"IsURLPath"` -} diff --git a/server/core/tools/placeholder/placeholder.go b/server/core/tools/placeholder/placeholder.go deleted file mode 100644 index 7eb98bd..0000000 --- a/server/core/tools/placeholder/placeholder.go +++ /dev/null @@ -1,94 +0,0 @@ -package placeholder - -import ( - "holerr/core/tools/file" - "io/ioutil" - "log" - "os" - "path/filepath" - "strings" -) - -var replacers map[string]Replacer = map[string]Replacer{} - -func SetReplacer(replacer Replacer) { - replacers[replacer.Placeholder] = replacer -} - -func ReplaceInFiles(rootDirectory string) { - err := filepath.Walk(rootDirectory, - func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() && !strings.HasSuffix(path, ".original") && containsPlaceHolders(path) { - file.Copy(path, path+".original") - } - return nil - }) - if err != nil { - log.Fatal(err) - } - - err = filepath.Walk(rootDirectory, - func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if strings.HasSuffix(path, ".original") { - err = replacePlaceHolders(path) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - log.Fatal(err) - } -} - -func containsPlaceHolders(path string) bool { - b, err := ioutil.ReadFile(path) - if err != nil { - log.Fatal(err) - } - contains := false - for _, replacer := range replacers { - contains = contains || strings.Contains(string(b), replacer.Placeholder) - } - - return contains -} - -func replacePlaceHolders(path string) error { - b, err := ioutil.ReadFile(path) - if err != nil { - log.Fatal(err) - } - - result := string(b) - - for _, replacer := range replacers { - fullReplace := replacer.Replacement - if replacer.IsURLPath && replacer.Replacement != "/" { - fullReplace = replacer.Replacement + "/" - } - - if replacer.IsURLPath { - result = strings.Replace(result, "/"+replacer.Placeholder+"/", fullReplace, -1) - result = strings.Replace(result, "/"+replacer.Placeholder, replacer.Replacement, -1) - } else { - result = strings.Replace(result, replacer.Placeholder, replacer.Replacement, -1) - } - } - - outputPath := strings.TrimSuffix(path, ".original") - destination, err := os.Create(outputPath) - if err != nil { - return err - } - defer destination.Close() - destination.WriteString(result) - return nil -} diff --git a/server/debriders/debrider/debrider.go b/server/debriders/debrider/debrider.go deleted file mode 100644 index 75dfce6..0000000 --- a/server/debriders/debrider/debrider.go +++ /dev/null @@ -1,13 +0,0 @@ -package debrider - -type Debrider interface { - GetName() string - IsConnected() bool - Me() (string, error) - GetSlotsAvailable() (int, error) - GetActiveDownloads() - AddTorrent(torrent string) (string, error) - GetTorrentInfos(torrentId string) (TorrentInfo, error) - SelectFiles(torrentId string, files []string) error - DeleteTorrent(torrentId string) error -} diff --git a/server/debriders/debrider/model.go b/server/debriders/debrider/model.go deleted file mode 100644 index d57c21e..0000000 --- a/server/debriders/debrider/model.go +++ /dev/null @@ -1,32 +0,0 @@ -package debrider - -var TorrentStatus = map[string]string{ - "MAGNET_ERROR": "magnet_error", - "MAGNET_CONVERSION": "magnet_conversion", - "WAITING_FILES_SELECTION": "waiting_files_selection", - "QUEUED": "queued", - "DOWNLOADING": "downloading", - "DOWNLOADED": "downloaded", - "ERROR": "error", - "VIRUS": "virus", - "COMPRESSING": "compressing", - "UPLOADING": "uploading", - "DEAD": "dead", -} - -type File struct { - Id int `json:"id"` - Path string `json:"path"` // Path to the file inside the torrent, starting with "/" - Bytes int `json:"bytes"` - Selected int `json:"selected"` // 0 or 1 -} - -type TorrentInfo struct { - Id string `json:"id"` - Filename string `json:"filename"` - Bytes int `json:"bytes"` // Size of selected files only - Progress int `json:"progress"` // Possible values: 0 to 100 - Status string `json:"status"` // Current status of the torrent - Files []File `json:"files"` - Links []string `json:"links"` -} diff --git a/server/debriders/debriders.go b/server/debriders/debriders.go deleted file mode 100644 index 8a88602..0000000 --- a/server/debriders/debriders.go +++ /dev/null @@ -1,19 +0,0 @@ -package debriders - -import ( - "holerr/core/config" - "holerr/debriders/debrider" - "holerr/debriders/realdebrid" -) - -var d debrider.Debrider = nil - -func Get() debrider.Debrider { - if d == nil { - _, err := config.GetRealDebrid() - if err == nil { - d = realdebrid.New() - } - } - return d -} diff --git a/server/debriders/realdebrid/model.go b/server/debriders/realdebrid/model.go deleted file mode 100644 index c9ff938..0000000 --- a/server/debriders/realdebrid/model.go +++ /dev/null @@ -1,38 +0,0 @@ -package realdebrid - -import "holerr/debriders/debrider" - -type Profile struct { - Id int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Points int `json:"points"` - Locale string `json:"locale"` - Avatar string `json:"avatar"` - Type string `json:"type"` - Premium int `json:"premium"` - Expiration string `json:"expiration"` -} - -type ActiveCount struct { - Nb int `json:"nb"` - Limit int `json:"limit"` -} - -type Torrent struct { - Id string `json:"id"` - Uri string `json:"uri"` -} - -type RealDebridTorrentInfo struct { - debrider.TorrentInfo - OriginalFilename string `json:"original_filename"` // Original name of the torrent - Hash string `json:"hash"` // SHA1 Hash of the torrent - OriginalBytes int `json:"original_bytes"` // Total size of the torrent - Host string `json:"host"` // Host main domain - Split int `json:"split"` // Split size of links - Added string `json:"added"` // jsonDate - Ended string `json:"ended"` // !! Only present when finished, jsonDate - Speed int `json:"speed"` // !! Only present in "downloading", "compressing", "uploading" status - Seeders int `json:"seeders"` // !! Only present in "downloading", "magnet_conversion" status -} diff --git a/server/debriders/realdebrid/realdebrid.go b/server/debriders/realdebrid/realdebrid.go deleted file mode 100644 index 13ad5d7..0000000 --- a/server/debriders/realdebrid/realdebrid.go +++ /dev/null @@ -1,142 +0,0 @@ -package realdebrid - -import ( - "encoding/json" - "errors" - "github.com/monaco-io/request" - "holerr/core/config" - "holerr/debriders/debrider" - "io/ioutil" - "net/url" - "strings" -) - -const ENDPOINT = "https://api.real-debrid.com/rest/1.0" -const MAXIMUM_ACTIVE_DOWNLOADS = 20 - -type RealDebrid struct { - debrider.Debrider -} - -func New() RealDebrid { - return RealDebrid{} -} - -func prepareClient(client *request.Client) { - client.URL = ENDPOINT + client.URL - - if client.Method == "" { - client.Method = "GET" - } - - if client.Header == nil { - client.Header = map[string]string{} - } - - if client.Header["Authorization"] == "" { - realDebridConf, _ := config.GetRealDebrid() - client.Header["Authorization"] = "Bearer " + realDebridConf.ApiKey - } -} - -func (r RealDebrid) GetName() string { - return "Real-Debrid" -} - -func (r RealDebrid) IsConnected() bool { - me, err := r.Me() - return err == nil && me != "" -} - -func (r RealDebrid) Me() (string, error) { - client := request.Client{ - URL: "/user", - } - prepareClient(&client) - resp, err := client.Do() - if err != nil || resp.Code != 200 { - return "", err - } - var obj Profile - jsonError := json.Unmarshal(resp.Data, &obj) - return obj.Username, jsonError -} - -func (r RealDebrid) GetSlotsAvailable() (int, error) { - client := request.Client{ - URL: "/torrents/activeCount", - } - prepareClient(&client) - resp, err := client.Do() - if err != nil || resp.Code != 200 { - return 0, err - } - var obj ActiveCount - jsonError := json.Unmarshal(resp.Data, &obj) - return MAXIMUM_ACTIVE_DOWNLOADS - obj.Nb, jsonError -} - -func (r RealDebrid) GetActiveDownloads() { - // TODO: get active downloads -} - -func (r RealDebrid) AddTorrent(torrent string) (string, error) { - content, err := ioutil.ReadFile(torrent) - if err != nil { - return "", err - } - client := request.Client{ - URL: "/torrents/addTorrent", - Method: "PUT", - Body: content, - } - prepareClient(&client) - resp, err := client.Do() - if err != nil || resp.Code != 201 { - return "", err - } - var obj Torrent - jsonError := json.Unmarshal(resp.Data, &obj) - return obj.Id, jsonError -} - -func (r RealDebrid) GetTorrentInfos(torrentId string) (debrider.TorrentInfo, error) { - client := request.Client{ - URL: "/torrents/info/" + torrentId, - } - prepareClient(&client) - resp, err := client.Do() - if err != nil { - return debrider.TorrentInfo{}, err - } - if resp.Code != 200 { - return debrider.TorrentInfo{}, errors.New("Error while getting torrent info") - } - var obj debrider.TorrentInfo - jsonError := json.Unmarshal(resp.Data, &obj) - return obj, jsonError -} - -func (r RealDebrid) SelectFiles(torrentId string, files []string) error { - data := url.Values{} - data.Set("files", strings.Join(files, ",")) - client := request.Client{ - URL: "/torrents/selectFiles/" + torrentId, - Method: "POST", - ContentType: request.ApplicationXWwwFormURLEncoded, - Body: []byte(data.Encode()), - } - prepareClient(&client) - _, err := client.Do() - return err -} - -func (r RealDebrid) DeleteTorrent(torrentId string) error { - client := request.Client{ - URL: "/torrents/delete/" + torrentId, - Method: "DELETE", - } - prepareClient(&client) - _, err := client.Do() - return err -} diff --git a/server/downloaders/downloader/downloader.go b/server/downloaders/downloader/downloader.go deleted file mode 100644 index e936413..0000000 --- a/server/downloaders/downloader/downloader.go +++ /dev/null @@ -1,11 +0,0 @@ -package downloader - -import "holerr/core/config" - -type Downloader interface { - GetName() string - IsConnected() bool - AddDownload(uri string, name string, preset config.Preset) (string, error) - GetTaskStatus(id string) (int, int, error) // Status, SizeDownloaded, error - DeleteDownload(id string) error -} diff --git a/server/downloaders/downloader/model.go b/server/downloaders/downloader/model.go deleted file mode 100644 index df64a97..0000000 --- a/server/downloaders/downloader/model.go +++ /dev/null @@ -1,14 +0,0 @@ -package downloader - -var DownloadStatus = map[string]string{ - "WAITING": "waiting", - "DOWNLOADING": "downloading", - "PAUSED": "paused", - "FINISHING": "finishing", - "FINISHED": "finished", - "HASH_CHECKING": "hash_checking", - "SEEDING": "seeding", - "FILEHOSTING_WAITING": "filehosting_waiting", - "EXTRACTING": "extracting", - "ERROR": "error", -} diff --git a/server/downloaders/downloaders.go b/server/downloaders/downloaders.go deleted file mode 100644 index 1f534be..0000000 --- a/server/downloaders/downloaders.go +++ /dev/null @@ -1,19 +0,0 @@ -package downloaders - -import ( - "holerr/core/config" - "holerr/downloaders/downloader" - "holerr/downloaders/synologyDownloadStation" -) - -var d downloader.Downloader = nil - -func Get() downloader.Downloader { - if d == nil { - _, err := config.GetSynologyDownloadStation() - if err == nil { - d = synologyDownloadStation.New() - } - } - return d -} diff --git a/server/downloaders/synologyDownloadStation/model.go b/server/downloaders/synologyDownloadStation/model.go deleted file mode 100644 index 3c15e99..0000000 --- a/server/downloaders/synologyDownloadStation/model.go +++ /dev/null @@ -1,68 +0,0 @@ -package synologyDownloadStation - -type AuthData struct { - Sid string `json:"sid"` -} - -type Auth struct { - Status - Data AuthData `json:"data"` -} - -type Task struct { - Id string `json:"id"` - Type string `json:"type"` - Username string `json:"username"` - Title string `json:"title"` - Size int `json:"size"` - Status string `json:"status"` - Additional TaskAdditional `json:"additional"` -} - -type TaskAdditional struct { - Detail TaskDetail `json:"detail"` - File []TaskFile `json:"file"` - Transfer TaskTransfer `json:"transfer"` -} - -type TaskDetail struct { - CreateTime int `json:"create_time"` - Destination string `json:"destination"` - Priority string `json:"priority"` - Uri string `json:"uri"` -} - -type TaskFile struct { - Filename string `json:"filename"` - Priority string `json:"priority"` - Size string `json:"size"` - SizeDownloaded string `json:"size_downloaded"` -} - -type TaskTransfer struct { - DownloadedPieces int `json:"downloaded_pieces"` - SizeDownloaded int `json:"size_downloaded"` - SizeUploaded int `json:"size_uploaded"` - SpeedDownload int `json:"speed_download"` - SpeedUpload int `json:"speed_upload"` -} - -type Tasks struct { - Status - Data TasksData `json:"data"` -} - -type TasksData struct { - Total int `json:"total"` - Offset int `json:"offset"` - Tasks []Task `json:"tasks"` -} - -type Error struct { - Code int `json:"code"` -} - -type Status struct { - Error Error `json:"error"` - Success bool `json:"success"` -} diff --git a/server/downloaders/synologyDownloadStation/synologyDownloadStation.go b/server/downloaders/synologyDownloadStation/synologyDownloadStation.go deleted file mode 100644 index d79e0f1..0000000 --- a/server/downloaders/synologyDownloadStation/synologyDownloadStation.go +++ /dev/null @@ -1,322 +0,0 @@ -package synologyDownloadStation - -import ( - "encoding/json" - "errors" - "holerr/core/config" - "holerr/core/db" - "holerr/core/log" - "holerr/downloaders/downloader" - "io/ioutil" - "net/http" - "net/url" - "strings" - - "github.com/monaco-io/request" -) - -type SynologyDownloadStation struct { - downloader.Downloader -} - -func New() SynologyDownloadStation { - s := SynologyDownloadStation{} - sid, err := connect("DownloadStation") - if err != nil { - log.Error(err) - } else { - log.Info("DownloadStation session ID: " + sid) - } - return s -} - -func (s SynologyDownloadStation) GetName() string { - return "Synology Download Station" -} - -func (s SynologyDownloadStation) IsConnected() bool { - sid, err := connect("DownloadStation") - return err == nil && sid != "" -} - -func (s SynologyDownloadStation) AddDownload(uri string, name string, preset config.Preset) (string, error) { - sid, err := connect("DownloadStation") - if err != nil { - log.Error(err) - return "", err - } - - req, err := http.NewRequest(http.MethodGet, getApiUrl("/DownloadStation/task.cgi"), nil) - if err != nil { - return "", err - } - - q := req.URL.Query() - q.Add("api", "SYNO.DownloadStation.Task") - q.Add("version", "1") - q.Add("method", "create") - q.Add("uri", uri) - q.Add("_sid", sid) - - destination := preset.OutputDir - if destination != "" { - if preset.CreateSubDir != nil && *preset.CreateSubDir { - err := createOutputDir(destination, name) - if err != nil { - return "", err - } - destination = destination + "/" + name - } - q.Add("destination", destination) - } - req.URL.RawQuery = q.Encode() - - statusCode, body, err := makeRequest(req) - if err != nil { - return "", err - } - if statusCode != 200 { - return "", errors.New("Could not add download") - } - var obj Status - jsonError := json.Unmarshal(body, &obj) - if jsonError != nil { - return "", jsonError - } - if !obj.Success { - return "", errors.New("Error while adding download") - } - - return getId(uri) -} - -func (s SynologyDownloadStation) GetTaskStatus(id string) (int, int, error) { - sid, err := connect("DownloadStation") - if err != nil { - log.Error(err) - return 0, 0, err - } - client := request.Client{ - URL: "/DownloadStation/task.cgi", - Params: map[string]string{ - "api": "SYNO.DownloadStation.Task", - "version": "1", - "method": "getinfo", - "additional": "transfer", - "id": id, - "_sid": sid, - }, - } - prepareClient(&client) - resp, err := client.Do() - if err != nil { - return 0, 0, err - } - if resp.Code != 200 { - return 0, 0, errors.New("Could retrieve download info") - } - - var obj Tasks - jsonError := json.Unmarshal(resp.Data, &obj) - if jsonError != nil { - return 0, 0, jsonError - } - if !obj.Success { - return 0, 0, errors.New("Error while get download info") - } - - downloaderStatusMapping := map[string]int{ - downloader.DownloadStatus["WAITING"]: db.DownloadStatus["DOWNLOADER_DOWNLOADING"], - downloader.DownloadStatus["DOWNLOADING"]: db.DownloadStatus["DOWNLOADER_DOWNLOADING"], - downloader.DownloadStatus["PAUSED"]: db.DownloadStatus["DOWNLOADER_DOWNLOADING"], - downloader.DownloadStatus["FINISHING"]: db.DownloadStatus["DOWNLOADER_DOWNLOADING"], - downloader.DownloadStatus["FINISHED"]: db.DownloadStatus["DOWNLOADER_DOWNLOADED"], - downloader.DownloadStatus["HASH_CHECKING"]: db.DownloadStatus["DOWNLOADER_DOWNLOADING"], - downloader.DownloadStatus["SEEDING"]: db.DownloadStatus["DOWNLOADER_DOWNLOADED"], - downloader.DownloadStatus["EXTRACTING"]: db.DownloadStatus["DOWNLOADER_DOWNLOADING"], - downloader.DownloadStatus["ERROR"]: db.DownloadStatus["ERROR_DOWNLOADER"], - } - return downloaderStatusMapping[obj.Data.Tasks[0].Status], obj.Data.Tasks[0].Additional.Transfer.SizeDownloaded, nil -} - -func (s SynologyDownloadStation) DeleteDownload(id string) error { - sid, err := connect("DownloadStation") - if err != nil { - log.Error(err) - return err - } - - client := request.Client{ - URL: "/DownloadStation/task.cgi", - Params: map[string]string{ - "api": "SYNO.DownloadStation.Task", - "version": "1", - "method": "delete", - "id": id, - "_sid": sid, - }, - } - prepareClient(&client) - resp, err := client.Do() - if err != nil { - return err - } - if resp.Code != 200 { - return errors.New("Could not delete download task") - } - - return nil -} - -func prepareClient(client *request.Client) { - client.URL = getApiUrl(client.URL) - - if client.Method == "" { - client.Method = "GET" - } -} - -func getApiUrl(path string) string { - synoCfg, _ := config.GetSynologyDownloadStation() - return synoCfg.Endpoint + "/webapi" + path -} - -func connect(session string) (string, error) { - synoCfg, _ := config.GetSynologyDownloadStation() - - req, err := http.NewRequest(http.MethodGet, getApiUrl("/auth.cgi"), nil) - if err != nil { - return "", err - } - - q := url.Values{} - q.Add("api", "SYNO.API.Auth") - q.Add("version", "3") - q.Add("method", "login") - q.Add("session", session) - q.Add("account", synoCfg.Username) - q.Add("passwd", synoCfg.Password) - q.Add("format", "sid") - req.URL.RawQuery = q.Encode() - - statusCode, body, err := makeRequest(req) - if err != nil { - return "", err - } - - if statusCode != 200 { - return "", errors.New("Could not login to " + session) - } - - var obj Auth - jsonError := json.Unmarshal(body, &obj) - if jsonError != nil { - return "", jsonError - } - if !obj.Success { - return "", errors.New("Error while login to " + session) - } - return obj.Data.Sid, nil -} - -func makeRequest(req *http.Request) (int, []byte, error) { - req.URL.RawQuery = strings.ReplaceAll(req.URL.RawQuery, "+", "%20") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return 0, []byte{}, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - return resp.StatusCode, body, err -} - -func getId(uri string) (string, error) { - sid, err := connect("DownloadStation") - if err != nil { - log.Error(err) - return "", err - } - - client := request.Client{ - URL: "/DownloadStation/task.cgi", - Params: map[string]string{ - "api": "SYNO.DownloadStation.Task", - "version": "1", - "method": "list", - "additional": "detail", - "_sid": sid, - }, - } - prepareClient(&client) - resp, err := client.Do() - if err != nil { - return "", err - } - if resp.Code != 200 { - return "", errors.New("Could not list downloads") - } - var obj Tasks - jsonError := json.Unmarshal(resp.Data, &obj) - if jsonError != nil { - return "", jsonError - } - if !obj.Success { - return "", errors.New("Error while search download id") - } - - for _, task := range obj.Data.Tasks { - if task.Additional.Detail.Uri == uri { - return task.Id, nil - } - } - - return "", errors.New("Download not found") -} - -func createOutputDir(parent string, name string) error { - sid, err := connect("FileStation") - if err != nil { - log.Error(err) - return err - } - - req, err := http.NewRequest(http.MethodGet, getApiUrl("/entry.cgi"), nil) - if err != nil { - return err - } - - folder_path := parent - if folder_path[0] != '/' { - folder_path = "/" + folder_path - } - - q := req.URL.Query() - q.Add("api", "SYNO.FileStation.CreateFolder") - q.Add("version", "2") - q.Add("method", "create") - q.Add("folder_path", folder_path) - q.Add("name", name) - q.Add("_sid", sid) - req.URL.RawQuery = q.Encode() - - statusCode, body, err := makeRequest(req) - if err != nil { - return err - } - if statusCode != 200 { - return errors.New("Could not create sub folder") - } - var obj Status - jsonError := json.Unmarshal(body, &obj) - if jsonError != nil { - return jsonError - } - if !obj.Success { - return errors.New("Error while creating sub folder") - } - - return nil -} diff --git a/server/go.mod b/server/go.mod deleted file mode 100644 index f1d6ea8..0000000 --- a/server/go.mod +++ /dev/null @@ -1,30 +0,0 @@ -module holerr - -go 1.17 - -require ( - github.com/go-chi/chi v1.5.1 - github.com/gorilla/websocket v1.4.2 - github.com/imdario/mergo v0.3.12 - github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect - github.com/monaco-io/request v1.0.5 - github.com/nanobox-io/golang-scribble v0.0.0-20190309225732-aa3e7c118975 - github.com/spf13/viper v1.9.0 -) - -require ( - github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/magiconair/properties v1.8.5 // indirect - github.com/mitchellh/mapstructure v1.4.2 // indirect - github.com/pelletier/go-toml v1.9.4 // indirect - github.com/spf13/afero v1.6.0 // indirect - github.com/spf13/cast v1.4.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.2.0 // indirect - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect - golang.org/x/text v0.3.6 // indirect - gopkg.in/ini.v1 v1.63.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) diff --git a/server/go.sum b/server/go.sum deleted file mode 100644 index c284dee..0000000 --- a/server/go.sum +++ /dev/null @@ -1,667 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w= -github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8= -github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/monaco-io/request v1.0.5 h1:QAJb5m1pCPZUGv3zzTZn7GlQI3q+uJWi7fH9QxDGbm4= -github.com/monaco-io/request v1.0.5/go.mod h1:EmggwHktBsbJmCgwZXqy7o0H1NNsAstQBWZrFVd3xtQ= -github.com/nanobox-io/golang-scribble v0.0.0-20190309225732-aa3e7c118975 h1:zm/Rb2OsnLWCY88Njoqgo4X6yt/lx3oBNWhepX0AOMU= -github.com/nanobox-io/golang-scribble v0.0.0-20190309225732-aa3e7c118975/go.mod h1:4Mct/lWCFf1jzQTTAaWtOI7sXqmG+wBeiBfT4CxoaJk= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= -github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= -gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/server/holerr/__main__.py b/server/holerr/__main__.py new file mode 100644 index 0000000..740eb08 --- /dev/null +++ b/server/holerr/__main__.py @@ -0,0 +1,46 @@ + +#------------------------------------------------------------------------------ +# Init +# Not called in __init__.py because of alembic importing holerr.database +#------------------------------------------------------------------------------ +from holerr.core import config + +config.load() + +from holerr.core.log import Log + +log = Log.get_logger(__name__) + +from holerr.core.config_repositories import PresetRepository + +PresetRepository.create_watch_directories() + +from holerr.debriders import debrider + +if not debrider.is_connected(): + log.info("Debrider not connected") + +from holerr.downloaders import downloader + +if not downloader.is_connected(): + log.info("Downloader not connected") + + +#------------------------------------------------------------------------------ +# Main itself +#------------------------------------------------------------------------------ +from holerr.tasks import worker +from holerr.api import server +import sys + +def main() -> int: + worker.start() + server.start() + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + worker.stop() diff --git a/server/holerr/api/__init__.py b/server/holerr/api/__init__.py new file mode 100644 index 0000000..b51791b --- /dev/null +++ b/server/holerr/api/__init__.py @@ -0,0 +1,23 @@ +from holerr.core import config +from .server import Server +from .routers import api_router + +from fastapi import HTTPException +from starlette.exceptions import HTTPException as StarletteHTTPException +from fastapi.staticfiles import StaticFiles + +server = Server() +server.app.include_router(api_router) + +# Serve the frontend +# https://stackoverflow.com/a/73552966 +class SpaStaticFiles(StaticFiles): + async def get_response(self, path: str, scope): + try: + return await super().get_response(path, scope) + except (HTTPException, StarletteHTTPException) as ex: + if ex.status_code == 404: + return await super().get_response("index.html", scope) + else: + raise ex +server.app.mount("", SpaStaticFiles(directory=config.public_dir, check_dir=False, html = True), name="Front") \ No newline at end of file diff --git a/server/holerr/api/routers/__init__.py b/server/holerr/api/routers/__init__.py new file mode 100644 index 0000000..cd62173 --- /dev/null +++ b/server/holerr/api/routers/__init__.py @@ -0,0 +1,12 @@ +from . import actions, constants, config, downloads, presets, status, websocket + +from fastapi import APIRouter, Depends + +api_router = APIRouter(prefix="/api") +api_router.include_router(actions.router) +api_router.include_router(constants.router) +api_router.include_router(config.router) +api_router.include_router(downloads.router) +api_router.include_router(presets.router) +api_router.include_router(status.router) +api_router.include_router(websocket.router) diff --git a/server/holerr/api/routers/actions.py b/server/holerr/api/routers/actions.py new file mode 100644 index 0000000..027343c --- /dev/null +++ b/server/holerr/api/routers/actions.py @@ -0,0 +1,63 @@ +from holerr.core.db import db +from holerr.database.repositories import DownloadRepository +from holerr.core.exceptions import NotFoundException +from .routers_models import Download, Magnet +from holerr.utils import torrent +from holerr.core.websockets import manager, Actions + +from typing import Annotated +from fastapi import APIRouter, HTTPException, status, File, Form +import tempfile + +router = APIRouter(prefix="/actions") + + +@router.post("/add_magnet", response_model=Download, tags=["Actions"]) +async def add_magnet(magnet: Magnet): + try: + session = db.new_session() + download = DownloadRepository(session).create_model_from_magnet(magnet.uri, magnet.preset) + session.refresh(download) + + await manager.broadcast(Actions["DOWNLOADS_NEW"], download) + + return download + except NotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e), + ) + except Exception as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +@router.post("/add_torrent", tags=["Actions"]) +async def add_torrent(file: Annotated[bytes, File()], preset: Annotated[str, Form()]): + try: + tmp_torrent = tempfile.NamedTemporaryFile() + tmp_torrent.write(file) + magnet_uri = torrent.get_magnet_link(tmp_torrent.name) + tmp_torrent.close() + session = db.new_session() + download = DownloadRepository(session).create_model_from_magnet(magnet_uri, preset) + session.refresh(download) + + await manager.broadcast(Actions["DOWNLOADS_NEW"], download) + + return download + except NotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e), + ) + except Exception as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +@router.post("/clean_downloaded", response_model=list[str], tags=["Actions"]) +async def clean_downloaded(): + session = db.new_session() + cleaned = DownloadRepository(session).clean_downloaded() + for download in cleaned: + manager.broadcast(Actions["DOWNLOADS_DELETE"], download) + return cleaned diff --git a/server/holerr/api/routers/config.py b/server/holerr/api/routers/config.py new file mode 100644 index 0000000..0396ced --- /dev/null +++ b/server/holerr/api/routers/config.py @@ -0,0 +1,27 @@ +from holerr.core import config +from holerr.core.config_models import Config +from holerr.core.log import Log +from .routers_models import PartialConfig +from holerr.debriders import debrider +from holerr.downloaders import downloader + +from fastapi import APIRouter + +router = APIRouter(prefix="/configuration") + +log = Log.get_logger(__name__) + + +@router.get("", response_model=Config, tags=["Configuration"]) +async def get_configuration(): + return config.raw + + +@router.patch("", response_model=Config, tags=["Configuration"]) +async def update_configuration(cfg: PartialConfig): + update_data = cfg.model_dump(exclude_unset=True) + config.update(update_data) + debrider.update() + downloader.update() + + return config.raw diff --git a/server/holerr/api/routers/constants.py b/server/holerr/api/routers/constants.py new file mode 100644 index 0000000..e70d417 --- /dev/null +++ b/server/holerr/api/routers/constants.py @@ -0,0 +1,12 @@ +from .routers_models import Constants +from holerr.database.models import DownloadStatus +from holerr.debriders.debrider_models import TorrentStatus + +from fastapi import APIRouter + +router = APIRouter(prefix="/constants") + + +@router.get("", response_model=Constants, tags=["Constants"]) +async def get_constants(): + return {"download_status": DownloadStatus, "torrent_status": TorrentStatus} diff --git a/server/holerr/api/routers/downloads.py b/server/holerr/api/routers/downloads.py new file mode 100644 index 0000000..0563146 --- /dev/null +++ b/server/holerr/api/routers/downloads.py @@ -0,0 +1,29 @@ +from holerr.core.db import db +from .routers_models import Download +from holerr.database.repositories import DownloadRepository +from holerr.core.websockets import manager, Actions + +from fastapi import APIRouter, HTTPException + +router = APIRouter(prefix="/downloads") + + +@router.get("", response_model=list[Download], tags=["Downloads"]) +async def list_downloads(): + session = db.new_session() + return DownloadRepository(session).get_all_models() + + +@router.delete("/{download_id}", response_model=Download, tags=["Downloads"]) +async def delete_download(download_id: str): + session = db.new_session() + download = DownloadRepository(session).get_model(download_id) + if download is None: + raise HTTPException(status_code=404, detail=f"Download {download_id} not found") + download.to_delete = True + session.commit() + session.refresh(download) + + await manager.broadcast(Actions["DOWNLOADS_UPDATE"], download) + + return download diff --git a/server/holerr/api/routers/presets.py b/server/holerr/api/routers/presets.py new file mode 100644 index 0000000..3382336 --- /dev/null +++ b/server/holerr/api/routers/presets.py @@ -0,0 +1,56 @@ +from holerr.core import config +from holerr.core.config_models import Preset +from holerr.core.config_repositories import PresetRepository +from holerr.core.exceptions import NotFoundException +from .routers_models import PartialPreset + +from fastapi import APIRouter, HTTPException, status + +router = APIRouter(prefix="/presets") + + +@router.get("", response_model=list[Preset], tags=["Presets"]) +async def list_presets(): + return config.presets + + +@router.post( + "", response_model=Preset, tags=["Presets"], status_code=status.HTTP_201_CREATED +) +async def add_preset(preset: Preset): + try: + return PresetRepository.add_preset(preset) + except Exception as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +@router.patch("/{preset_name}", response_model=Preset, tags=["Presets"]) +async def update_preset(preset_name: str, preset: PartialPreset): + try: + update_data = preset.model_dump(exclude_unset=True) + return PresetRepository.update_preset(preset_name, update_data) + except NotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e), + ) + except Exception as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +@router.delete( + "/{preset_name}", tags=["Presets"], status_code=status.HTTP_204_NO_CONTENT +) +async def delete_preset(preset_name): + try: + PresetRepository.delete_preset(preset_name) + except NotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e), + ) + except Exception: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Preset {preset_name} not found", + ) diff --git a/server/holerr/api/routers/routers_models.py b/server/holerr/api/routers/routers_models.py new file mode 100644 index 0000000..0b19042 --- /dev/null +++ b/server/holerr/api/routers/routers_models.py @@ -0,0 +1,90 @@ +from pydantic import BaseModel, SecretStr + +from typing import Optional +from datetime import datetime + + +# ------------------------------------------------------------ +# CONSTANTS +# ------------------------------------------------------------ +class Constants(BaseModel): + download_status: dict[str, int] + torrent_status: dict[str, str] + + +# ------------------------------------------------------------ +# CONFIGURATION +# ------------------------------------------------------------ +class PartialPreset(BaseModel): + name: Optional[str] = None + watch_dir: Optional[str] = None + output_dir: Optional[str] = None + create_sub_dir: Optional[bool] = None + file_extensions: Optional[list[str]] = None + min_file_size: Optional[str] = None + + +class PartialRealDebrid(BaseModel): + api_key: Optional[SecretStr] = None + + +class PartialDebrider(BaseModel): + real_debrid: Optional[PartialRealDebrid] = None + + +class PartialSynologyDownloadStation(BaseModel): + endpoint: Optional[str] = None + username: Optional[str] = None + password: Optional[SecretStr] = None + + +class PartialDownloader(BaseModel): + synology_download_station: Optional[PartialSynologyDownloadStation] = None + + +class PartialConfig(BaseModel): + debug: list[str] = None + debrider: Optional[PartialDebrider] = None + downloader: Optional[PartialDownloader] = None + presets: list[PartialPreset] = None + + +# ------------------------------------------------------------ +# DOWNLOADS +# ------------------------------------------------------------ +class Download(BaseModel): + id: str + magnet: str + title: str + preset: str + status: int + to_delete: bool + total_bytes: int + total_progress: int + created_at: datetime + updated_at: datetime + + +# ------------------------------------------------------------ +# STATUS +# ------------------------------------------------------------ +class StatusElement(BaseModel): + id: str + name: str + connected: bool + +class StatusApp(BaseModel): + version: str + +class Status(BaseModel): + app: StatusApp + debrider: StatusElement + downloader: StatusElement + + +# ------------------------------------------------------------ +# ACTIONS +# ------------------------------------------------------------ +class Magnet(BaseModel): + uri: str + preset: str \ No newline at end of file diff --git a/server/holerr/api/routers/status.py b/server/holerr/api/routers/status.py new file mode 100644 index 0000000..cc36ef1 --- /dev/null +++ b/server/holerr/api/routers/status.py @@ -0,0 +1,27 @@ +from holerr.debriders import debrider +from holerr.downloaders import downloader +from .routers_models import Status +from holerr.utils import info + +from fastapi import APIRouter + +router = APIRouter(prefix="/status") + + +@router.get("", response_model=Status, tags=["Status"]) +async def get_status(): + return { + "app": { + "version": info.get_app_version(), + }, + "debrider": { + "id": debrider.get_id(), + "name": debrider.get_name(), + "connected": debrider.is_connected(), + }, + "downloader": { + "id": downloader.get_id(), + "name": downloader.get_name(), + "connected": downloader.is_connected(), + }, + } diff --git a/server/holerr/api/routers/websocket.py b/server/holerr/api/routers/websocket.py new file mode 100644 index 0000000..d8a562f --- /dev/null +++ b/server/holerr/api/routers/websocket.py @@ -0,0 +1,14 @@ +from holerr.core.websockets import manager + +from fastapi import APIRouter, WebSocket, WebSocketDisconnect + +router = APIRouter(prefix="/ws") + +@router.websocket("") +async def websocket_endpoint(websocket: WebSocket): + await manager.connect(websocket) + try: + while True: + await websocket.receive_text() + except WebSocketDisconnect: + manager.disconnect(websocket) diff --git a/server/holerr/api/server.py b/server/holerr/api/server.py new file mode 100644 index 0000000..c497ebe --- /dev/null +++ b/server/holerr/api/server.py @@ -0,0 +1,22 @@ +from holerr.core import config +from holerr.utils import info +from holerr.core.log import Log + +from fastapi import FastAPI +import uvicorn + +log = Log.get_logger(__name__) + + +class Server: + def __init__(self): + root_path = "" if config.base_path is None else config.base_path + self._app = FastAPI(title="Holerr", version=info.get_app_version(), root_path=root_path) + + def start(self, port: int = 8765): + log.debug(f"Starting server on port {port}") + uvicorn.run(self._app, host="0.0.0.0", port=port) + + @property + def app(self): + return self._app diff --git a/server/holerr/core/__init__.py b/server/holerr/core/__init__.py new file mode 100644 index 0000000..2aa0c58 --- /dev/null +++ b/server/holerr/core/__init__.py @@ -0,0 +1,6 @@ +"""Init file for core module.""" + +from .config import Config + + +config = Config() \ No newline at end of file diff --git a/server/holerr/core/config.py b/server/holerr/core/config.py new file mode 100644 index 0000000..7379dba --- /dev/null +++ b/server/holerr/core/config.py @@ -0,0 +1,102 @@ +from .log import Log +from . import config_models + +import os +import json +import yaml + +log = Log.get_logger(__name__) + + +class Config: + @property + def file_path(self) -> str: + return os.path.abspath(self.data_dir + "/config.yaml") + + @property + def server_dir(self) -> str: + return os.path.abspath(os.path.dirname(__file__) + "/../..") + + @property + def public_dir(self) -> str: + return os.path.abspath(self.server_dir + "/../public") + + @property + def data_dir(self) -> str: + return os.path.abspath(self.server_dir + "/../data") + + def __init__(self): + self._conf: config_models.Config = None + + log.info("Init config...") + conf_file = os.path.abspath(self.data_dir + "/config.yaml") + v1_conf_file = os.path.abspath(self.data_dir + "/config.json") + if os.path.exists(v1_conf_file): + log.debug("Migrating v1 json configuration file...") + self._load_v1_json(v1_conf_file) + self.write() + os.remove(v1_conf_file) + + if not os.path.exists(conf_file): + raise Exception( + "Configuration file not found, please create a config.yaml file in the data directory." + ) + + def __getattr__(self, index): + return self._conf[index] + + @property + def raw(self): + return self._conf + + def update(self, data: dict): + self._conf.update(data) + self.write() + + def load(self): + content = yaml.load(open(self.file_path, "r"), Loader=yaml.FullLoader) + + self._conf = config_models.Config(**content) + if self.debug: + Log.set_debug_regex(self.debug) + + def _load_v1_json(self, json_path: str): + json_file = open(json_path, "r") + content = json.load(json_file) + + if "api_key" in content: + del content["api_key"] + if "debug" in content: + content["debug"] = ["holerr.*"] + if "debriders" in content: + content["debrider"] = content["debriders"] + del content["debriders"] + if "downloaders" in content: + content["downloader"] = content["downloaders"] + del content["downloaders"] + if "presets" in content: + for preset in content["presets"]: + # Convert min_file_size from bytes to human readable format + if "min_file_size" in preset and preset["min_file_size"] is not None: + num = preset["min_file_size"] + suffix = "B" + for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]: + if abs(num) < 1024.0: + preset["min_file_size"] = f"{num:3.1f}{unit}{suffix}" + break + num /= 1024.0 + + self._conf = config_models.Config(**content) + + def _dump(self) -> str: + return yaml.dump( + self._conf.model_dump(), + default_flow_style=False, + allow_unicode=True, + sort_keys=False, + ).replace(": null\n", ":\n") + + def write(self): + str_data = self._dump() + with open(self.file_path, "w", encoding="utf-8") as outfile: + outfile.write(str_data) diff --git a/server/holerr/core/config_models.py b/server/holerr/core/config_models.py new file mode 100644 index 0000000..3a34611 --- /dev/null +++ b/server/holerr/core/config_models.py @@ -0,0 +1,116 @@ +from .log import Log +from holerr.utils import secrets + +from typing import Optional +from pydantic import ( + SecretStr, + BaseModel, + model_validator, + ValidationError, + field_serializer, +) + +import re + +log = Log.get_logger(__name__) + + +class Model(BaseModel): + def update(self, data: dict): + cur_update = {} + for k, v in data.items(): + if isinstance(v, dict): + log.debug(f"updating {k} is dict, {v}") + getattr(self, k).update(v) + else: + cur_update[k] = v + + # validate + self.model_copy(update=cur_update) + for k, v in cur_update.items(): + if isinstance(v, list): + cur = getattr(self, k) + if not isinstance(cur, list): + setattr(self, k, []) + else: + getattr(self, k).clear() + for item in v: + getattr(self, k).append(item) + else: + setattr(self, k, v) + + def _dump_secret(self, v, info): + value = v.get_secret_value() + if info.mode == "python": + return value + return secrets.hide(value) + + +class Preset(Model): + name: str + watch_dir: str + output_dir: str + create_sub_dir: Optional[bool] = None + file_extensions: Optional[list[str]] = None + min_file_size: Optional[str] = None + + @property + def min_file_size_byte(self) -> int: + if not self.min_file_size: + return 0 + + units = {"B": 1, "KB": 2**10, "MB": 2**20, "GB": 2**30, "TB": 2**40} + size = self.min_file_size + if not re.match(r" ", size): + size = re.sub(r"([KMGT]?B)", r" \1", size) + number, unit = [string.strip() for string in size.split()] + return int(float(number) * units[unit]) + + +class RealDebrid(Model): + api_key: SecretStr + + @field_serializer("api_key") + def dump_secret(self, v, info): + return self._dump_secret(v, info) + + +class Debrider(Model): + real_debrid: Optional[RealDebrid] = None + + @model_validator(mode="after") + def verify_any_of(self): + if not self.real_debrid: + raise ValidationError("A debrider needs to be set.") + return self + + +class SynologyDownloadStation(Model): + endpoint: str + username: str + password: SecretStr + + @field_serializer("password") + def dump_secret(self, v, info): + return self._dump_secret(v, info) + + +class Downloader(Model): + synology_download_station: Optional[SynologyDownloadStation] = None + + @model_validator(mode="after") + def verify_any_of(self): + if not self.synology_download_station: + raise ValidationError("A downloader needs to be set.") + return self + + +class Config(Model): + debug: Optional[list[str]] = None + base_path: Optional[str] = None + debrider: Debrider = None + downloader: Downloader = None + presets: Optional[list[Preset]] = None + + def __getitem__(self, index): + return getattr(self, index) diff --git a/server/holerr/core/config_repositories.py b/server/holerr/core/config_repositories.py new file mode 100644 index 0000000..1685d16 --- /dev/null +++ b/server/holerr/core/config_repositories.py @@ -0,0 +1,83 @@ +from . import config +from .log import Log +from .config_models import Preset +from .exceptions import NotFoundException, AlreadyExistsException + +import os + +log = Log.get_logger(__name__) + + +class PresetRepository: + @staticmethod + def get_watch_directory(preset: Preset) -> str: + return config.data_dir + "/" + preset.watch_dir + + @staticmethod + def create_watch_directory(preset: Preset): + path = PresetRepository.get_watch_directory(preset) + if not os.path.exists(path): + os.makedirs(path) + log.debug(f"Created directory {path}") + else: + log.debug(f"Preset directory {path} already exists, skipping...") + + @staticmethod + def create_watch_directories(): + for preset in config.presets: + PresetRepository.create_watch_directory(preset) + + @staticmethod + def delete_watch_directory(preset: Preset): + path = PresetRepository.get_watch_directory(preset) + if os.path.exists(path): + os.rmdir(path) + + @staticmethod + def get_preset(name: str) -> Preset | None: + for preset in config.presets: + if preset.name == name: + return preset + return None + + @staticmethod + def get_preset_by_folder(folder: str) -> Preset | None: + data_dir = config.data_dir + for preset in config.presets: + if data_dir + "/" + preset.watch_dir == folder: + return preset + return None + + @staticmethod + def add_preset(preset: Preset) -> Preset: + p = PresetRepository.get_preset(preset.name) + if p is not None: + raise AlreadyExistsException(f"Preset {preset.name} already exists") + config.presets.append(preset) + PresetRepository.create_watch_directory(preset) + config.write() + return preset + + @staticmethod + def update_preset(preset_name: str, update_data: dict) -> Preset: + preset = PresetRepository.get_preset(preset_name) + if preset is None: + raise NotFoundException(f"Preset {preset_name} not found") + update_watch_dir = "watch_dir" in update_data + if update_watch_dir: + PresetRepository.delete_watch_directory(preset) + preset.update(update_data) + config.write() + if update_watch_dir: + PresetRepository.create_watch_directory(preset) + return preset + + @staticmethod + def delete_preset(name: str): + for index, preset in enumerate(config.presets): + if preset.name == name: + PresetRepository.delete_watch_directory(preset) + config.presets.pop(index) + config.write() + return + raise NotFoundException(f"Preset {name} not found") diff --git a/server/holerr/core/db.py b/server/holerr/core/db.py new file mode 100644 index 0000000..8631dbe --- /dev/null +++ b/server/holerr/core/db.py @@ -0,0 +1,25 @@ +from holerr.core.log import Log +from holerr.core import config + +import sqlalchemy +from sqlalchemy.orm import Session, scoped_session, sessionmaker + +log = Log.get_logger(__name__) + + +class Database: + def __init__(self): + self.engine = sqlalchemy.create_engine( + "sqlite:///" + config.data_dir + "/db.sqlite3" + ) + self.engine.connect() + self._scoped_session_factory = sessionmaker(bind=self.engine) + + def new_session(self): + return Session(self.engine) + + def new_scoped_session(self): + return scoped_session(self._scoped_session_factory) + + +db = Database() diff --git a/server/holerr/core/exceptions.py b/server/holerr/core/exceptions.py new file mode 100644 index 0000000..6a22fdb --- /dev/null +++ b/server/holerr/core/exceptions.py @@ -0,0 +1,12 @@ +class NotFoundException(Exception): + """custom exception class for not found item""" + + +class AlreadyExistsException(Exception): + """custom exception class for already existing item""" + + +class HttpRequestException(Exception): + def __init__(self, message: str, status_code: int): + super().__init__(f"{message} (status code: {str(status_code)})") + self.status_code = status_code diff --git a/server/holerr/core/log.py b/server/holerr/core/log.py new file mode 100644 index 0000000..3234bcf --- /dev/null +++ b/server/holerr/core/log.py @@ -0,0 +1,33 @@ +import logging +import re + +logging.basicConfig(level=logging.WARN) + + +class Log: + _debug_regex: list[re.Pattern] = None + + @staticmethod + def set_debug_regex(regexes: list[str]): + Log._debug_regex = [re.compile(regex) for regex in regexes] + loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict] + for logger in loggers: + logger.setLevel(logging.WARN) + for regex in Log._debug_regex: + if regex.match(logger.name): + logger.setLevel(logging.DEBUG) + break + + @staticmethod + def get_logger(package_name: str) -> logging.Logger: + name = f"holerr.{package_name}" + logger = logging.getLogger(name) + log_level = logging.INFO + if Log._debug_regex: + for regex in Log._debug_regex: + if regex.match(name): + log_level = logging.DEBUG + break + + logger.setLevel(log_level) + return logger diff --git a/server/holerr/core/websockets.py b/server/holerr/core/websockets.py new file mode 100644 index 0000000..343bd80 --- /dev/null +++ b/server/holerr/core/websockets.py @@ -0,0 +1,27 @@ +from fastapi import WebSocket +from typing import Any +from fastapi.encoders import jsonable_encoder + +Actions = { + "DOWNLOADS_NEW": "downloads/new", + "DOWNLOADS_UPDATE": "downloads/update", + "DOWNLOADS_DELETE": "downloads/delete" +} + +class ConnectionManager: + def __init__(self): + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def broadcast(self, action: str, payload: Any): + for connection in self.active_connections: + await connection.send_json({"action": action, "payload": jsonable_encoder(payload)}) + + +manager = ConnectionManager() \ No newline at end of file diff --git a/server/holerr/database/models.py b/server/holerr/database/models.py new file mode 100644 index 0000000..11cb0e7 --- /dev/null +++ b/server/holerr/database/models.py @@ -0,0 +1,142 @@ +from typing import List +from sqlalchemy import ForeignKey +from sqlalchemy.orm import ( + DeclarativeBase, + Mapped, + mapped_column, + relationship, +) +from sqlalchemy.sql import func +from datetime import datetime + +TABLE_DOWNLOAD = "download" +TABLE_DEBRIDER_INFO = "debrider_info" +TABLE_DEBRIDER_FILE = "debrider_file" +TABLE_DEBRIDER_LINK = "debrider_link" +TABLE_DOWNLOADER_INFO = "downloader_info" +TABLE_DOWNLOADER_TASK = "downloader_task" + +DownloadStatus = { + "TORRENT_FOUND": 0, + "TORRENT_SENT_TO_DEBRIDER": 10, + "DEBRIDER_DOWNLOADING": 11, + "DEBRIDER_POST_DOWNLOAD": 12, + "DEBRIDER_DOWNLOADED": 13, + "SENT_TO_DOWNLOADER": 20, + "DOWNLOADER_DOWNLOADING": 21, + "DOWNLOADER_DOWNLOADED": 22, + "DOWNLOADED": 30, + "ERROR_NO_FILES_FOUND": 100, + "ERROR_DEBRIDER": 101, + "ERROR_DOWNLOADER": 102, + "ERROR_DELETED_ON_DEBRIDER": 103, +} + + +class Base(DeclarativeBase): + pass + + +class Download(Base): + __tablename__ = TABLE_DOWNLOAD + + id: Mapped[str] = mapped_column(primary_key=True) + magnet: Mapped[str] + title: Mapped[str] + preset: Mapped[str] + status: Mapped[int] + total_bytes: Mapped[int] + total_progress: Mapped[int] + to_delete: Mapped[bool] = mapped_column(default=False) + created_at: Mapped[datetime] = mapped_column(default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + default=func.now(), onupdate=func.now() + ) + + @property + def hash(self): + return f"{self.id}.{str(self.status)}.{str(self.total_progress)}" + + debrider_info: Mapped["DebriderInfo"] = relationship( + back_populates="download", cascade="all, delete-orphan" + ) + debrider_files: Mapped[List["DebriderFile"]] = relationship( + back_populates="download", cascade="all, delete-orphan" + ) + debrider_links: Mapped[List["DebriderLink"]] = relationship( + back_populates="download", cascade="all, delete-orphan" + ) + downloader_info: Mapped["DownloaderInfo"] = relationship( + back_populates="download", cascade="all, delete-orphan" + ) + downloader_tasks: Mapped[List["DownloaderTask"]] = relationship( + back_populates="download", cascade="all, delete-orphan" + ) + + def __repr__(self): + return f"DownloadModel(id={self.id}, title={self.title}, status={self.status})" + + +class DebriderInfo(Base): + __tablename__ = TABLE_DEBRIDER_INFO + + id: Mapped[str] = mapped_column(primary_key=True) + download_id: Mapped[int] = mapped_column(ForeignKey(TABLE_DOWNLOAD + ".id")) + filename: Mapped[str] + bytes: Mapped[int] + progress: Mapped[int] + status: Mapped[str] + created_at: Mapped[datetime] = mapped_column(default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + default=func.now(), onupdate=func.now() + ) + + download: Mapped["Download"] = relationship(back_populates="debrider_info") + + +class DebriderFile(Base): + __tablename__ = TABLE_DEBRIDER_FILE + + id: Mapped[str] = mapped_column(primary_key=True) + download_id: Mapped[int] = mapped_column(ForeignKey(TABLE_DOWNLOAD + ".id")) + path: Mapped[str] + bytes: Mapped[int] + selected: Mapped[int] + + download: Mapped["Download"] = relationship(back_populates="debrider_files") + + +class DebriderLink(Base): + __tablename__ = TABLE_DEBRIDER_LINK + + link: Mapped[str] = mapped_column(primary_key=True) + download_id: Mapped[int] = mapped_column(ForeignKey(TABLE_DOWNLOAD + ".id")) + is_unrestricted: Mapped[bool] + + download: Mapped["Download"] = relationship(back_populates="debrider_links") + + +class DownloaderInfo(Base): + __tablename__ = TABLE_DOWNLOADER_INFO + + download_id: Mapped[int] = mapped_column( + ForeignKey(TABLE_DOWNLOAD + ".id"), primary_key=True + ) + progress: Mapped[int] + created_at: Mapped[datetime] = mapped_column(default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + default=func.now(), onupdate=func.now() + ) + + download: Mapped["Download"] = relationship(back_populates="downloader_info") + + +class DownloaderTask(Base): + __tablename__ = TABLE_DOWNLOADER_TASK + + id: Mapped[str] = mapped_column(primary_key=True) + download_id: Mapped[int] = mapped_column(ForeignKey(TABLE_DOWNLOAD + ".id")) + status: Mapped[int] + bytes_downloaded: Mapped[int] + + download: Mapped["Download"] = relationship(back_populates="downloader_tasks") diff --git a/server/holerr/database/repositories.py b/server/holerr/database/repositories.py new file mode 100644 index 0000000..b0e6e51 --- /dev/null +++ b/server/holerr/database/repositories.py @@ -0,0 +1,242 @@ +from .models import ( + Base, + Download, + DownloadStatus, + DebriderInfo, + DebriderFile, + DebriderLink, + DownloaderInfo, + DownloaderTask, +) +from holerr.utils import torrent, magnet +from holerr.core.config_repositories import PresetRepository +from holerr.debriders.debrider_models import TorrentInfo +from holerr.core.exceptions import NotFoundException, AlreadyExistsException + +import os +from sqlalchemy import select +from sqlalchemy.orm import Session + + +class Repository: + def __init__(self, session: Session, entity: Base): + self.session = session + self.entity = entity + + def get_model(self, id: any) -> Base | None: + res = self.session.scalars(select(self.entity).where(self.entity.id == id)) + return res.one_or_none() + + def get_all_models(self, conditions=True, options=None) -> list[Base]: + query = select(self.entity).where(conditions) + if options: + query = query.options(options) + res = self.session.scalars(query) + return res.all() + + def create_model(self, **kwargs) -> Base: + model = self.entity(**kwargs) + self.session.add(model) + self.session.commit() + return model + + +class DownloadRepository(Repository): + def __init__(self, session: Session): + super().__init__(session, Download) + + @staticmethod + def compute_id_from_torrent(path: str) -> str: + return torrent.get_hash(path) + + @staticmethod + def get_name_from_torrent(path: str) -> str: + return torrent.get_name(path) + + def create_model_from_torrent(self, path: str) -> Download: + id = DownloadRepository.compute_id_from_torrent(path) + if self.get_model(id) is not None: + raise AlreadyExistsException(f"Download {id} already exists") + title = DownloadRepository.get_name_from_torrent(path) + status = DownloadStatus["TORRENT_FOUND"] + preset = PresetRepository.get_preset_by_folder(os.path.dirname(path)) + if preset is None: + raise NotFoundException("Preset not found for {path}") + return self.create_model( + id=id, + magnet=torrent.get_magnet_link(path), + title=title, + status=status, + preset=preset.name, + total_bytes=0, + total_progress=0, + ) + + def create_model_from_magnet(self, magnet_uri: str, preset_name: str) -> Download: + id = magnet.get_hash(magnet_uri) + if self.get_model(id) is not None: + raise AlreadyExistsException(f"Download {id} already exists") + title = magnet.get_name(magnet_uri) + status = DownloadStatus["TORRENT_FOUND"] + preset = PresetRepository.get_preset(preset_name) + if preset is None: + raise NotFoundException(f"Preset {preset_name} not found") + return self.create_model( + id=id, + magnet=magnet_uri, + title=title, + status=status, + preset=preset.name, + total_bytes=0, + total_progress=0, + ) + + def get_all_handled_by_debrider(self) -> list[Download]: + return self.get_all_models( + Download.status.in_( + tuple( + [ + DownloadStatus["TORRENT_SENT_TO_DEBRIDER"], + DownloadStatus["DEBRIDER_DOWNLOADING"], + DownloadStatus["DEBRIDER_POST_DOWNLOAD"], + ] + ) + ), + not Download.to_delete, + ) + + def get_all_handled_by_download_state_transition(self) -> list[Download]: + return self.get_all_models( + Download.status.in_( + tuple( + [ + DownloadStatus["TORRENT_FOUND"], + DownloadStatus["DEBRIDER_DOWNLOADED"], + ] + ) + ), + not Download.to_delete, + ) + + def get_all_handled_by_downloader(self) -> list[Download]: + return self.get_all_models( + Download.status.in_( + tuple( + [ + DownloadStatus["SENT_TO_DOWNLOADER"], + DownloadStatus["DOWNLOADER_DOWNLOADING"], + ] + ) + ), + not Download.to_delete, + ) + + def get_all_to_delete(self) -> list[Download]: + return self.get_all_models( + Download.to_delete, + ) + + def clean_downloaded(self) -> list[str]: + deleted_ids = [] + downloads = self.get_all_models(Download.status == DownloadStatus["DOWNLOADED"]) + for download in downloads: + deleted_ids.append(download.id) + self.session.delete(download) + self.session.commit() + return deleted_ids + + def delete_download(self, id: str): + download = self.get_model(id) + if download is None: + raise NotFoundException(f"Download {id} not found") + if download.status == DownloadStatus["TORRENT_FOUND"]: + raise Exception(f"Cannot delete download {id}, download is not started") + + if ( + download.status >= DownloadStatus["TORRENT_SENT_TO_DEBRIDER"] + and download.status < DownloadStatus["DEBRIDER_DOWNLOADED"] + ): + # Debrider is working on it, we can't delete it + raise Exception(f"Download {id} is being processed by the debrider") + + +class DebriderInfoRepository(Repository): + def __init__(self, session: Session): + super().__init__(session, DebriderInfo) + + def create_model_from_torrent_info( + self, torrent_info: TorrentInfo, download: Download + ) -> DebriderInfo: + return self.create_model( + id=torrent_info.id, + download=download, + filename=torrent_info.filename, + bytes=torrent_info.bytes, + progress=torrent_info.progress, + status=torrent_info.status, + ) + + +class DebriderFileRepository(Repository): + def __init__(self, session: Session): + super().__init__(session, DebriderFile) + + def create_models_from_torrent_info( + self, torrent_info: TorrentInfo, download: Download + ) -> list[DebriderFile]: + files = [] + for torrent_file in torrent_info.files: + files.append( + self.create_model( + id=DebriderFileRepository.compute_id(torrent_info, torrent_file.id), + download=download, + path=torrent_file.path, + bytes=torrent_file.bytes, + selected=torrent_file.selected, + ) + ) + + @staticmethod + def compute_id(torrent_info: TorrentInfo, file_id: int) -> str: + return torrent_info.id + "." + str(file_id) + + @staticmethod + def get_torrent_file_id(model: DebriderFile) -> int: + return int(model.id.split(".")[-1]) + + +class DebriderLinkRepository(Repository): + def __init__(self, session: Session): + super().__init__(session, DebriderLink) + + def create_models_from_torrent_info( + self, torrent_info: TorrentInfo, download: Download + ) -> list[DebriderLink]: + links = [] + for link in torrent_info.links: + links.append(link) + return self.create_models(links, False, download) + + def create_models( + self, in_links: list[str], is_unrestricted: bool, download: Download + ): + links = [] + for link in in_links: + links.append( + self.create_model( + link=link, + download=download, + is_unrestricted=is_unrestricted, + ) + ) + return links + + +class DownloaderInfoRepository(Repository): + def __init__(self, session: Session): + super().__init__(session, DownloaderInfo) + + +class DownloaderTaskRepository(Repository): + def __init__(self, session: Session): + super().__init__(session, DownloaderTask) diff --git a/server/holerr/debriders/__init__.py b/server/holerr/debriders/__init__.py new file mode 100644 index 0000000..c9958ec --- /dev/null +++ b/server/holerr/debriders/__init__.py @@ -0,0 +1,20 @@ +from holerr.core import config + +class WrappedDebrider(): + def __init__(self) -> None: + self.update() + + def update(self): + if config.debrider.real_debrid: + from .real_debrid import RealDebrid + + self._debrider = RealDebrid(config.debrider.real_debrid) + else: + self._debrider = None + + def __getattr__(self, name): + if self._debrider is None: + raise Exception("No debrider found") + return getattr(self._debrider, name) + +debrider = WrappedDebrider() diff --git a/server/holerr/debriders/debrider.py b/server/holerr/debriders/debrider.py new file mode 100644 index 0000000..8dfbdac --- /dev/null +++ b/server/holerr/debriders/debrider.py @@ -0,0 +1,44 @@ +from abc import ABC, abstractmethod +from .debrider_models import TorrentInfo + + +class Debrider(ABC): + @abstractmethod + def get_id(self) -> str: + pass + + @abstractmethod + def get_name(self) -> str: + pass + + @abstractmethod + def is_connected(self) -> bool: + pass + + @abstractmethod + def get_slots_available(self) -> int: + pass + + @abstractmethod + def get_active_downloads(self): + pass + + @abstractmethod + def add_magnet(self, magnet: str) -> str: + pass + + @abstractmethod + def get_torrent_info(self, torrent_id: str) -> TorrentInfo: + pass + + @abstractmethod + def select_files(self, torrent_id: str, files: list[str]): + pass + + @abstractmethod + def delete_torrent(self, torrent_id: str): + pass + + @abstractmethod + def unrestricted_link(self, link: str) -> str | None: + pass diff --git a/server/holerr/debriders/debrider_models.py b/server/holerr/debriders/debrider_models.py new file mode 100644 index 0000000..a1996f4 --- /dev/null +++ b/server/holerr/debriders/debrider_models.py @@ -0,0 +1,32 @@ +from pydantic import BaseModel + +TorrentStatus = { + "MAGNET_ERROR": "magnet_error", + "MAGNET_CONVERSION": "magnet_conversion", + "WAITING_FILES_SELECTION": "waiting_files_selection", + "QUEUED": "queued", + "DOWNLOADING": "downloading", + "DOWNLOADED": "downloaded", + "ERROR": "error", + "VIRUS": "virus", + "COMPRESSING": "compressing", + "UPLOADING": "uploading", + "DEAD": "dead", +} + + +class File(BaseModel): + id: int + path: str # Path to the file inside the torrent, starting with "/" + bytes: int + selected: int # 0 or 1 + + +class TorrentInfo(BaseModel): + id: str + filename: str + bytes: int # Size of selected files only + progress: float # Possible values: 0 to 100 + status: str # Current status of the torrent + files: list[File] + links: list[str] diff --git a/server/holerr/debriders/debrider_repositories.py b/server/holerr/debriders/debrider_repositories.py new file mode 100644 index 0000000..d8b52f3 --- /dev/null +++ b/server/holerr/debriders/debrider_repositories.py @@ -0,0 +1,22 @@ +from holerr.core.config_models import Preset +from .debrider_models import File + + +class FileRepository: + @staticmethod + def get_preset_files(files: list[File], preset: Preset) -> list[File]: + filtered_files = [] + for file in files: + if not ( + preset.min_file_size_byte == 0 + or file.bytes >= preset.min_file_size_byte + ): + continue + file_extention = file.path.split(".")[-1] + if not ( + preset.file_extensions is None + or file_extention in preset.file_extensions + ): + continue + filtered_files.append(file) + return filtered_files diff --git a/server/holerr/debriders/real_debrid.py b/server/holerr/debriders/real_debrid.py new file mode 100644 index 0000000..8c0d1d2 --- /dev/null +++ b/server/holerr/debriders/real_debrid.py @@ -0,0 +1,106 @@ +from .debrider import Debrider +from holerr.core.config_models import RealDebrid as RealDebridConfig +from .real_debrid_models import ( + Profile, + ActiveCount, + Torrent, + TorrentInfo, + UnrestrictedLink, +) +from holerr.core.exceptions import HttpRequestException + +import requests + +ENDPOINT = "https://api.real-debrid.com/rest/1.0" +MAXIMUM_ACTIVE_DOWNLOADS = 20 + + +class RealDebrid(Debrider): + def __init__(self, conf: RealDebridConfig): + self.api_key = conf.api_key + + def get_id(self) -> str: + return "real_debrid" + + def get_name(self) -> str: + return "Real-Debrid" + + def is_connected(self) -> bool: + try: + if self._me() is not None: + return True + except Exception: + pass + return False + + def get_slots_available(self) -> int: + return MAXIMUM_ACTIVE_DOWNLOADS - self._get_torrent_active_count().nb + + def get_active_downloads(self): + # TODO + pass + + def add_magnet(self, magnet: str) -> str: + res = self._call("POST", "/torrents/addMagnet", data={"magnet": magnet}) + if res.status_code != 201: + raise HttpRequestException( + "Error while adding magnet", res.status_code + ) + torrent_info = Torrent(**res.json()) + return torrent_info.id + + def get_torrent_info(self, torrent_id: str) -> TorrentInfo | None: + res = self._call("GET", "/torrents/info/" + torrent_id) + if res.status_code != 200: + return None + return TorrentInfo(**res.json()) + + def select_files(self, torrent_id: str, files: list[str]): + res = self._call( + "POST", + "/torrents/selectFiles/" + torrent_id, + data={"files": ",".join(files)}, + ) + if res.status_code != 204: + raise HttpRequestException( + "Error while deleting torrent", res.status_code + ) + + def delete_torrent(self, torrent_id: str): + res = self._call("DELETE", "/torrents/delete/" + torrent_id) + if res.status_code != 204: + raise HttpRequestException( + "Error while deleting torrent", res.status_code + ) + + def unrestricted_link(self, link: str) -> str | None: + return self._get_unrestricted_link(link).download + + def _call(self, method, path, **kwargs): + if not kwargs.get("headers"): + kwargs["headers"] = {} + + kwargs["headers"]["Authorization"] = "Bearer " + self.api_key.get_secret_value() + return requests.request(method, ENDPOINT + path, **kwargs) + + def _me(self) -> Profile | None: + res = self._call("GET", "/user") + if res.status_code != 200: + raise HttpRequestException( + "Error while getting user info", res.status_code + ) + return Profile(**res.json()) + + def _get_torrent_active_count(self) -> ActiveCount: + res = self._call("GET", "/torrents/activeCount") + if res.status_code != 200: + raise Exception("Error while getting active count") + return ActiveCount(**res.json()) + + def _get_unrestricted_link(self, link: str) -> str: + res = self._call("POST", "/unrestrict/link", data={"link": link}) + if res.status_code != 200: + raise HttpRequestException( + "Error while unrestricting link", res.status_code + ) + return UnrestrictedLink(**res.json()) diff --git a/server/holerr/debriders/real_debrid_models.py b/server/holerr/debriders/real_debrid_models.py new file mode 100644 index 0000000..93afcf8 --- /dev/null +++ b/server/holerr/debriders/real_debrid_models.py @@ -0,0 +1,54 @@ +from typing import Optional +from pydantic import BaseModel +from .debrider_models import TorrentInfo as DebriderTorrentInfo + + +class Profile(BaseModel): + id: int + username: str + email: str + points: int # Fidelity points + locale: str # User language + avatar: str # URL + type: str # "premium" or "free" + premium: int # seconds left as a Premium user + expiration: str # jsonDate + + +class ActiveCount(BaseModel): + nb: int # Number of currently active torrents + limit: int # Maximum number of active torrents you can have + + +class Torrent(BaseModel): + id: str + uri: str # URL of the created ressource + + +class TorrentInfo(DebriderTorrentInfo): + original_filename: str # Original name of the torrent + hash: str # SHA1 Hash of the torrent + original_bytes: int # Total size of the torrent + host: str # Host main domain + split: int # Split size of links + added: str # jsonDate + ended: Optional[str] = None # Only present when finished, jsonDate + speed: Optional[int] = ( + None # Only present in "downloading", "compressing", "uploading" status + ) + seeders: Optional[int] = ( + None # Only present in "downloading", "magnet_conversion" status + ) + + +class UnrestrictedLink(BaseModel): + id: str + filename: str + mimeType: str # Mime Type of the file, guessed by the file extension + filesize: int # Filesize in bytes, 0 if unknown + link: str # Original link + host: str # Host main domain + chunks: int # Max Chunks allowed + crc: int # Disable / enable CRC check + download: str # Generated link + streamable: int # Is the file streamable on website diff --git a/server/holerr/downloaders/__init__.py b/server/holerr/downloaders/__init__.py new file mode 100644 index 0000000..ba6cc86 --- /dev/null +++ b/server/holerr/downloaders/__init__.py @@ -0,0 +1,21 @@ +from holerr.core import config + +class WrappedDownloader(): + def __init__(self) -> None: + self.update() + + def update(self): + if config.downloader.synology_download_station: + from .synology_download_station import SynologyDownloadStation + + self._downloader = SynologyDownloadStation(config.downloader.synology_download_station) + else: + self._downloader = None + + def __getattr__(self, name): + if self._downloader is None: + raise Exception("No downloader found") + return getattr(self._downloader, name) + +downloader = WrappedDownloader() + diff --git a/server/holerr/downloaders/downloader.py b/server/holerr/downloaders/downloader.py new file mode 100644 index 0000000..327b0f5 --- /dev/null +++ b/server/holerr/downloaders/downloader.py @@ -0,0 +1,29 @@ +from abc import ABC, abstractmethod +from holerr.core.config_models import Preset + + +class Downloader(ABC): + @abstractmethod + def get_id(self) -> str: + pass + + @abstractmethod + def get_name(self) -> str: + pass + + @abstractmethod + def is_connected(self) -> bool: + pass + + @abstractmethod + def add_download(self, uri: str, title: str, preset: Preset) -> str: + pass + + @abstractmethod + # Returns a tuple of the status of the download and the size downloaded + def get_task_status(self, id: str) -> tuple[str, int]: + pass + + @abstractmethod + def delete_download(self, id: str): + pass diff --git a/server/holerr/downloaders/downloader_models.py b/server/holerr/downloaders/downloader_models.py new file mode 100644 index 0000000..847829b --- /dev/null +++ b/server/holerr/downloaders/downloader_models.py @@ -0,0 +1,12 @@ +DownloadStatus = { + "WAITING": "waiting", + "DOWNLOADING": "downloading", + "PAUSED": "paused", + "FINISHING": "finishing", + "FINISHED": "finished", + "HASH_CHECKING": "hash_checking", + "SEEDING": "seeding", + "FILEHOSTING_WAITING": "filehosting_waiting", + "EXTRACTING": "extracting", + "ERROR": "error", +} diff --git a/server/holerr/downloaders/downloader_repositories.py b/server/holerr/downloaders/downloader_repositories.py new file mode 100644 index 0000000..0d93241 --- /dev/null +++ b/server/holerr/downloaders/downloader_repositories.py @@ -0,0 +1,26 @@ +from .downloader_models import DownloadStatus +from holerr.database.models import DownloadStatus as DBDownloadStatus + +StatusMapping: dict[str, int] = {} +StatusMapping[DownloadStatus["WAITING"]] = DBDownloadStatus["DOWNLOADER_DOWNLOADING"] +StatusMapping[DownloadStatus["DOWNLOADING"]] = DBDownloadStatus[ + "DOWNLOADER_DOWNLOADING" +] +StatusMapping[DownloadStatus["PAUSED"]] = DBDownloadStatus["DOWNLOADER_DOWNLOADING"] +StatusMapping[DownloadStatus["FINISHING"]] = DBDownloadStatus["DOWNLOADER_DOWNLOADING"] +StatusMapping[DownloadStatus["FINISHED"]] = DBDownloadStatus["DOWNLOADER_DOWNLOADED"] +StatusMapping[DownloadStatus["HASH_CHECKING"]] = DBDownloadStatus[ + "DOWNLOADER_DOWNLOADING" +] +StatusMapping[DownloadStatus["SEEDING"]] = DBDownloadStatus["DOWNLOADER_DOWNLOADED"] +StatusMapping[DownloadStatus["FILEHOSTING_WAITING"]] = DBDownloadStatus[ + "DOWNLOADER_DOWNLOADING" +] +StatusMapping[DownloadStatus["EXTRACTING"]] = DBDownloadStatus["DOWNLOADER_DOWNLOADING"] +StatusMapping[DownloadStatus["ERROR"]] = DBDownloadStatus["ERROR_DOWNLOADER"] + + +class DownloaderRepository: + @staticmethod + def downloader_status_to_download_status(status: str) -> int: + return StatusMapping[status] or DBDownloadStatus["DOWNLOADER_DOWNLOADING"] diff --git a/server/holerr/downloaders/synology_download_station.py b/server/holerr/downloaders/synology_download_station.py new file mode 100644 index 0000000..d6911ca --- /dev/null +++ b/server/holerr/downloaders/synology_download_station.py @@ -0,0 +1,190 @@ +from .downloader import Downloader +from holerr.core import config +from holerr.core.log import Log +from .synology_download_station_models import Auth, Tasks, Status +from holerr.core.config_models import Preset +from holerr.core.exceptions import HttpRequestException + + +import requests +from pathlib import Path +import urllib + +log = Log.get_logger(__name__) + + +class SynologyDownloadStation(Downloader): + def __init__(self, config): + pass + + def get_id(self) -> str: + return "synology_download_station" + + def get_name(self) -> str: + return "Synology Download Station" + + def is_connected(self) -> bool: + sid = self._connect("DownloadStation") + return sid is not None + + def add_download(self, uri: str, title: str, preset: Preset) -> str: + sid = self._connect("DownloadStation") + if sid is None: + raise Exception("Could not connect to Synology Download Station") + + params = { + "api": "SYNO.DownloadStation.Task", + "version": "1", + "method": "create", + "uri": uri, + "_sid": sid, + } + + if preset.output_dir is not None: + destination = preset.output_dir + if preset.create_sub_dir: + sub_folder = self.get_sub_folder_name(title) + self._create_output_dir(destination, sub_folder) + destination += "/" + sub_folder + + params = urllib.parse.urlencode(params) + # known bug in Synology Download Station API fails with "+" destination + params += "&destination=" + requests.utils.quote(destination).replace( + "+", "%20" + ) + + res = self._call("/DownloadStation/task.cgi", params=params) + if res.status_code != 200: + raise HttpRequestException("Could not add download " + res, res.status_code) + obj = Status(**res.json()) + if not obj.success: + log.debug(res.request.url) + raise Exception("Error while adding download, code: " + str(obj.error.code)) + return self._get_download_id(uri) + + def get_task_status(self, id: str) -> tuple[str, int]: + sid = self._connect("DownloadStation") + if sid is None: + raise Exception("Could not connect to Synology Download Station") + + params = { + "api": "SYNO.DownloadStation.Task", + "version": "1", + "method": "getinfo", + "additional": "transfer", + "id": id, + "_sid": sid, + } + res = self._call("/DownloadStation/task.cgi", params=params) + if res.status_code != 200: + raise HttpRequestException("Could not get task status", res.status_code) + + obj = Tasks(**res.json()) + if not obj.success: + raise Exception( + "Error while getting task status, code: " + str(obj.error.code) + ) + + task = obj.data.tasks[0] + + return task.status, task.additional.transfer.size_downloaded + + def delete_download(self, id: str): + sid = self._connect("DownloadStation") + if sid is None: + raise Exception("Could not connect to Synology Download Station") + + params = { + "api": "SYNO.DownloadStation.Task", + "version": "1", + "method": "delete", + "id": id, + "_sid": sid, + } + res = self._call("/DownloadStation/task.cgi", params=params) + if res.status_code != 200: + raise HttpRequestException(f"Could not delete download {id}", res.status_code) + + def _call(self, path, **kwargs): + return requests.request("GET", self._get_api_url(path), **kwargs) + + def _get_api_url(self, path: str) -> str: + return config.downloader.synology_download_station.endpoint + "/webapi" + path + + def _connect(self, session: str) -> str | None: + syno_cfg = config.downloader.synology_download_station + params = { + "api": "SYNO.API.Auth", + "version": "3", + "session": session, + "account": syno_cfg.username, + "passwd": syno_cfg.password.get_secret_value(), + "format": "sid", + "method": "login", + } + res = self._call("/auth.cgi", params=params) + if res.status_code != 200: + raise HttpRequestException(f"Could not login to {session}", res.status_code) + + auth = Auth(**res.json()) + if not auth.success: + return None + return auth.data.sid + + def _get_download_id(self, uri: str) -> str: + sid = self._connect("DownloadStation") + if sid is None: + raise Exception("Could not connect to Synology Download Station") + + params = { + "api": "SYNO.DownloadStation.Task", + "version": "1", + "format": "sid", + "method": "list", + "additional": "detail", + "_sid": sid, + } + + res = self._call("/DownloadStation/task.cgi", params=params) + if res.status_code != 200: + raise HttpRequestException(f"Could not list downloads", res.status_code) + obj = Tasks(**res.json()) + if not obj.success: + raise Exception( + "Error while searching download id, code: " + str(obj.error.code) + ) + + for task in obj.data.tasks: + if task.additional.detail.uri == uri: + return task.id + log.debug(f"Download {uri} not found") + return None + + def _create_output_dir(self, parent: str, name: str): + sid = self._connect("FileStation") + if sid is None: + raise Exception("Could not connect to Synology File Station") + + folder_path = parent + if folder_path[0] != "/": + folder_path = "/" + folder_path + + params = { + "api": "SYNO.FileStation.CreateFolder", + "version": "2", + "method": "create", + "folder_path": folder_path, + "name": name, + "_sid": sid, + } + res = self._call("/entry.cgi", params=params) + if res.status_code != 200: + raise HttpRequestException(f"Could not create folder", res.status_code) + obj = Status(**res.json()) + if not obj.success and obj.error.code != 109: # 109 = folder already exists + log.debug(res.request.url) + raise Exception("Error while creating folder, code: " + str(obj.error.code)) + + @staticmethod + def get_sub_folder_name(name: str) -> str: + return Path(name).stem diff --git a/server/holerr/downloaders/synology_download_station_models.py b/server/holerr/downloaders/synology_download_station_models.py new file mode 100644 index 0000000..2153cd4 --- /dev/null +++ b/server/holerr/downloaders/synology_download_station_models.py @@ -0,0 +1,72 @@ +from typing import Optional, Any +from pydantic import BaseModel + + +class Error(BaseModel): + code: int + + +class Status(BaseModel): + error: Optional[Error] = None + success: bool + + +class AuthData(BaseModel): + sid: str + did: str + + +class Auth(Status): + data: Optional[AuthData] = None + + +class TaskDetail(BaseModel): + completed_time: int + connected_leechers: int + connected_peers: int + connected_seeders: int + create_time: int + destination: str + seedelapsed: int + started_time: int + total_peers: int + total_pieces: int + unzip_password: str + uri: str + waiting_seconds: int + + +class TaskTransfer(BaseModel): + downloaded_pieces: int + size_downloaded: int + size_uploaded: int + speed_download: int + speed_upload: int + + +class TaskAdditional(BaseModel): + detail: Optional[TaskDetail] = None + transfer: Optional[TaskTransfer] = None + file: Optional[list[Any]] = None # BT only + tracker: Optional[Any] = None # BT only + peer: Optional[Any] = None # BT only + + +class Task(BaseModel): + id: str + type: str + username: str + title: str + size: int + status: str + additional: TaskAdditional + + +class TasksData(BaseModel): + total: Optional[int] = None + offset: Optional[int] = None + tasks: list[Task] + + +class Tasks(Status): + data: Optional[TasksData] = None diff --git a/server/holerr/tasks/__init__.py b/server/holerr/tasks/__init__.py new file mode 100644 index 0000000..24a6f35 --- /dev/null +++ b/server/holerr/tasks/__init__.py @@ -0,0 +1,17 @@ +"""Init file for workers module.""" + +from .worker import Worker +from .tasks import ( + delete_downloads, + download_state_transitions, + torrent_files, + debrider, + downloader, +) + +worker = Worker() +worker.add(delete_downloads) +worker.add(download_state_transitions) +worker.add(torrent_files) +worker.add(debrider) +worker.add(downloader) diff --git a/server/holerr/tasks/task.py b/server/holerr/tasks/task.py new file mode 100644 index 0000000..a11cef8 --- /dev/null +++ b/server/holerr/tasks/task.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class Task(ABC): + @abstractmethod + async def run(self): + pass diff --git a/server/holerr/tasks/tasks/__init__.py b/server/holerr/tasks/tasks/__init__.py new file mode 100644 index 0000000..91587f7 --- /dev/null +++ b/server/holerr/tasks/tasks/__init__.py @@ -0,0 +1,11 @@ +from .delete_downloads import TaskDeleteDownloads +from .download_state_transitions import TaskDownloadStateTransition +from .torrent_files import TaskTorrentFiles +from .debrider import TaskDebrider +from .downloader import TaskDownloader + +delete_downloads = TaskDeleteDownloads() +download_state_transitions = TaskDownloadStateTransition() +torrent_files = TaskTorrentFiles() +debrider = TaskDebrider() +downloader = TaskDownloader() diff --git a/server/holerr/tasks/tasks/debrider.py b/server/holerr/tasks/tasks/debrider.py new file mode 100644 index 0000000..578c716 --- /dev/null +++ b/server/holerr/tasks/tasks/debrider.py @@ -0,0 +1,125 @@ +from ..task import Task +from holerr.core.log import Log +from holerr.core.db import db +from holerr.core.config_repositories import PresetRepository +from holerr.database.models import Download, DownloadStatus +from holerr.database.repositories import ( + DownloadRepository, + DebriderFileRepository, + DebriderLinkRepository, +) +from holerr.debriders import debrider +from holerr.debriders.debrider_models import TorrentStatus, TorrentInfo +from holerr.debriders.debrider_repositories import FileRepository +from holerr.core.websockets import manager, Actions + +from sqlalchemy.orm import Session + +log = Log.get_logger(__name__) + + +class DebriderDownloadHanlder: + def __init__(self, session: Session): + self._db_session = session + + def handle_download(self, download: Download): + debrider_info = debrider.get_torrent_info(download.debrider_info.id) + + if debrider_info is None: + download.status = DownloadStatus["ERROR_DELETED_ON_DEBRIDER"] + return + + download.debrider_info.status = debrider_info.status + download.debrider_info.progress = debrider_info.progress + + if download.debrider_info.status == TorrentStatus["MAGNET_ERROR"]: + log.debug("Magnet error " + str(download.id)) + download.status = DownloadStatus["ERROR_DEBRIDER"] + + if download.debrider_info.status == TorrentStatus["WAITING_FILES_SELECTION"]: + self._on_waiting_file_selection(download) + + if download.debrider_info.status == TorrentStatus["DOWNLOADING"]: + self._on_downloading(download) + + if download.debrider_info.status == TorrentStatus["DOWNLOADED"]: + self._on_downloaded(download, debrider_info) + + if ( + download.debrider_info.status == TorrentStatus["COMPRESSING"] + or download.debrider_info.status == TorrentStatus["UPLOADING"] + ): + self._on_post_download(download) + + if download.debrider_info.status == TorrentStatus["ERROR"]: + log.debug("Debrider has torrent error " + download.id) + download.status = DownloadStatus["ERROR_DEBRIDER"] + + if download.debrider_info.status == TorrentStatus["VIRUS"]: + log.debug("Debrider found a virus " + download.id) + download.status = DownloadStatus["ERROR_DEBRIDER"] + + if download.debrider_info.status == TorrentStatus["DEAD"]: + log.debug("Debrider download is dead " + download.id) + download.status = DownloadStatus["ERROR_DEBRIDER"] + + def _on_waiting_file_selection(self, download: Download): + log.debug("Selecting files for " + str(download.id)) + preset = PresetRepository.get_preset(download.preset) + preset_files = FileRepository.get_preset_files(download.debrider_files, preset) + download.total_progress = 2 + if len(preset_files) == 0: + log.debug("No file that match preset rules found " + download.id) + download.status = DownloadStatus["ERROR_NO_FILES_FOUND"] + return + + files = [] + download.total_bytes = 0 + for file in preset_files: + files.append(str(DebriderFileRepository.get_torrent_file_id(file))) + file.selected = True + download.total_bytes += file.bytes + debrider.select_files(download.debrider_info.id, files) + download.total_progress = 3 + + def _on_downloading(self, download: Download): + if download.status != DownloadStatus["DEBRIDER_DOWNLOADING"]: + download.status = DownloadStatus["DEBRIDER_DOWNLOADING"] + log.debug("Debrider is downloading " + str(download.id)) + download.total_progress = 3 + int((download.debrider_info.progress) * 0.44) + + def _on_post_download(self, download: Download): + log.debug("Debrider is doing post download actions " + str(download.id)) + download.status = DownloadStatus["DEBRIDER_POST_DOWNLOAD"] + download.total_progress = 48 + + def _on_downloaded(self, download: Download, debrider_info: TorrentInfo): + log.debug("Debrider downloaded " + str(download.id)) + links_repo = DebriderLinkRepository(self._db_session) + links_repo.create_models_from_torrent_info(debrider_info, download) + unrestricted_links = [] + for link in debrider_info.links: + unrestricted_link = debrider.unrestricted_link(link) + unrestricted_links.append(unrestricted_link) + links_repo.create_models(unrestricted_links, True, download) + download.status = DownloadStatus["DEBRIDER_DOWNLOADED"] + download.total_progress = 49 + + +class TaskDebrider(Task): + async def run(self): + self._db_session = db.new_scoped_session() + for download in self.get_downloads(): + handler = DebriderDownloadHanlder(self._db_session) + before_hash = download.hash + handler.handle_download(download) + if before_hash != download.hash: + log.debug("Hash changed, updating download") + await manager.broadcast(Actions["DOWNLOADS_UPDATE"], download) + + self._db_session.commit() + self._db_session.remove() + + def get_downloads(self) -> list[Download]: + rep = DownloadRepository(self._db_session) + return rep.get_all_handled_by_debrider() diff --git a/server/holerr/tasks/tasks/delete_downloads.py b/server/holerr/tasks/tasks/delete_downloads.py new file mode 100644 index 0000000..30342c6 --- /dev/null +++ b/server/holerr/tasks/tasks/delete_downloads.py @@ -0,0 +1,69 @@ +from ..task import Task +from holerr.core.log import Log +from holerr.core.db import db +from holerr.database.models import Download, DownloadStatus +from holerr.database.repositories import ( + DownloadRepository, +) +from holerr.debriders import debrider +from holerr.downloaders import downloader +from holerr.core.websockets import manager, Actions + +from sqlalchemy.orm import Session + +log = Log.get_logger(__name__) + + +class DeleteDownloadsdHanlder: + def __init__(self, session: Session): + self._db_session = session + + def handle_download(self, download: Download): + if download.status == DownloadStatus["TORRENT_FOUND"]: + # To early, torrent is added just after + return + + if ( + download.status >= DownloadStatus["TORRENT_SENT_TO_DEBRIDER"] + and download.status <= DownloadStatus["DEBRIDER_DOWNLOADED"] + ): + self._delete_debrider_download(download) + + if ( + download.status >= DownloadStatus["SENT_TO_DOWNLOADER"] + and download.status <= DownloadStatus["DOWNLOADER_DOWNLOADED"] + ): + self._delete_downloader_download(download) + self._db_session.delete(download) + + def _delete_debrider_download(self, download: Download): + log.debug(f"Delete debrider download {download.debrider_info.id}") + try: + debrider.delete_torrent(download.debrider_info.id) + except Exception as e: + if (e.status_code == 404): + log.debug(f"Debrider torrent {download.debrider_info.id} already deleted") + else: + raise e + + def _delete_downloader_download(self, download: Download): + downloader_ids = [task.id for task in download.downloader_tasks] + if len(downloader_ids) > 0: + log.debug(f"Delete downloader downloads {','.join(downloader_ids)}") + downloader.delete_download(",".join(downloader_ids)) + + +class TaskDeleteDownloads(Task): + async def run(self): + self._db_session = db.new_scoped_session() + for download in self.get_downloads(): + handler = DeleteDownloadsdHanlder(self._db_session) + handler.handle_download(download) + await manager.broadcast(Actions["DOWNLOADS_DELETE"], download) + + self._db_session.commit() + self._db_session.remove() + + def get_downloads(self) -> list[Download]: + rep = DownloadRepository(self._db_session) + return rep.get_all_to_delete() diff --git a/server/holerr/tasks/tasks/download_state_transitions.py b/server/holerr/tasks/tasks/download_state_transitions.py new file mode 100644 index 0000000..0856cf5 --- /dev/null +++ b/server/holerr/tasks/tasks/download_state_transitions.py @@ -0,0 +1,78 @@ +from ..task import Task +from holerr.core.log import Log +from holerr.core.db import db +from holerr.core.config_repositories import PresetRepository +from holerr.database.models import Download, DownloadStatus +from holerr.database.repositories import DownloadRepository +from holerr.downloaders import downloader +from holerr.database.repositories import ( + DebriderInfoRepository, + DebriderFileRepository, + DownloaderInfoRepository, + DownloaderTaskRepository, +) +from holerr.debriders import debrider +from holerr.core.websockets import manager, Actions + +from sqlalchemy.orm import Session + +log = Log.get_logger(__name__) + + +class TransitionHanlder: + def __init__(self, session: Session): + self._db_session = session + + def handle_transition(self, download: Download): + if download.status == DownloadStatus["TORRENT_FOUND"]: + log.debug("Sending torrent " + str(download.id) + " to debrider") + self._send_to_debrider(download) + + if download.status == DownloadStatus["DEBRIDER_DOWNLOADED"]: + log.debug(f"Sending torrent {download.id} to downloader") + self._send_to_downloader(download) + + def _send_to_debrider(self, download: Download): + debrider_id = debrider.add_magnet(download.magnet) + debrider_info = debrider.get_torrent_info(debrider_id) + download.status = DownloadStatus["TORRENT_SENT_TO_DEBRIDER"] + download.total_progress = 1 + DebriderInfoRepository(self._db_session).create_model_from_torrent_info( + debrider_info, download + ) + DebriderFileRepository(self._db_session).create_models_from_torrent_info( + debrider_info, download + ) + + def _send_to_downloader(self, download: Download): + preset = PresetRepository.get_preset(download.preset) + downloader_task_repo = DownloaderTaskRepository(self._db_session) + for link in download.debrider_links: + if link.is_unrestricted: + id = downloader.add_download(link.link, download.title, preset) + status = DownloadStatus["DOWNLOADER_DOWNLOADING"] + downloader_task_repo.create_model( + id=id, status=status, bytes_downloaded=0, download=download + ) + download.status = DownloadStatus["DOWNLOADER_DOWNLOADING"] + download.total_progress = 50 + DownloaderInfoRepository(self._db_session).create_model( + download=download, progress=0 + ) + debrider.delete_torrent(download.debrider_info.id) + + +class TaskDownloadStateTransition(Task): + async def run(self): + self._db_session = db.new_scoped_session() + for download in self.get_downloads(): + handler = TransitionHanlder(self._db_session) + handler.handle_transition(download) + await manager.broadcast(Actions["DOWNLOADS_UPDATE"], download) + + self._db_session.commit() + self._db_session.remove() + + def get_downloads(self) -> list[Download]: + rep = DownloadRepository(self._db_session) + return rep.get_all_handled_by_download_state_transition() diff --git a/server/holerr/tasks/tasks/downloader.py b/server/holerr/tasks/tasks/downloader.py new file mode 100644 index 0000000..04859b8 --- /dev/null +++ b/server/holerr/tasks/tasks/downloader.py @@ -0,0 +1,69 @@ +from ..task import Task +from holerr.core.log import Log +from holerr.core.db import db +from holerr.database.models import Download, DownloadStatus +from holerr.database.repositories import ( + DownloadRepository, +) +from holerr.downloaders import downloader +from holerr.downloaders.downloader_repositories import DownloaderRepository +from holerr.core.websockets import manager, Actions + +log = Log.get_logger(__name__) + + +class DownloaderDownloadHanlder: + def __init__(self, session): + self._db_session = session + + def handle_download(self, download: Download): + total_bytes_downloaded = 0 + for task in download.downloader_tasks: + try: + [status, size_downloaded] = downloader.get_task_status(task.id) + task.status = DownloaderRepository.downloader_status_to_download_status( + status + ) + task.bytes_downloaded = size_downloaded + total_bytes_downloaded += size_downloaded + except Exception: + task.status = DownloadStatus["ERROR_DOWNLOADER"] + + download_status = download.downloader_tasks[0].status + for task in download.downloader_tasks: + task_is_error = task.status == DownloadStatus["ERROR_DOWNLOADER"] + download_is_error = download_status == DownloadStatus["ERROR_DOWNLOADER"] + if ( + task.status < download_status or task_is_error + ) and not download_is_error: + download_status = task.status + + download.downloader_info.progress = int( + (total_bytes_downloaded * 100) / download.total_bytes + ) + + if download_status == DownloadStatus["DOWNLOADER_DOWNLOADED"]: + download.status = DownloadStatus["DOWNLOADED"] + download.total_progress = 100 + else: + download.status = download_status + download.total_progress = 50 + int(download.downloader_info.progress * 0.49) + + +class TaskDownloader(Task): + async def run(self): + self._db_session = db.new_scoped_session() + for download in self.get_downloads(): + handler = DownloaderDownloadHanlder(self._db_session) + before_hash = download.hash + handler.handle_download(download) + if before_hash != download.hash: + log.debug("Hash changed, updating download") + await manager.broadcast(Actions["DOWNLOADS_UPDATE"], download) + + self._db_session.commit() + self._db_session.remove() + + def get_downloads(self) -> list[Download]: + rep = DownloadRepository(self._db_session) + return rep.get_all_handled_by_downloader() diff --git a/server/holerr/tasks/tasks/torrent_files.py b/server/holerr/tasks/tasks/torrent_files.py new file mode 100644 index 0000000..a037c02 --- /dev/null +++ b/server/holerr/tasks/tasks/torrent_files.py @@ -0,0 +1,51 @@ +from ..task import Task +from holerr.core import config +from holerr.core.log import Log +from holerr.database.repositories import ( + DownloadRepository, +) +from holerr.core.db import db +from holerr.core.websockets import manager, Actions + +import glob +import os +from sqlalchemy.orm import Session + +log = Log.get_logger(__name__) + + +class TorrentFileHandler: + @staticmethod + def _is_torrent_file(path): + return path.endswith(".torrent") + + def __init__(self, session: Session): + self._db_session = session + self._download_repository = DownloadRepository(self._db_session) + + def handle_file(self, path): + if TorrentFileHandler._is_torrent_file(path): + id = DownloadRepository.compute_id_from_torrent(path) + log.info(f"{path} found") + log.debug("Download ID is " + str(id)) + model = self._download_repository.get_model(id) + if model is None: + log.debug(f"Adding {id} to database") + model = self._download_repository.create_model_from_torrent(path) + manager.broadcast(Actions["DOWNLOADS_NEW"], model) + else: + log.debug(f"{id} already in database") + self._delete_torrent_file(path) + + def _delete_torrent_file(self, path): + os.remove(path) + + +class TaskTorrentFiles(Task): + async def run(self): + self._db_session = db.new_scoped_session() + holes_path = config.data_dir + "/holes" + torrents = glob.glob(holes_path + "/**/*.torrent") + handler = TorrentFileHandler(self._db_session) + for f in torrents: + handler.handle_file(f) diff --git a/server/holerr/tasks/worker.py b/server/holerr/tasks/worker.py new file mode 100644 index 0000000..376a44a --- /dev/null +++ b/server/holerr/tasks/worker.py @@ -0,0 +1,39 @@ +from holerr.core.log import Log +from .task import Task + +import asyncio +import threading + +log = Log.get_logger(__name__) + + +class Worker: + _worker: threading.Thread = None + _tasks: list[Task] = [] + + def add(self, task: Task): + log.debug(type(task).__name__ + " added") + self._tasks.append(task) + + def start(self): + self.stop() + + self._worker = threading.Thread(target=self.run) + + self._worker.start() + + def stop(self): + if self._worker: + log.debug("Stopping worker") + self._worker.join() + del self._worker + + def run(self): + asyncio.run(self._run()) + + async def _run(self): + log.debug("Starting workers") + while True: + for task in self._tasks: + await task.run() + await asyncio.sleep(5) diff --git a/server/holerr/utils/info.py b/server/holerr/utils/info.py new file mode 100644 index 0000000..2a5565a --- /dev/null +++ b/server/holerr/utils/info.py @@ -0,0 +1,4 @@ +import os + +def get_app_version(): + return os.getenv("APP_VERSION", "local") \ No newline at end of file diff --git a/server/holerr/utils/magnet.py b/server/holerr/utils/magnet.py new file mode 100644 index 0000000..fa1eeb2 --- /dev/null +++ b/server/holerr/utils/magnet.py @@ -0,0 +1,11 @@ +from torf import Magnet + + +def get_hash(magnet_uri: str) -> str: + magnet = Magnet.from_string(magnet_uri) + return magnet.infohash + + +def get_name(magnet_uri: str) -> str: + magnet = Magnet.from_string(magnet_uri) + return magnet.dn diff --git a/server/holerr/utils/secrets.py b/server/holerr/utils/secrets.py new file mode 100644 index 0000000..93a52b3 --- /dev/null +++ b/server/holerr/utils/secrets.py @@ -0,0 +1,4 @@ +def hide(secret: str) -> str: + if len(secret) < 3: + return "*" * len(secret) + return secret[0] + ("*" * (len(secret) - 2)) + secret[-1] diff --git a/server/holerr/utils/torrent.py b/server/holerr/utils/torrent.py new file mode 100644 index 0000000..a6a705b --- /dev/null +++ b/server/holerr/utils/torrent.py @@ -0,0 +1,16 @@ +from torf import Torrent + + +def get_hash(path: str) -> str: + torrent = Torrent.read(path) + return torrent.infohash_base32 + + +def get_name(path: str) -> str: + torrent = Torrent.read(path) + return torrent.metainfo["info"]["name"] + + +def get_magnet_link(path: str) -> str: + torrent = Torrent.read(path) + return str(torrent.magnet()) diff --git a/server/main.go b/server/main.go deleted file mode 100644 index 4e049c8..0000000 --- a/server/main.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" - "holerr/api" - "holerr/core/config" - "holerr/core/log" - "holerr/debriders" - "holerr/downloaders" - "holerr/scheduler" - "net/http" - "os" - "path/filepath" - "strings" -) - -func init() { - log.Error("init") - config.InitFromFile() - - log.Info("Service RUN on DEBUG mode") - - debrider := debriders.Get() - if debrider != nil { - log.Info("Using debrider " + debrider.GetName() + "") - } - - downloader := downloaders.Get() - if downloader != nil { - log.Info("Using downloader " + downloader.GetName()) - } -} - -func main() { - log.Error("main") - - // Scheduler - go func() { - scheduler.Downloads() - }() - - r := chi.NewRouter() - - // A good base middleware stack - r.Use(middleware.RequestID) - r.Use(middleware.RealIP) - if config.IsDebug() { - r.Use(middleware.Logger) - } - r.Use(middleware.Recoverer) - - basePath := config.GetBasePath() - if basePath == "" { - config.SetBasePath("/") - basePath = "/" - } - if basePath != "/" && basePath[len(basePath)-1] != '/' { - r.Get(basePath, http.RedirectHandler(basePath+"/", 301).ServeHTTP) - basePath += "/" - } - - r.Get(basePath+"*", func(w http.ResponseWriter, r *http.Request) { - requestUri := "/" + strings.TrimPrefix(r.RequestURI, basePath) - staticHandler := http.FileServer(http.Dir(config.GetPublicDir())) - fs := http.StripPrefix(basePath, staticHandler) - if _, err := os.Stat(filepath.Join(config.GetPublicDir(), requestUri)); os.IsNotExist(err) { - // If not exists, fallback on index.html - http.ServeFile(w, r, filepath.Join(config.GetPublicDir(), "index.html")) - } else { - // If file exists in public dir, serve it - fs.ServeHTTP(w, r) - } - }) - - r.Route(basePath+"api", api.Router) - - log.Fatal(http.ListenAndServe(":8781", r)) - log.Info("Server started: http://0.0.0.0:8781") -} diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..b318031 --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,32 @@ +alembic==1.13.1 +annotated-types==0.6.0 +anyio==4.3.0 +certifi==2024.2.2 +charset-normalizer==3.3.2 +click==8.1.7 +fastapi==0.110.2 +flatbencode==0.2.1 +greenlet==3.0.3 +h11==0.14.0 +httptools==0.6.1 +idna==3.7 +Mako==1.3.3 +MarkupSafe==2.1.5 +pyaml==24.4.0 +pydantic==2.7.0 +pydantic_core==2.18.1 +python-dotenv==1.0.1 +python-multipart==0.0.9 +PyYAML==6.0.1 +requests==2.31.0 +setuptools==69.5.1 +sniffio==1.3.1 +SQLAlchemy==2.0.29 +starlette==0.37.2 +torf==4.2.6 +typing_extensions==4.11.0 +urllib3==2.2.1 +uvicorn==0.29.0 +uvloop==0.19.0 +watchfiles==0.21.0 +websockets==12.0 diff --git a/server/scheduler/addTorrents.go b/server/scheduler/addTorrents.go deleted file mode 100644 index 3d8f75e..0000000 --- a/server/scheduler/addTorrents.go +++ /dev/null @@ -1,116 +0,0 @@ -package scheduler - -import ( - "crypto/sha1" - "fmt" - "holerr/api" - "holerr/core/config" - "holerr/core/db" - "holerr/core/log" - "holerr/debriders" - debriderInterface "holerr/debriders/debrider" - "os" - "path/filepath" - "reflect" - "strings" - "time" -) - -func AddTorrents() { - publicDir := config.GetDataDir() - err := filepath.Walk(publicDir, - func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() && strings.HasSuffix(path, ".torrent") { - log.Info("New torrent found: " + path) - handleTorrent(path) - } - return nil - }) - if err != nil { - log.Fatal(err) - } -} - -func handleTorrent(path string) { - dbi := db.Get() - - name, id := computeTorrentInfo(path) - - downCheck := db.Download{} - dbi.Read("downloads", id, &downCheck) - if !reflect.DeepEqual(downCheck, db.Download{}) { - log.Error("Torrent already in database!") - } else { - preset, err := config.GetPresetByPath(path) - if err != nil { - log.Error(err) - } else { - down := handleNewTorrentFound(id, name, preset) - sendToDebrider(path, &down) - - api.WebsocketBroadcast("downloads/new", down) - } - } - err := os.Remove(path) - if err != nil { - log.Error(err) - } -} - -func computeTorrentInfo(path string) (string, string) { - basename := filepath.Base(path) - // Remove .torrent in file name - name := basename[0 : len(basename)-8] - h := sha1.New() - h.Write([]byte(name)) - id := fmt.Sprintf("%x", h.Sum(nil))[1:8] - - return name, id -} - -func handleNewTorrentFound(id string, name string, preset config.Preset) db.Download { - dbi := db.Get() - now := time.Now() - down := db.Download{ - Id: id, - Title: name, - Preset: preset.Name, - Status: db.DownloadStatus["TORRENT_FOUND"], - TorrentInfo: debriderInterface.TorrentInfo{}, - CreatedAt: now, - UpdatedAt: now, - } - down.StatusDetails = db.DownloadStatusDetail[down.Status] - if writeErr := dbi.Write("downloads", id, down); writeErr != nil { - log.Error(writeErr) - } - - return down -} - -func sendToDebrider(path string, down *db.Download) { - dbi := db.Get() - debrider := debriders.Get() - if debrider == nil { - log.Info("No debrider set") - return - } - - torrentId, err := debrider.AddTorrent(path) - if err != nil { - log.Error(err) - } - down.Status = db.DownloadStatus["TORRENT_SENT_TO_DEBRIDER"] - down.StatusDetails = db.DownloadStatusDetail[down.Status] - down.TorrentInfo, err = debrider.GetTorrentInfos(torrentId) - if err != nil { - log.Error(err) - } - down.UpdatedAt = time.Now() - if writeErr := dbi.Write("downloads", down.Id, down); writeErr != nil { - log.Error(writeErr) - } -} diff --git a/server/scheduler/downloadFiles.go b/server/scheduler/downloadFiles.go deleted file mode 100644 index 1e524a1..0000000 --- a/server/scheduler/downloadFiles.go +++ /dev/null @@ -1,55 +0,0 @@ -package scheduler - -import ( - "holerr/core/config" - "holerr/core/db" - "holerr/core/log" - "holerr/debriders" - "holerr/downloaders" -) - -func DownloadFiles(download *db.Download) { - downloader := downloaders.Get() - if downloader == nil { - log.Info("No downloader set") - return - } - - preset, err := config.GetPresetByName(download.Preset) - if err != nil { - log.Error(err) - return - } - downloadInfo := db.DownloadInfo{Progress: 0, Bytes: 0, Tasks: map[string]db.DownloadInfoTask{}} - for _, file := range download.TorrentInfo.Files { - if file.Selected == 1 { - downloadInfo.Bytes += file.Bytes - } - } - - for _, link := range download.TorrentInfo.Links { - id, err := downloader.AddDownload(link, download.Title, preset) - if err != nil { - log.Error(err) - } - downloadInfo.Tasks[link] = db.DownloadInfoTask{Id: id} - } - - debrider := debriders.Get() - if debrider == nil { - log.Info("No debrider set") - return - } - err = debrider.DeleteTorrent(download.TorrentInfo.Id) - if err != nil { - log.Error(err) - } - - dbi := db.Get() - download.Status = db.DownloadStatus["SENT_TO_DOWNLOADER"] - download.StatusDetails = db.DownloadStatusDetail[download.Status] - download.DownloadInfo = downloadInfo - if writeErr := dbi.Write("downloads", download.Id, download); writeErr != nil { - log.Error(writeErr) - } -} diff --git a/server/scheduler/scheduler.go b/server/scheduler/scheduler.go deleted file mode 100644 index 7283ca7..0000000 --- a/server/scheduler/scheduler.go +++ /dev/null @@ -1,58 +0,0 @@ -package scheduler - -import ( - "encoding/json" - "reflect" - "time" - "holerr/api" - "holerr/core/db" - "holerr/core/log" - "holerr/debriders/debrider" -) - -func Downloads() { - dbi := db.Get() - - for { - AddTorrents() - - records, err := dbi.ReadAll("downloads") - if err != nil { - log.Error(err) - } - - for _, f := range records { - downloadBeforeOperation := db.Download{} - if err := json.Unmarshal([]byte(f), &downloadBeforeOperation); err != nil { - log.Error(err) - } - - download := db.Download{} - if err := json.Unmarshal([]byte(f), &download); err != nil { - log.Error(err) - } - - if download.Status == db.DownloadStatus["TORRENT_SENT_TO_DEBRIDER"] && download.TorrentInfo.Status == debrider.TorrentStatus["WAITING_FILES_SELECTION"] { - SelectFiles(&download) - } - - if download.Status == db.DownloadStatus["DEBRIDER_DOWNLOADING"] { - UpdateDebriderInfos(&download) - } - - if download.Status == db.DownloadStatus["DEBRIDER_DOWNLOADED"] { - DownloadFiles(&download) - } - - if download.Status == db.DownloadStatus["SENT_TO_DOWNLOADER"] || download.Status == db.DownloadStatus["DOWNLOADER_DOWNLOADING"] { - UpdateDownloaderInfo(&download) - } - - // Send change notification if change detected - if !reflect.DeepEqual(downloadBeforeOperation, download) { - api.WebsocketBroadcast("downloads/update", download) - } - } - time.Sleep(5 * time.Second) - } -} diff --git a/server/scheduler/selectFiles.go b/server/scheduler/selectFiles.go deleted file mode 100644 index c01fa7e..0000000 --- a/server/scheduler/selectFiles.go +++ /dev/null @@ -1,85 +0,0 @@ -package scheduler - -import ( - "fmt" - "holerr/core/config" - "holerr/core/db" - "holerr/core/log" - "holerr/debriders" - "strings" -) - -func SelectFiles(download *db.Download) { - debrider := debriders.Get() - if debrider != nil { - log.Info("No debrider set") - } - - if download.Preset != "" && len(download.TorrentInfo.Files) >= 1 { - preset, err := config.GetPresetByName(download.Preset) - if err != nil { - log.Error(err) - return - } - - filesExtensionFiltred := []string{} - if len(preset.FileExtensions) > 0 { - for _, file := range download.TorrentInfo.Files { - for _, ext := range preset.FileExtensions { - if strings.HasSuffix(file.Path, ext) { - filesExtensionFiltred = append(filesExtensionFiltred, fmt.Sprintf("%d", file.Id)) - } - break - } - } - } else { - for _, file := range download.TorrentInfo.Files { - filesExtensionFiltred = append(filesExtensionFiltred, fmt.Sprintf("%d", file.Id)) - } - } - - filesMinSizeFiltred := []string{} - if preset.MinFileSize != nil && *preset.MinFileSize > 0 { - for _, file := range download.TorrentInfo.Files { - if file.Bytes >= *preset.MinFileSize { - filesMinSizeFiltred = append(filesMinSizeFiltred, fmt.Sprintf("%d", file.Id)) - } - } - } else { - for _, file := range download.TorrentInfo.Files { - filesMinSizeFiltred = append(filesMinSizeFiltred, fmt.Sprintf("%d", file.Id)) - } - } - - files := intersect(filesExtensionFiltred, filesMinSizeFiltred) - if len(files) == 0 { - // TODO: delete torrent - log.Info("No file selected for " + download.Title) - } - - err = debrider.SelectFiles(download.TorrentInfo.Id, files) - if err != nil { - log.Error(err) - } - UpdateDebriderInfos(download) - } -} - -func intersect(s1 []string, s2 []string) []string { - res := []string{} - for _, a := range s1 { - if contains(s2, a) { - res = append(res, a) - } - } - return res -} - -func contains(s []string, e string) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} diff --git a/server/scheduler/updateDebriderInfos.go b/server/scheduler/updateDebriderInfos.go deleted file mode 100644 index 4d9fd8b..0000000 --- a/server/scheduler/updateDebriderInfos.go +++ /dev/null @@ -1,43 +0,0 @@ -package scheduler - -import ( - "holerr/core/db" - "holerr/core/log" - "holerr/debriders" - debriderInterface "holerr/debriders/debrider" - "time" -) - -func UpdateDebriderInfos(download *db.Download) { - debrider := debriders.Get() - if debrider == nil { - log.Info("No debrider set") - return - } - - dbi := db.Get() - - previousStatus := download.Status - - var err error - download.TorrentInfo, err = debrider.GetTorrentInfos(download.TorrentInfo.Id) - if err != nil { - log.Error(err) - } - if download.TorrentInfo.Status == debriderInterface.TorrentStatus["DOWNLOADED"] { - download.Status = db.DownloadStatus["DEBRIDER_DOWNLOADED"] - } else if download.TorrentInfo.Status == debriderInterface.TorrentStatus["ERROR"] || download.TorrentInfo.Status == debriderInterface.TorrentStatus["VIRUS"] || download.TorrentInfo.Status == debriderInterface.TorrentStatus["DEAD"] { - download.Status = db.DownloadStatus["ERROR_DEBRIDER"] - } else { - download.Status = db.DownloadStatus["DEBRIDER_DOWNLOADING"] - } - - if previousStatus != download.Status { - download.StatusDetails = db.DownloadStatusDetail[download.Status] - download.UpdatedAt = time.Now() - } - - if writeErr := dbi.Write("downloads", download.Id, download); writeErr != nil { - log.Error(writeErr) - } -} diff --git a/server/scheduler/updateDownloaderInfo.go b/server/scheduler/updateDownloaderInfo.go deleted file mode 100644 index 8ad9ec9..0000000 --- a/server/scheduler/updateDownloaderInfo.go +++ /dev/null @@ -1,52 +0,0 @@ -package scheduler - -import ( - "holerr/core/db" - "holerr/core/log" - "holerr/downloaders" - "time" -) - -func UpdateDownloaderInfo(download *db.Download) { - previousUpdatedAt := download.UpdatedAt - - downloader := downloaders.Get() - if downloader == nil { - log.Info("No downloader set") - return - } - dbi := db.Get() - - // Do not ask all at once, status is messy (first is a valid status, others are int) - downloadStatus := db.DownloadStatus["DOWNLOADER_DOWNLOADED"] - totalBytesDownloaded := 0 - for uri, task := range download.DownloadInfo.Tasks { - if task.Status != db.DownloadStatus["DOWNLOADER_DOWNLOADED"] && task.Status != db.DownloadStatus["ERROR_DOWNLOADER"] { - taskStatus, sizeDownloaded, err := downloader.GetTaskStatus(task.Id) - task.Status = taskStatus - task.BytesDownloaded = sizeDownloaded - if downloadStatus != db.DownloadStatus["ERROR_DOWNLOADER"] { - if err != nil { - log.Error(err) - downloadStatus = db.DownloadStatus["ERROR_DOWNLOADER"] - } - - if taskStatus < downloadStatus || taskStatus == db.DownloadStatus["ERROR_DOWNLOADER"] { - downloadStatus = taskStatus - } - } - download.DownloadInfo.Tasks[uri] = task - download.UpdatedAt = time.Now() - } - totalBytesDownloaded += task.BytesDownloaded - } - - if download.UpdatedAt != previousUpdatedAt { - download.DownloadInfo.Progress = int((totalBytesDownloaded * 100 / download.DownloadInfo.Bytes)) - download.Status = downloadStatus - download.StatusDetails = db.DownloadStatusDetail[download.Status] - if writeErr := dbi.Write("downloads", download.Id, download); writeErr != nil { - log.Error(writeErr) - } - } -}