Compare commits
No commits in common. "main" and "v2.0.129" have entirely different histories.
81 changed files with 1413 additions and 3994 deletions
|
|
@ -1,14 +0,0 @@
|
|||
FROM mcr.microsoft.com/devcontainers/dotnet:2-10.0-noble
|
||||
|
||||
ARG NODE_MAJOR=22
|
||||
ARG ANGULAR_CLI_VERSION=21
|
||||
|
||||
RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nodejs sqlite3 \
|
||||
&& npm install -g "@angular/cli@^${ANGULAR_CLI_VERSION}" \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /data/db /data/downloads \
|
||||
&& chown -R vscode:vscode /data
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
{
|
||||
"name": "rdt-client",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": {
|
||||
"NODE_MAJOR": "22"
|
||||
}
|
||||
},
|
||||
"remoteUser": "vscode",
|
||||
"runArgs": ["--init"],
|
||||
"containerEnv": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BASE_PATH": "",
|
||||
"DOTNET_USE_POLLING_FILE_WATCHER": "1",
|
||||
"CHOKIDAR_USEPOLLING": "true"
|
||||
},
|
||||
"forwardPorts": [4200, 6500],
|
||||
"portsAttributes": {
|
||||
"4200": {
|
||||
"label": "Angular client",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"6500": {
|
||||
"label": "RdtClient backend",
|
||||
"onAutoForward": "silent"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
"source=rdtclient-data-db,target=/data/db,type=volume",
|
||||
"source=rdtclient-data-downloads,target=/data/downloads,type=volume",
|
||||
"source=rdtclient-nuget,target=/home/vscode/.nuget,type=volume",
|
||||
"source=rdtclient-npm-cache,target=/home/vscode/.npm,type=volume"
|
||||
],
|
||||
"postCreateCommand": "/bin/bash .devcontainer/post-create.sh",
|
||||
"postStartCommand": "/bin/bash .devcontainer/post-start.sh",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-dotnettools.csharp",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"Angular.ng-template",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
"settings": {
|
||||
"dotnet.defaultSolution": "server/RdtClient.sln",
|
||||
"terminal.integrated.defaultProfile.linux": "zsh"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
sudo install -d -m 0775 -o vscode -g vscode \
|
||||
/data/db \
|
||||
/data/downloads \
|
||||
/home/vscode/.nuget \
|
||||
/home/vscode/.nuget/NuGet \
|
||||
/home/vscode/.nuget/packages \
|
||||
/home/vscode/.npm
|
||||
|
||||
sudo chown -R vscode:vscode /data /home/vscode/.nuget /home/vscode/.npm
|
||||
|
||||
dotnet restore server/RdtClient.sln
|
||||
npm install --prefix client
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
sudo install -d -m 0775 -o vscode -g vscode \
|
||||
/data/db \
|
||||
/data/downloads \
|
||||
/home/vscode/.nuget \
|
||||
/home/vscode/.nuget/NuGet \
|
||||
/home/vscode/.nuget/packages \
|
||||
/home/vscode/.npm
|
||||
|
||||
sudo chown -R vscode:vscode /data /home/vscode/.nuget /home/vscode/.npm
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -8,4 +8,3 @@ server/RdtClient.Web/appsettings.Development.json
|
|||
data
|
||||
Dockerfile.dev
|
||||
test.bat
|
||||
.DS_Store
|
||||
|
|
|
|||
45
CHANGELOG.md
45
CHANGELOG.md
|
|
@ -6,51 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased
|
||||
|
||||
## [2.0.138] - 2026-06-05
|
||||
### Fixed
|
||||
- SABnzbd fixes.
|
||||
|
||||
## [2.0.137] - 2026-06-05
|
||||
### Fixed
|
||||
- Fix torbox save path mapping for single file downloads.
|
||||
|
||||
## [2.0.136] - 2026-05-30
|
||||
### Added
|
||||
- Added devcontainer development workflow.
|
||||
|
||||
### Changed
|
||||
- Upgraded torbox.net dependency.
|
||||
|
||||
## [2.0.135] - 2026-05-27
|
||||
### Changed
|
||||
- When adding a torrent through the qBittorrent endpoints, it will wait to see if the torrent gets added properly or errors out, resulting in a better experience for infringing files and Sonarr / Radarr.
|
||||
- Upgraded torbox.net dependency.
|
||||
- Support SABnzbd API key auth, thanks to @ALenfant!
|
||||
|
||||
## [2.0.134] - 2026-05-22
|
||||
### Changed
|
||||
- Upgraded torbox.net dependency.
|
||||
|
||||
## [2.0.133] - 2026-05-17
|
||||
### Added
|
||||
- Added NZB support for Premiumize, thanks to @ALenfant!
|
||||
|
||||
## [2.0.132] - 2026-05-17
|
||||
### Changed
|
||||
- Fixed for Cleanuparr
|
||||
- Upgrade Torbox.NET dependency
|
||||
|
||||
## [2.0.131] - 2026-05-14
|
||||
### Changed
|
||||
- Fixes to the rate limiting.
|
||||
|
||||
## [2.0.130] - 2026-04-28
|
||||
### Changed
|
||||
- Fixes to unit tests
|
||||
- Upgraded .NET and Angular dependencies
|
||||
- Added better qBittorrent capabilities for Cleanuparr
|
||||
- Performance fixes
|
||||
|
||||
## [2.0.129] - 2026-04-06
|
||||
### Added
|
||||
- Optimized mobile views, thanks to @sylvaindd!
|
||||
|
|
|
|||
39
README.md
39
README.md
|
|
@ -1,15 +1,15 @@
|
|||
# Real-Debrid Torrent & Usenet Client
|
||||
|
||||
This is a web interface to manage your torrents on Real-Debrid, AllDebrid, Premiumize, TorBox or DebridLink. It supports the following features:
|
||||
This is a web interface to manage your torrents on Real-Debrid, AllDebrid, Premiumize TorBox or DebridLink. It supports the following features:
|
||||
|
||||
- Add new torrents through magnets or files
|
||||
- Add usenet downloads through NZB files (TorBox and Premiumize only)
|
||||
- Add usenet downloads through NZB files (TorBox only)
|
||||
- Download all files from Real-Debrid, AllDebrid, Premiumize or TorBox to your local machine automatically
|
||||
- Unpack all files when finished downloading
|
||||
- Implements a fake qBittorrent API so you can hook up other applications like Sonarr, Radarr or Couchpotato.
|
||||
- Built with Angular 21 and .NET 10
|
||||
|
||||
**You will need a Premium service at Real-Debrid, AllDebrid, Premiumize, Torbox or DebridLink!**
|
||||
**You will need a Premium service at Real-Debrid, AllDebrid, Premiumize or Torbox!**
|
||||
|
||||
[Click here to sign up for Real-Debrid.](https://real-debrid.com/?id=1348683)
|
||||
|
||||
|
|
@ -166,9 +166,7 @@ It has the following options:
|
|||
|
||||
### Connecting Sonarr/Radarr
|
||||
|
||||
RdtClient emulates the qBittorrent web protocol and allows applications to use those APIs. This way you can use Sonarr and Radarr to download directly from RealDebrid.
|
||||
|
||||
#### Torrents
|
||||
RdtClient emulates the qBittorrent web protocol and allow applications to use those APIs. This way you can use Sonarr and Radarr to download directly from RealDebrid.
|
||||
|
||||
1. Login to Sonarr or Radarr and click `Settings`.
|
||||
1. Go to the `Download Client` tab and click the plus to add.
|
||||
|
|
@ -185,24 +183,6 @@ When downloading files it will append the `category` setting in the Sonarr/Radar
|
|||
|
||||
Notice: the progress and ETA reported in Sonarr's Activity tab will not be accurate, but it will report the torrent as completed so it can be processed after it is done downloading.
|
||||
|
||||
#### Usenet/NZB
|
||||
|
||||
RdtClient also emulates part of the SABnzbd API so Sonarr and Radarr can add NZB downloads. This requires a provider that supports Usenet/NZB downloads, currently TorBox or Premiumize.
|
||||
|
||||
1. Login to Sonarr or Radarr and click `Settings`.
|
||||
1. Go to the `Download Client` tab and click the plus to add.
|
||||
1. Click `SABnzbd` in the list.
|
||||
1. Enter the IP or hostname of RdtClient in the `Host` field.
|
||||
1. Enter `6500` in the `Port` field.
|
||||
1. Enable `Use SSL` only if you access RdtClient through HTTPS.
|
||||
1. Leave `URL Base` empty unless RdtClient is configured with a `BasePath`, for example `/rdt`.
|
||||
1. If RdtClient authentication is enabled, leave `API Key` empty and enter your RdtClient username and password. If your client only supports an API key, enter `{username}:{password}` in `API Key`.
|
||||
1. If RdtClient authentication is disabled, enter any value in `API Key`, for example `rdtclient`, and leave username/password empty.
|
||||
1. Set the category to `sonarr` for Sonarr or `radarr` for Radarr.
|
||||
1. Hit `Test` and then `Save` if all is well.
|
||||
|
||||
When importing completed NZB downloads, Sonarr/Radarr must be able to access the path reported by RdtClient. In Docker setups this may require a Remote Path Mapping from the RdtClient download path to the path mounted inside Sonarr/Radarr.
|
||||
|
||||
### Running within a folder
|
||||
|
||||
By default the application runs in the root of your hosted address (i.e. https://rdt.myserver.com/), but if you want to run it as a relative folder (i.e. https://myserver.com/rdt) you will have to change the `BasePath` setting in the `appsettings.json` file. You can set the `BASE_PATH` environment variable for docker enviroments.
|
||||
|
|
@ -218,17 +198,6 @@ By default the application runs in the root of your hosted address (i.e. https:/
|
|||
- Visual Studio 2025
|
||||
- (optional) Resharper
|
||||
|
||||
### Dev Container
|
||||
|
||||
The repository includes a dev container under `.devcontainer/` for the split development workflow used by this project.
|
||||
|
||||
It installs .NET 10 and Node 22, forwards ports `4200` and `6500`, and persists `/data/db` and `/data/downloads` in named volumes so the local SQLite database, logs, and downloads survive container rebuilds.
|
||||
|
||||
1. Open the repository in the dev container.
|
||||
1. In one terminal run `dotnet watch run --project server/RdtClient.Web`.
|
||||
1. In another terminal run `cd client && npm start`.
|
||||
1. Open `http://localhost:4200`. The Angular dev server proxies `/Api` and `/hub` to the backend running on `6500`.
|
||||
|
||||
1. Open the client folder project in VS Code and run `npm install`.
|
||||
1. To debug run `ng serve`, to build run `ng build -c production`.
|
||||
1. Open the Visual Studio 2025 project `RdtClient.sln` and `Publish` the `RdtClient.Web` to the given `PublishFolder` target.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const minorOnly = ["eslint", "typescript"];
|
||||
const minorOnly = ["eslint"];
|
||||
const patchOnly = [];
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
485
client/package-lock.json
generated
485
client/package-lock.json
generated
|
|
@ -8,42 +8,42 @@
|
|||
"name": "client",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^21.2.10",
|
||||
"@angular/cdk": "^21.2.8",
|
||||
"@angular/common": "^21.2.10",
|
||||
"@angular/compiler": "^21.2.10",
|
||||
"@angular/core": "^21.2.10",
|
||||
"@angular/forms": "^21.2.10",
|
||||
"@angular/platform-browser": "^21.2.10",
|
||||
"@angular/platform-browser-dynamic": "^21.2.10",
|
||||
"@angular/router": "^21.2.10",
|
||||
"@angular/animations": "^21.2.4",
|
||||
"@angular/cdk": "^21.2.2",
|
||||
"@angular/common": "^21.2.4",
|
||||
"@angular/compiler": "^21.2.4",
|
||||
"@angular/core": "^21.2.4",
|
||||
"@angular/forms": "^21.2.4",
|
||||
"@angular/platform-browser": "^21.2.4",
|
||||
"@angular/platform-browser-dynamic": "^21.2.4",
|
||||
"@angular/router": "^21.2.4",
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"@microsoft/signalr": "^10.0.0",
|
||||
"bulma": "^1.0.4",
|
||||
"curray": "^1.0.12",
|
||||
"file-saver-es": "^2.0.5",
|
||||
"filesize": "^11.0.17",
|
||||
"filesize": "^11.0.13",
|
||||
"rxjs": "^7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "^0.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-eslint/builder": "21.3.1",
|
||||
"@angular-eslint/eslint-plugin": "21.3.1",
|
||||
"@angular-eslint/eslint-plugin-template": "21.3.1",
|
||||
"@angular-eslint/schematics": "21.3.1",
|
||||
"@angular-eslint/template-parser": "21.3.1",
|
||||
"@angular/build": "^21.2.8",
|
||||
"@angular/cli": "^21.2.8",
|
||||
"@angular/compiler-cli": "^21.2.10",
|
||||
"@angular/language-service": "^21.2.10",
|
||||
"@angular-eslint/builder": "21.3.0",
|
||||
"@angular-eslint/eslint-plugin": "21.3.0",
|
||||
"@angular-eslint/eslint-plugin-template": "21.3.0",
|
||||
"@angular-eslint/schematics": "21.3.0",
|
||||
"@angular-eslint/template-parser": "21.3.0",
|
||||
"@angular/build": "^21.2.2",
|
||||
"@angular/cli": "^21.2.2",
|
||||
"@angular/compiler-cli": "^21.2.4",
|
||||
"@angular/language-service": "^21.2.4",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/file-saver-es": "^2.0.3",
|
||||
"@types/node": "^25.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
||||
"@typescript-eslint/parser": "^8.59.1",
|
||||
"@types/node": "^25.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"eslint": "^9.39.4",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
},
|
||||
|
|
@ -271,13 +271,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/architect": {
|
||||
"version": "0.2102.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.8.tgz",
|
||||
"integrity": "sha512-b7su7AHIO5F2I6InEu/Bx/oXvGjdCP7kos2tGX73he/lPrTuizooils62OgAzgJ2UeKscyRNUjBPieFCy6XvHQ==",
|
||||
"version": "0.2102.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.2.tgz",
|
||||
"integrity": "sha512-CDvFtXwyBtMRkTQnm+LfBNLL0yLV8ZGskrM1T6VkcGwXGFDott1FxUdj96ViodYsYL5fbJr0MNA6TlLcanV3kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "21.2.8",
|
||||
"@angular-devkit/core": "21.2.2",
|
||||
"rxjs": "7.8.2"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -290,16 +290,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/core": {
|
||||
"version": "21.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.8.tgz",
|
||||
"integrity": "sha512-DyxCILaaic/hfcfiBjAC/SdKE1ybSQIrU62/K5Msn3gZtThZj/T7cG0VHfbmpEFcgYkrQ9caUt6MCg8OoOVDzw==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.2.tgz",
|
||||
"integrity": "sha512-xUeKGe4BDQpkz0E6fnAPIJXE0y0nqtap0KhJIBhvN7xi3NenIzTmoi6T9Yv5OOBUdLZbOm4SOel8MhdXiIBpAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "8.18.0",
|
||||
"ajv-formats": "3.0.1",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"picomatch": "4.0.4",
|
||||
"picomatch": "4.0.3",
|
||||
"rxjs": "7.8.2",
|
||||
"source-map": "0.7.6"
|
||||
},
|
||||
|
|
@ -335,13 +335,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/schematics": {
|
||||
"version": "21.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.8.tgz",
|
||||
"integrity": "sha512-UTEMM1JXzzxufLsTGDsWth2E7+8e9PaFT7nbjUvJ2qevltACkiqAbHEpiD2ISzrSRIO3OirJ+cZtnzXO0FyoBQ==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.2.tgz",
|
||||
"integrity": "sha512-CCeyQxGUq+oyGnHd7PfcYIVbj9pRnqjQq0rAojoAqs1BJdtInx9weLBCLy+AjM3NHePeZrnwm+wEVr8apED8kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "21.2.8",
|
||||
"@angular-devkit/core": "21.2.2",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"magic-string": "0.30.21",
|
||||
"ora": "9.3.0",
|
||||
|
|
@ -354,9 +354,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/builder": {
|
||||
"version": "21.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.3.1.tgz",
|
||||
"integrity": "sha512-1f1Lyp5e7OH6txiV224HaY3G1uRCj91OSKq7hT2Vw9NRw6zWFc1anBpDeLVjpL9ptUxzUGIQR5jEV54hOPayoQ==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.3.0.tgz",
|
||||
"integrity": "sha512-26QUUouei52biUFAlJSrWNAU9tuF2miKwd8uHdxWwCF31xz+OxC5+NfudWvt1AFaYow7gWueX1QX3rNNtSPDrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -370,21 +370,21 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/bundled-angular-compiler": {
|
||||
"version": "21.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.3.1.tgz",
|
||||
"integrity": "sha512-jjbnJPUXQeQBJ8RM+ahlbt4GH2emVN8JvG3AhFbPci1FrqXi9cOOfkbwLmvpoyTli4LF8gy7g4ctFqnlRgqryw==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.3.0.tgz",
|
||||
"integrity": "sha512-l521I24J9gJxyMbRkrM24Tc7W8J8BP+TDAmVs2nT8+lXbS3kg8QpWBRtd+hNUgq6o+vt+lKBkytnEfu8OiqeRg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@angular-eslint/eslint-plugin": {
|
||||
"version": "21.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.3.1.tgz",
|
||||
"integrity": "sha512-08NNTxwawRLTWPLl8dg1BnXMwimx93y4wMEwx2aWQpJbIt4pmNvwJzd+NgoD/Ag2VdLS/gOMadhJH5fgaYKsPQ==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.3.0.tgz",
|
||||
"integrity": "sha512-Whf/AUUBekOlfSJRS78m76YGrBQAZ3waXE7oOdlW5xEQvn8jBDN9EGuNnjg/syZzvzjK4ZpYC4g1XYXrc+fQIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.1",
|
||||
"@angular-eslint/utils": "21.3.1",
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.0",
|
||||
"@angular-eslint/utils": "21.3.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
@ -394,19 +394,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/eslint-plugin-template": {
|
||||
"version": "21.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.3.1.tgz",
|
||||
"integrity": "sha512-ndPWJodkcEOu2PVUxlUwyz4D2u3r9KO7veWmStVNOLeNrICJA+nQvrz2BWCu0l48rO0K5ezsy0JFcQDVwE/5mw==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.3.0.tgz",
|
||||
"integrity": "sha512-lVixd/KypPWgA/5/pUOhJV9MTcaHjYZEqyOi+IiLk+h+maGxn6/s6Ot+20n+XGS85zAgOY+qUw6EEQ11hoojIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.1",
|
||||
"@angular-eslint/utils": "21.3.1",
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.0",
|
||||
"@angular-eslint/utils": "21.3.0",
|
||||
"aria-query": "5.3.2",
|
||||
"axobject-query": "4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular-eslint/template-parser": "21.3.1",
|
||||
"@angular-eslint/template-parser": "21.3.0",
|
||||
"@typescript-eslint/types": "^7.11.0 || ^8.0.0",
|
||||
"@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
|
|
@ -414,16 +414,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/schematics": {
|
||||
"version": "21.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.3.1.tgz",
|
||||
"integrity": "sha512-1U2u4ZsZvwT30aXRLsIJf6tULIiioo9BtASNsldpYecU3/m/1+F61lCYG79qt7YWbif9KABPYZlFTJUFGN8HWA==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.3.0.tgz",
|
||||
"integrity": "sha512-8deU/zVY9f8k8kAQQ9PL130ox2VlrZw3fMxgsPNAY5tjQ0xk0J2YVSszYHhcqdMGG1J01IsxIjvQaJ4pFfEmMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": ">= 21.0.0 < 22.0.0",
|
||||
"@angular-devkit/schematics": ">= 21.0.0 < 22.0.0",
|
||||
"@angular-eslint/eslint-plugin": "21.3.1",
|
||||
"@angular-eslint/eslint-plugin-template": "21.3.1",
|
||||
"@angular-eslint/eslint-plugin": "21.3.0",
|
||||
"@angular-eslint/eslint-plugin-template": "21.3.0",
|
||||
"ignore": "7.0.5",
|
||||
"semver": "7.7.4",
|
||||
"strip-json-comments": "3.1.1"
|
||||
|
|
@ -433,14 +433,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/template-parser": {
|
||||
"version": "21.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.3.1.tgz",
|
||||
"integrity": "sha512-moERVCTekQKOvR8RMuEOtWSO3VS1qrzA3keI1dPto/JVB8Nqp9w3R5ZpEoXHzh4zgEryosxmPgdi6UczJe2ouQ==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.3.0.tgz",
|
||||
"integrity": "sha512-ysyou1zAY6M6rSZNdIcYKGd4nk6TCapamyFNB3ivmTlVZ0O35TS9o/rJ0aUttuHgDp+Ysgs3ql+LA746PXgCyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.1",
|
||||
"eslint-scope": "9.1.2"
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.0",
|
||||
"eslint-scope": "^9.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
|
|
@ -467,13 +467,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/utils": {
|
||||
"version": "21.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.3.1.tgz",
|
||||
"integrity": "sha512-Q3SGA1/36phZhmsp1mYrKzp/jcmqofRr861MYn46FaWIKSYXBYRzl+H3FIJKBu5CE36Bggu6hbNpwGPuUp+MCg==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.3.0.tgz",
|
||||
"integrity": "sha512-oNigH6w3l+owTMboj/uFG0tHOy43uH8BpQRtBOQL1/s2+5in/BJ2Fjobv3SyizxTgeJ1FhRefbkT8GmVjK7jAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.1"
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
|
||||
|
|
@ -482,9 +482,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular/animations": {
|
||||
"version": "21.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.10.tgz",
|
||||
"integrity": "sha512-sIzAcxwtRCJ/fu0tK4mo1ooiEaDxJ+Nl6s9nK1D1NP1em12VX03Jx8CMixp/kVtgh4mZnm1x6psBB0FUz3U3Ug==",
|
||||
"version": "21.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.4.tgz",
|
||||
"integrity": "sha512-hO1P7ks9n7lW3D31bzHohSuoAaj05xJUlK8rZgX8IkH5DLx4qhvfNh0t4bbLuBJLP2r1TaLsQ8KFcemCkFRO2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
|
|
@ -493,18 +493,18 @@
|
|||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "21.2.10"
|
||||
"@angular/core": "21.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/build": {
|
||||
"version": "21.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.8.tgz",
|
||||
"integrity": "sha512-t0PHT7ONDMLwcjC9GaClNF+gsUKN78ofBikw4huiu6np5Rwmxp8KKCrdoRx20lOiibSolXgjZ2Ny0xxjNdNdQA==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.2.tgz",
|
||||
"integrity": "sha512-Vq2eIneNxzhHm1MwEmRqEJDwHU9ODfSRDaMWwtysGMhpoMQmLdfTqkQDmkC2qVUr8mV8Z1i5I+oe5ZJaMr/PlQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "2.3.0",
|
||||
"@angular-devkit/architect": "0.2102.8",
|
||||
"@angular-devkit/architect": "0.2102.2",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-annotate-as-pure": "7.27.3",
|
||||
"@babel/helper-split-export-declaration": "7.24.7",
|
||||
|
|
@ -520,15 +520,15 @@
|
|||
"magic-string": "0.30.21",
|
||||
"mrmime": "2.0.1",
|
||||
"parse5-html-rewriting-stream": "8.0.0",
|
||||
"picomatch": "4.0.4",
|
||||
"picomatch": "4.0.3",
|
||||
"piscina": "5.1.4",
|
||||
"rolldown": "1.0.0-rc.4",
|
||||
"sass": "1.97.3",
|
||||
"semver": "7.7.4",
|
||||
"source-map-support": "0.5.21",
|
||||
"tinyglobby": "0.2.15",
|
||||
"undici": "7.24.4",
|
||||
"vite": "7.3.2",
|
||||
"undici": "7.22.0",
|
||||
"vite": "7.3.1",
|
||||
"watchpack": "2.5.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -547,7 +547,7 @@
|
|||
"@angular/platform-browser": "^21.0.0",
|
||||
"@angular/platform-server": "^21.0.0",
|
||||
"@angular/service-worker": "^21.0.0",
|
||||
"@angular/ssr": "^21.2.8",
|
||||
"@angular/ssr": "^21.2.2",
|
||||
"karma": "^6.4.0",
|
||||
"less": "^4.2.0",
|
||||
"ng-packagr": "^21.0.0",
|
||||
|
|
@ -597,9 +597,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular/cdk": {
|
||||
"version": "21.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.8.tgz",
|
||||
"integrity": "sha512-WdvMLpuFcRgDWLDyin3sw5a65PQYdI0Y+4BxiMxOkesoZ2RZTBAlLKIfQ9Nz5CY3LamUTO3Qel2T8ZhJ+Cqfuw==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.2.tgz",
|
||||
"integrity": "sha512-9AsZkwqy07No7+0qPydcJfXB6SpA9qLDBanoesNj5KsiZJ62PJH3oIjVyNeQEEe1HQWmSwBnhwN12OPLNMUlnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse5": "^8.0.0",
|
||||
|
|
@ -613,19 +613,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular/cli": {
|
||||
"version": "21.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.8.tgz",
|
||||
"integrity": "sha512-Y+/US12o+7X2774oeKPsEfHeeYM2SxwnyoXfcaLR8vrMn0zxUrhHebmlz9h83th4EJEuex1Qk0JtF7j5vcwrqQ==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.2.tgz",
|
||||
"integrity": "sha512-eZo8/qX+ZIpIWc0CN+cCX13Lbgi/031wAp8DRVhDDO6SMVtcr/ObOQ2S16+pQdOMXxiG3vby6IhzJuz9WACzMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-devkit/architect": "0.2102.8",
|
||||
"@angular-devkit/core": "21.2.8",
|
||||
"@angular-devkit/schematics": "21.2.8",
|
||||
"@angular-devkit/architect": "0.2102.2",
|
||||
"@angular-devkit/core": "21.2.2",
|
||||
"@angular-devkit/schematics": "21.2.2",
|
||||
"@inquirer/prompts": "7.10.1",
|
||||
"@listr2/prompt-adapter-inquirer": "3.0.5",
|
||||
"@modelcontextprotocol/sdk": "1.26.0",
|
||||
"@schematics/angular": "21.2.8",
|
||||
"@schematics/angular": "21.2.2",
|
||||
"@yarnpkg/lockfile": "1.1.0",
|
||||
"algoliasearch": "5.48.1",
|
||||
"ini": "6.0.0",
|
||||
|
|
@ -648,9 +648,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular/common": {
|
||||
"version": "21.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.10.tgz",
|
||||
"integrity": "sha512-WLyi/CRLtgALg2mmaqIuKuPnE4i+8PGt/uuz26pVqx+ASh28/TWr5KSCAMomgxEc8kt4OE7lopoQsTihrQCfEw==",
|
||||
"version": "21.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.4.tgz",
|
||||
"integrity": "sha512-NrP6qOuUpo3fqq14UJ1b2bIRtWsfvxh1qLqOyFV4gfBrHhXd0XffU1LUlUw1qp4w1uBSgPJ0/N5bSPUWrAguVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
|
|
@ -659,14 +659,14 @@
|
|||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "21.2.10",
|
||||
"@angular/core": "21.2.4",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/compiler": {
|
||||
"version": "21.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.10.tgz",
|
||||
"integrity": "sha512-IrgdFuzzD7NTK3WQaSfowjAPxPbnTqsgR92NsOs5ZaWu3RgLl21dHThNc0BK1KwVwppLUSWmD4qePbcLW71VzQ==",
|
||||
"version": "21.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.4.tgz",
|
||||
"integrity": "sha512-9+ulVK3idIo/Tu4X2ic7/V0+Uj7pqrOAbOuIirYe6Ymm3AjexuFRiGBbfcH0VJhQ5cf8TvIJ1fuh+MI4JiRIxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
|
|
@ -676,9 +676,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular/compiler-cli": {
|
||||
"version": "21.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.10.tgz",
|
||||
"integrity": "sha512-FDcnj3ogRmnTca4m2GbKP2khFOCtoVvWDZyfw2ZCPAf+zsQlKTyscKvx4GpTFo+KHrYXpawUpDIWHORFpuqFEA==",
|
||||
"version": "21.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.4.tgz",
|
||||
"integrity": "sha512-vGjd7DZo/Ox50pQCm5EycmBu91JclimPtZoyNXu/2hSxz3oAkzwiHCwlHwk2g58eheSSp+lYtYRLmHAqSVZLjg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -699,7 +699,7 @@
|
|||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "21.2.10",
|
||||
"@angular/compiler": "21.2.4",
|
||||
"typescript": ">=5.9 <6.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
|
|
@ -709,9 +709,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular/core": {
|
||||
"version": "21.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.10.tgz",
|
||||
"integrity": "sha512-uxH+mbPiCE7rInWKYOPe9Ytas97+mFM6FhFORoN234yBK3b8he+iDuxX6dsbhEFCxhRmfS6hLxe7BdLY6U6kIA==",
|
||||
"version": "21.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.4.tgz",
|
||||
"integrity": "sha512-2+gd67ZuXHpGOqeb2o7XZPueEWEP81eJza2tSHkT5QMV8lnYllDEmaNnkPxnIjSLGP1O3PmiXxo4z8ibHkLZwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
|
|
@ -720,7 +720,7 @@
|
|||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "21.2.10",
|
||||
"@angular/compiler": "21.2.4",
|
||||
"rxjs": "^6.5.3 || ^7.4.0",
|
||||
"zone.js": "~0.15.0 || ~0.16.0"
|
||||
},
|
||||
|
|
@ -734,9 +734,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular/forms": {
|
||||
"version": "21.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.10.tgz",
|
||||
"integrity": "sha512-XOo9qkuBqCLzSBXmyga9ke2tSulxWl+E7Y9Uwqgz8sJtQUlyP/0GYJfu60jiC3NAYobk9K/6h6MsU8zftQKdaA==",
|
||||
"version": "21.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.4.tgz",
|
||||
"integrity": "sha512-1fOhctA9ADEBYjI3nPQUR5dHsK2+UWAjup37Ksldk/k0w8UpD5YsN7JVNvsDMZRFMucKYcGykPblU7pABtsqnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
|
|
@ -746,16 +746,16 @@
|
|||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "21.2.10",
|
||||
"@angular/core": "21.2.10",
|
||||
"@angular/platform-browser": "21.2.10",
|
||||
"@angular/common": "21.2.4",
|
||||
"@angular/core": "21.2.4",
|
||||
"@angular/platform-browser": "21.2.4",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/language-service": {
|
||||
"version": "21.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-21.2.10.tgz",
|
||||
"integrity": "sha512-9f5XgigqdscuCmUZ2WAbuCKgOZfq6dennEOsvqjfposnaU90Y01cD9O75xKf4ONEANOfTwVcQCqGZeZ6Cnpdvw==",
|
||||
"version": "21.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-21.2.4.tgz",
|
||||
"integrity": "sha512-seWlXWhayTwuL62Cfz+Ky/Wv67oYLX+cXplYoIinDVJPgQaU9jXpakLfKq8RwdRXVmgTE0HQ5dyoTozuWgJ8Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -763,9 +763,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser": {
|
||||
"version": "21.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.10.tgz",
|
||||
"integrity": "sha512-5WMoHGU8BOV3eO9h3vGMIUDPf+3SHis7+X2dHKMtKfFBUtiO8m/lq2x3PzkkKj1782i7KYt92EqPHuADd/eWOw==",
|
||||
"version": "21.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.4.tgz",
|
||||
"integrity": "sha512-1A9e/cQVu+3BkRCktLcO3RZGuw8NOTHw1frUUrpAz+iMyvIT4sDRFbL+U1g8qmOCZqRNC1Pi1HZfZ1kl6kvrcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
|
|
@ -774,9 +774,9 @@
|
|||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "21.2.10",
|
||||
"@angular/common": "21.2.10",
|
||||
"@angular/core": "21.2.10"
|
||||
"@angular/animations": "21.2.4",
|
||||
"@angular/common": "21.2.4",
|
||||
"@angular/core": "21.2.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@angular/animations": {
|
||||
|
|
@ -785,9 +785,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser-dynamic": {
|
||||
"version": "21.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.10.tgz",
|
||||
"integrity": "sha512-+/HMJSLnF87EODkHj0AKE3Q8AfYO/8jpTfr731QmplqBtCoLlA/1XR8aYow2hB9YKL9HZWDb2qGkRtCXhrtt+w==",
|
||||
"version": "21.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.4.tgz",
|
||||
"integrity": "sha512-LRJLnGh4rdgD0+S5xuDd4YRm5bV8WP2e6F1Pe5rIr6N4V9ofgpB0/uOjYy9se99FJZjoyPnpxaKsp8+XA753Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
|
|
@ -796,16 +796,16 @@
|
|||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "21.2.10",
|
||||
"@angular/compiler": "21.2.10",
|
||||
"@angular/core": "21.2.10",
|
||||
"@angular/platform-browser": "21.2.10"
|
||||
"@angular/common": "21.2.4",
|
||||
"@angular/compiler": "21.2.4",
|
||||
"@angular/core": "21.2.4",
|
||||
"@angular/platform-browser": "21.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/router": {
|
||||
"version": "21.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.10.tgz",
|
||||
"integrity": "sha512-4cHHwewIhFEAAaRgJ80371EOtNlydFHbjj/UENLZitjU0azal0mfFCBdkaEdVehd7+mH5xO7MRjy6eFTcTYR5Q==",
|
||||
"version": "21.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.4.tgz",
|
||||
"integrity": "sha512-OjWze4XT8i2MThcBXMv7ru1k6/5L6QYZbcXuseqimFCHm2avEJ+mXPovY066fMBZJhqbXdjB82OhHAWkIHjglQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
|
|
@ -814,9 +814,9 @@
|
|||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "21.2.10",
|
||||
"@angular/core": "21.2.10",
|
||||
"@angular/platform-browser": "21.2.10",
|
||||
"@angular/common": "21.2.4",
|
||||
"@angular/core": "21.2.4",
|
||||
"@angular/platform-browser": "21.2.4",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
|
|
@ -1114,38 +1114,35 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"@emnapi/wasi-threads": "1.2.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
|
|
@ -2941,22 +2938,20 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@npmcli/agent": {
|
||||
|
|
@ -4074,14 +4069,14 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@schematics/angular": {
|
||||
"version": "21.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.8.tgz",
|
||||
"integrity": "sha512-Kx3PmuZIXhwQqAqoERAXqDCORHFbKTMd+eflXwZfpKkrbWJTVPqKpL4R9RVdEr2E6/VEXDFrdL1whIvGd1xmDg==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.2.tgz",
|
||||
"integrity": "sha512-Ywa6HDtX7TRBQZTVMMnxX3Mk7yVnG8KtSFaXWrkx779+q8tqYdBwNwAqbNd4Zatr1GccKaz9xcptHJta5+DTxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "21.2.8",
|
||||
"@angular-devkit/schematics": "21.2.8",
|
||||
"@angular-devkit/core": "21.2.2",
|
||||
"@angular-devkit/schematics": "21.2.2",
|
||||
"jsonc-parser": "3.3.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -4247,30 +4242,30 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
|
||||
"integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz",
|
||||
"integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/type-utils": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/type-utils": "8.57.0",
|
||||
"@typescript-eslint/utils": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -4280,22 +4275,22 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.59.1",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
|
||||
"integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
|
||||
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -4307,18 +4302,18 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz",
|
||||
"integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz",
|
||||
"integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.1",
|
||||
"@typescript-eslint/types": "^8.59.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.57.0",
|
||||
"@typescript-eslint/types": "^8.57.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -4329,18 +4324,18 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
|
||||
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz",
|
||||
"integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1"
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -4351,9 +4346,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz",
|
||||
"integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -4364,21 +4359,21 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz",
|
||||
"integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0",
|
||||
"@typescript-eslint/utils": "8.57.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -4389,13 +4384,13 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
|
||||
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz",
|
||||
"integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -4407,21 +4402,21 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
|
||||
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz",
|
||||
"integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.59.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"@typescript-eslint/project-service": "8.57.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -4431,20 +4426,20 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
|
||||
"integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz",
|
||||
"integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1"
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -4455,17 +4450,17 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
|
||||
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz",
|
||||
"integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -6017,9 +6012,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filesize": {
|
||||
"version": "11.0.17",
|
||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.17.tgz",
|
||||
"integrity": "sha512-oHLTvMLw6imZUl1se/RBQrFlyy50nXce4sU7yGR6Qc0JgCwqnfiFsAnEwotdGmfKLD7SArGUk2/5STU0k8LOBQ==",
|
||||
"version": "11.0.13",
|
||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.13.tgz",
|
||||
"integrity": "sha512-mYJ/qXKvREuO0uH8LTQJ6v7GsUvVOguqxg2VTwQUkyTPXXRRWPdjuUPVqdBrJQhvci48OHlNGRnux+Slr2Rnvw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 10.8.0"
|
||||
|
|
@ -7932,9 +7927,9 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -8041,9 +8036,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
||||
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
|
@ -8927,9 +8922,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -9003,9 +8998,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.24.4",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
|
||||
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
|
||||
"version": "7.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz",
|
||||
"integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -9013,9 +9008,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -9136,9 +9131,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -13,42 +13,42 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^21.2.10",
|
||||
"@angular/cdk": "^21.2.8",
|
||||
"@angular/common": "^21.2.10",
|
||||
"@angular/compiler": "^21.2.10",
|
||||
"@angular/core": "^21.2.10",
|
||||
"@angular/forms": "^21.2.10",
|
||||
"@angular/platform-browser": "^21.2.10",
|
||||
"@angular/platform-browser-dynamic": "^21.2.10",
|
||||
"@angular/router": "^21.2.10",
|
||||
"@angular/animations": "^21.2.4",
|
||||
"@angular/cdk": "^21.2.2",
|
||||
"@angular/common": "^21.2.4",
|
||||
"@angular/compiler": "^21.2.4",
|
||||
"@angular/core": "^21.2.4",
|
||||
"@angular/forms": "^21.2.4",
|
||||
"@angular/platform-browser": "^21.2.4",
|
||||
"@angular/platform-browser-dynamic": "^21.2.4",
|
||||
"@angular/router": "^21.2.4",
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"@microsoft/signalr": "^10.0.0",
|
||||
"bulma": "^1.0.4",
|
||||
"curray": "^1.0.12",
|
||||
"file-saver-es": "^2.0.5",
|
||||
"filesize": "^11.0.17",
|
||||
"filesize": "^11.0.13",
|
||||
"rxjs": "^7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "^0.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-eslint/builder": "21.3.1",
|
||||
"@angular-eslint/eslint-plugin": "21.3.1",
|
||||
"@angular-eslint/eslint-plugin-template": "21.3.1",
|
||||
"@angular-eslint/schematics": "21.3.1",
|
||||
"@angular-eslint/template-parser": "21.3.1",
|
||||
"@angular/build": "^21.2.8",
|
||||
"@angular/cli": "^21.2.8",
|
||||
"@angular/compiler-cli": "^21.2.10",
|
||||
"@angular/language-service": "^21.2.10",
|
||||
"@angular-eslint/builder": "21.3.0",
|
||||
"@angular-eslint/eslint-plugin": "21.3.0",
|
||||
"@angular-eslint/eslint-plugin-template": "21.3.0",
|
||||
"@angular-eslint/schematics": "21.3.0",
|
||||
"@angular-eslint/template-parser": "21.3.0",
|
||||
"@angular/build": "^21.2.2",
|
||||
"@angular/cli": "^21.2.2",
|
||||
"@angular/compiler-cli": "^21.2.4",
|
||||
"@angular/language-service": "^21.2.4",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/file-saver-es": "^2.0.3",
|
||||
"@types/node": "^25.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
||||
"@typescript-eslint/parser": "^8.59.1",
|
||||
"@types/node": "^25.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"eslint": "^9.39.4",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { TorrentService } from 'src/app/torrent.service';
|
||||
import { DownloadType, Torrent, TorrentFileAvailability } from '../models/torrent.model';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgClass } from '@angular/common';
|
||||
import { TorrentService } from '../torrent.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-new-torrent',
|
||||
|
|
@ -16,7 +15,6 @@ import { TorrentService } from '../torrent.service';
|
|||
standalone: true,
|
||||
})
|
||||
export class AddNewTorrentComponent implements OnInit {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private router = inject(Router);
|
||||
private torrentService = inject(TorrentService);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
|
@ -31,9 +29,8 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
public provider: string;
|
||||
public downloadClient: number;
|
||||
|
||||
private _category = '';
|
||||
public category: string;
|
||||
public categories: string[] = [];
|
||||
public filteredCategories: string[] = [];
|
||||
public categoryDropdownOpen = false;
|
||||
public hostDownloadAction: number = 0;
|
||||
public downloadAction: number = 0;
|
||||
|
|
@ -59,19 +56,10 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
public excludeRegexError: string;
|
||||
public regexSelected: TorrentFileAvailability[];
|
||||
|
||||
private selectedFile: File | null = null;
|
||||
|
||||
public get category(): string {
|
||||
return this._category;
|
||||
}
|
||||
|
||||
public set category(value: string) {
|
||||
this._category = value;
|
||||
this.updateFilteredCategories();
|
||||
}
|
||||
private selectedFile: File;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activatedRoute.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
|
||||
this.activatedRoute.queryParams.subscribe((params) => {
|
||||
if (params['type'] === 'nzb') {
|
||||
this.type = 'nzb';
|
||||
} else if (params['type'] === 'torrent') {
|
||||
|
|
@ -87,56 +75,45 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
this.type = 'nzb';
|
||||
}
|
||||
});
|
||||
this.settingsService.get().subscribe((settings) => {
|
||||
const providerSetting = settings.find((m) => m.key === 'Provider:Provider');
|
||||
this.provider = providerSetting.enumValues[providerSetting.value as number];
|
||||
this.downloadClient = settings.find((m) => m.key === 'DownloadClient:Client')?.value as number;
|
||||
|
||||
this.settingsService
|
||||
.get()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((settings) => {
|
||||
const providerSetting = settings.find((m) => m.key === 'Provider:Provider');
|
||||
this.provider = providerSetting.enumValues[providerSetting.value as number];
|
||||
this.downloadClient = settings.find((m) => m.key === 'DownloadClient:Client')?.value as number;
|
||||
this.category = settings.find((m) => m.key === 'Gui:Default:Category')?.value as string;
|
||||
const categoriesSetting = settings.find((m) => m.key === 'General:Categories')?.value as string;
|
||||
this.categories = (categoriesSetting ?? '')
|
||||
.split(',')
|
||||
.map((c) => c.trim())
|
||||
.filter((c) => c.length > 0)
|
||||
.filter((c, i, arr) => arr.findIndex((a) => a.toLowerCase() === c.toLowerCase()) === i);
|
||||
const matchedCategory = this.categories.find((c) => c.toLowerCase() === (this.category ?? '').toLowerCase());
|
||||
if (matchedCategory) {
|
||||
this.category = matchedCategory;
|
||||
}
|
||||
this.hostDownloadAction = this.downloadAction = settings.find((m) => m.key === 'Gui:Default:HostDownloadAction')
|
||||
?.value as number;
|
||||
this.downloadAction =
|
||||
settings.find((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0;
|
||||
this.finishedAction = settings.find((m) => m.key === 'Gui:Default:FinishedAction')?.value as number;
|
||||
this.finishedActionDelay = settings.find((m) => m.key == 'Gui:Default:FinishedActionDelay')?.value as number;
|
||||
this.downloadMinSize = settings.find((m) => m.key === 'Gui:Default:MinFileSize')?.value as number;
|
||||
this.includeRegex = settings.find((m) => m.key === 'Gui:Default:IncludeRegex')?.value as string;
|
||||
this.excludeRegex = settings.find((m) => m.key === 'Gui:Default:ExcludeRegex')?.value as string;
|
||||
this.torrentRetryAttempts = settings.find((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number;
|
||||
this.downloadRetryAttempts = settings.find((m) => m.key === 'Gui:Default:DownloadRetryAttempts')?.value as number;
|
||||
this.torrentDeleteOnError = settings.find((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number;
|
||||
this.torrentLifetime = settings.find((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number;
|
||||
this.priority = settings.find((m) => m.key === 'Gui:Default:Priority')?.value as number;
|
||||
|
||||
this.category = settings.find((m) => m.key === 'Gui:Default:Category')?.value as string;
|
||||
const categoriesSetting = settings.find((m) => m.key === 'General:Categories')?.value as string;
|
||||
this.categories = (categoriesSetting ?? '')
|
||||
.split(',')
|
||||
.map((c) => c.trim())
|
||||
.filter((c) => c.length > 0)
|
||||
.filter((c, i, arr) => arr.findIndex((a) => a.toLowerCase() === c.toLowerCase()) === i);
|
||||
const matchedCategory = this.categories.find((c) => c.toLowerCase() === (this.category ?? '').toLowerCase());
|
||||
if (matchedCategory) {
|
||||
this.category = matchedCategory;
|
||||
} else {
|
||||
this.updateFilteredCategories();
|
||||
}
|
||||
this.hostDownloadAction = this.downloadAction = settings.find((m) => m.key === 'Gui:Default:HostDownloadAction')
|
||||
?.value as number;
|
||||
this.downloadAction =
|
||||
settings.find((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0;
|
||||
this.finishedAction = settings.find((m) => m.key === 'Gui:Default:FinishedAction')?.value as number;
|
||||
this.finishedActionDelay = settings.find((m) => m.key == 'Gui:Default:FinishedActionDelay')?.value as number;
|
||||
this.downloadMinSize = settings.find((m) => m.key === 'Gui:Default:MinFileSize')?.value as number;
|
||||
this.includeRegex = settings.find((m) => m.key === 'Gui:Default:IncludeRegex')?.value as string;
|
||||
this.excludeRegex = settings.find((m) => m.key === 'Gui:Default:ExcludeRegex')?.value as string;
|
||||
this.torrentRetryAttempts = settings.find((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number;
|
||||
this.downloadRetryAttempts = settings.find((m) => m.key === 'Gui:Default:DownloadRetryAttempts')
|
||||
?.value as number;
|
||||
this.torrentDeleteOnError = settings.find((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number;
|
||||
this.torrentLifetime = settings.find((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number;
|
||||
this.priority = settings.find((m) => m.key === 'Gui:Default:Priority')?.value as number;
|
||||
|
||||
this.setFinishAction();
|
||||
});
|
||||
this.setFinishAction();
|
||||
});
|
||||
}
|
||||
|
||||
private updateFilteredCategories(): void {
|
||||
if (!this.category) {
|
||||
this.filteredCategories = this.categories;
|
||||
return;
|
||||
}
|
||||
|
||||
get filteredCategories(): string[] {
|
||||
if (!this.category) return this.categories;
|
||||
const search = this.category.toLowerCase();
|
||||
this.filteredCategories = this.categories.filter((value) => value.toLowerCase().includes(search));
|
||||
return this.categories.filter((c) => c.toLowerCase().includes(search));
|
||||
}
|
||||
|
||||
public selectCategory(cat: string): void {
|
||||
|
|
@ -167,7 +144,7 @@ export class AddNewTorrentComponent implements OnInit {
|
|||
public pickFile(evt: Event): void {
|
||||
const files = (evt.target as HTMLInputElement).files;
|
||||
|
||||
if (files == null || files.length === 0) {
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,12 +44,9 @@ export class Torrent {
|
|||
public rdSpeed: number;
|
||||
public rdSeeders: number;
|
||||
public rdFiles: string;
|
||||
public statusText?: string;
|
||||
public filesCount?: number;
|
||||
public downloadsCount?: number;
|
||||
|
||||
public files?: TorrentFile[];
|
||||
public downloads?: Download[];
|
||||
public files: TorrentFile[];
|
||||
public downloads: Download[];
|
||||
}
|
||||
|
||||
export class TorrentFile {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { NavigationEnd, Router, RouterLink } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Profile } from '../models/profile.model';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { NgClass, DatePipe } from '@angular/common';
|
||||
import { filter } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
|
|
@ -15,7 +13,6 @@ import { filter } from 'rxjs';
|
|||
standalone: true,
|
||||
})
|
||||
export class NavbarComponent implements OnInit {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private settingsService = inject(SettingsService);
|
||||
private authService = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
|
|
@ -27,48 +24,39 @@ export class NavbarComponent implements OnInit {
|
|||
public version: string;
|
||||
|
||||
constructor() {
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.showMobileMenu = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsService
|
||||
.getProfile()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((result) => {
|
||||
this.profile = result;
|
||||
this.settingsService.getProfile().subscribe((result) => {
|
||||
this.profile = result;
|
||||
|
||||
switch (result.provider) {
|
||||
case 'RealDebrid':
|
||||
this.providerLink = 'https://real-debrid.com/?id=1348683';
|
||||
break;
|
||||
case 'AllDebrid':
|
||||
this.providerLink = 'https://alldebrid.com/?uid=2v91l&lang=en';
|
||||
break;
|
||||
case 'Premiumize':
|
||||
this.providerLink = 'https://www.premiumize.me/';
|
||||
break;
|
||||
case 'TorBox':
|
||||
this.providerLink = 'https://torbox.app/';
|
||||
break;
|
||||
case 'DebridLink':
|
||||
this.providerLink = 'https://debrid-link.com/';
|
||||
break;
|
||||
}
|
||||
});
|
||||
switch (result.provider) {
|
||||
case 'RealDebrid':
|
||||
this.providerLink = 'https://real-debrid.com/?id=1348683';
|
||||
break;
|
||||
case 'AllDebrid':
|
||||
this.providerLink = 'https://alldebrid.com/?uid=2v91l&lang=en';
|
||||
break;
|
||||
case 'Premiumize':
|
||||
this.providerLink = 'https://www.premiumize.me/';
|
||||
break;
|
||||
case 'TorBox':
|
||||
this.providerLink = 'https://torbox.app/';
|
||||
break;
|
||||
case 'DebridLink':
|
||||
this.providerLink = 'https://debrid-link.com/';
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.settingsService
|
||||
.getVersion()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((result) => {
|
||||
this.version = result.version;
|
||||
});
|
||||
this.settingsService.getVersion().subscribe((result) => {
|
||||
this.version = result.version;
|
||||
});
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { SettingsService } from 'src/app/settings.service';
|
||||
import { Setting } from '../models/setting.model';
|
||||
import { NgClass, KeyValuePipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Nl2BrPipe } from '../nl2br.pipe';
|
||||
import { FileSizePipe } from '../filesize.pipe';
|
||||
import { SettingsService } from '../settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
|
|
|
|||
|
|
@ -1,78 +1,20 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export function getSortFieldValue(item: unknown, field: string): unknown {
|
||||
if (item == null || !field) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return field.split('.').reduce<unknown>((value, key) => {
|
||||
if (value == null || typeof value !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (value as Record<string, unknown>)[key];
|
||||
}, item);
|
||||
}
|
||||
|
||||
function compareSortValues(left: unknown, right: unknown): number {
|
||||
if (left === right) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (left == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (right == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (typeof left === 'string' && typeof right === 'string') {
|
||||
return left.localeCompare(right, undefined, { sensitivity: 'base' });
|
||||
}
|
||||
|
||||
if (left instanceof Date && right instanceof Date) {
|
||||
return left.getTime() - right.getTime();
|
||||
}
|
||||
|
||||
if (typeof left === 'boolean' && typeof right === 'boolean') {
|
||||
return Number(left) - Number(right);
|
||||
}
|
||||
|
||||
const comparableLeft = left as string | number | bigint;
|
||||
const comparableRight = right as string | number | bigint;
|
||||
|
||||
if (comparableLeft < comparableRight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (comparableLeft > comparableRight) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function sortItems<T>(
|
||||
array: readonly T[],
|
||||
field: string,
|
||||
order: SortDirection = 'asc',
|
||||
accessor: (item: T, field: string) => unknown = getSortFieldValue,
|
||||
): T[] {
|
||||
if (!Array.isArray(array)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const direction = order === 'asc' ? 1 : -1;
|
||||
|
||||
return [...array].sort((left, right) => compareSortValues(accessor(left, field), accessor(right, field)) * direction);
|
||||
}
|
||||
|
||||
@Pipe({ name: 'sort' })
|
||||
export class SortPipe implements PipeTransform {
|
||||
transform(array: unknown[], field: string, order: SortDirection = 'asc'): unknown[] {
|
||||
return sortItems(array, field, order);
|
||||
transform(array: any[], field: string, order: 'asc' | 'desc' = 'asc'): any[] {
|
||||
if (!Array.isArray(array)) {
|
||||
return [];
|
||||
}
|
||||
const sortedArray = array.sort((a, b) => {
|
||||
if (a[field] < b[field]) {
|
||||
return -1;
|
||||
} else if (a[field] > b[field]) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
return order === 'asc' ? sortedArray : sortedArray.reverse();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,132 +1,99 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { Pipe, PipeTransform, inject } from '@angular/core';
|
||||
import { RealDebridStatus, Torrent } from './models/torrent.model';
|
||||
import { FileSizePipe } from './filesize.pipe';
|
||||
|
||||
const fileSizePipe = new FileSizePipe();
|
||||
@Pipe({ name: 'status' })
|
||||
export class TorrentStatusPipe implements PipeTransform {
|
||||
private pipe = inject(FileSizePipe);
|
||||
|
||||
export function getTorrentStatus(torrent: Torrent): string {
|
||||
if (torrent.error) {
|
||||
return torrent.error;
|
||||
}
|
||||
transform(torrent: Torrent): string {
|
||||
if (torrent.error) {
|
||||
return torrent.error;
|
||||
}
|
||||
|
||||
const downloads = torrent.downloads ?? [];
|
||||
if (torrent.downloads.length > 0) {
|
||||
const allFinished = torrent.downloads.every((m) => m.completed != null);
|
||||
|
||||
if (downloads.length > 0) {
|
||||
let allFinished = true;
|
||||
let downloadingCount = 0;
|
||||
let downloadedCount = 0;
|
||||
let downloadingBytesDone = 0;
|
||||
let downloadingBytesTotal = 0;
|
||||
let downloadingSpeed = 0;
|
||||
let unpackingCount = 0;
|
||||
let unpackedCount = 0;
|
||||
let unpackingBytesDone = 0;
|
||||
let unpackingBytesTotal = 0;
|
||||
let queuedForUnpackingCount = 0;
|
||||
let queuedForDownloadingCount = 0;
|
||||
|
||||
for (const download of downloads) {
|
||||
if (download.completed == null) {
|
||||
allFinished = false;
|
||||
if (allFinished) {
|
||||
return 'Finished';
|
||||
}
|
||||
|
||||
if (download.downloadFinished != null) {
|
||||
downloadedCount += 1;
|
||||
const downloading = torrent.downloads.filter((m) => m.downloadStarted && !m.downloadFinished && m.bytesDone > 0);
|
||||
const downloaded = torrent.downloads.filter((m) => m.downloadFinished != null);
|
||||
|
||||
if (downloading.length > 0) {
|
||||
const bytesDone = downloading.reduce((sum, m) => sum + m.bytesDone, 0);
|
||||
const bytesTotal = downloading.reduce((sum, m) => sum + m.bytesTotal, 0);
|
||||
const progress = (bytesDone / bytesTotal || 0) * 100;
|
||||
|
||||
const allSpeeds = downloading.reduce((sum, m) => sum + m.speed, 0);
|
||||
|
||||
const speed: string | string[] = this.pipe.transform(allSpeeds, 'filesize');
|
||||
|
||||
return `Downloading file ${downloading.length + downloaded.length}/${
|
||||
torrent.downloads.length
|
||||
} (${progress.toFixed(2)}% - ${speed}/s)`;
|
||||
}
|
||||
|
||||
if (download.downloadStarted && !download.downloadFinished && download.bytesDone > 0) {
|
||||
downloadingCount += 1;
|
||||
downloadingBytesDone += download.bytesDone;
|
||||
downloadingBytesTotal += download.bytesTotal;
|
||||
downloadingSpeed += download.speed;
|
||||
const unpacking = torrent.downloads.filter((m) => m.unpackingStarted && !m.unpackingFinished && m.bytesDone > 0);
|
||||
const unpacked = torrent.downloads.filter((m) => m.unpackingFinished != null);
|
||||
|
||||
if (unpacking.length > 0) {
|
||||
const bytesDone = unpacking.reduce((sum, m) => sum + m.bytesDone, 0);
|
||||
const bytesTotal = unpacking.reduce((sum, m) => sum + m.bytesTotal, 0);
|
||||
const progress = (bytesDone / bytesTotal || 0) * 100;
|
||||
|
||||
return `Extracting file ${unpacking.length + unpacked.length}/${torrent.downloads.length} (${progress.toFixed(
|
||||
2,
|
||||
)}%)`;
|
||||
}
|
||||
|
||||
if (download.unpackingFinished != null) {
|
||||
unpackedCount += 1;
|
||||
const queuedForUnpacking = torrent.downloads.filter((m) => m.unpackingQueued && !m.unpackingStarted);
|
||||
|
||||
if (queuedForUnpacking.length > 0) {
|
||||
return `Queued for unpacking`;
|
||||
}
|
||||
|
||||
if (download.unpackingStarted && !download.unpackingFinished && download.bytesDone > 0) {
|
||||
unpackingCount += 1;
|
||||
unpackingBytesDone += download.bytesDone;
|
||||
unpackingBytesTotal += download.bytesTotal;
|
||||
const queuedForDownload = torrent.downloads.filter((m) => !m.downloadStarted && !m.downloadFinished);
|
||||
|
||||
if (queuedForDownload.length > 0) {
|
||||
return `Queued for downloading`;
|
||||
}
|
||||
|
||||
if (download.unpackingQueued && !download.unpackingStarted) {
|
||||
queuedForUnpackingCount += 1;
|
||||
if (unpacked.length > 0) {
|
||||
return `Files unpacked`;
|
||||
}
|
||||
|
||||
if (!download.downloadStarted && !download.downloadFinished) {
|
||||
queuedForDownloadingCount += 1;
|
||||
if (downloaded.length > 0) {
|
||||
return `Files downloaded to host`;
|
||||
}
|
||||
}
|
||||
|
||||
if (allFinished) {
|
||||
if (torrent.completed) {
|
||||
return 'Finished';
|
||||
}
|
||||
|
||||
if (downloadingCount > 0) {
|
||||
const progress = ((downloadingBytesDone / downloadingBytesTotal) || 0) * 100;
|
||||
const speed = fileSizePipe.transform(downloadingSpeed, 'filesize') as string;
|
||||
|
||||
return `Downloading file ${downloadingCount + downloadedCount}/${downloads.length} (${progress.toFixed(2)}% - ${speed}/s)`;
|
||||
switch (torrent.rdStatus) {
|
||||
case RealDebridStatus.Queued:
|
||||
return 'Not Yet Added to Provider';
|
||||
case RealDebridStatus.Downloading:
|
||||
if (torrent.rdSeeders < 1 && torrent.type !== 1) {
|
||||
return `Torrent stalled`;
|
||||
}
|
||||
const speed = this.pipe.transform(torrent.rdSpeed, 'filesize');
|
||||
return `Torrent downloading (${torrent.rdProgress}% - ${speed}/s)`;
|
||||
case RealDebridStatus.Processing:
|
||||
return `Torrent processing`;
|
||||
case RealDebridStatus.WaitingForFileSelection:
|
||||
return `Torrent waiting for file selection`;
|
||||
case RealDebridStatus.Error:
|
||||
return `Torrent error: ${torrent.rdStatusRaw}`;
|
||||
case RealDebridStatus.Finished:
|
||||
return `Torrent finished, waiting for download links`;
|
||||
case RealDebridStatus.Uploading:
|
||||
return `Torrent uploading`;
|
||||
default:
|
||||
return 'Unknown status';
|
||||
}
|
||||
|
||||
if (unpackingCount > 0) {
|
||||
const progress = ((unpackingBytesDone / unpackingBytesTotal) || 0) * 100;
|
||||
|
||||
return `Extracting file ${unpackingCount + unpackedCount}/${downloads.length} (${progress.toFixed(2)}%)`;
|
||||
}
|
||||
|
||||
if (queuedForUnpackingCount > 0) {
|
||||
return 'Queued for unpacking';
|
||||
}
|
||||
|
||||
if (queuedForDownloadingCount > 0) {
|
||||
return 'Queued for downloading';
|
||||
}
|
||||
|
||||
if (unpackedCount > 0) {
|
||||
return 'Files unpacked';
|
||||
}
|
||||
|
||||
if (downloadedCount > 0) {
|
||||
return 'Files downloaded to host';
|
||||
}
|
||||
}
|
||||
|
||||
if (torrent.completed) {
|
||||
return 'Finished';
|
||||
}
|
||||
|
||||
const prefix = torrent.type === 1 ? 'NZB' : 'Torrent';
|
||||
|
||||
switch (torrent.rdStatus) {
|
||||
case RealDebridStatus.Queued:
|
||||
return 'Not Yet Added to Provider';
|
||||
case RealDebridStatus.Downloading:
|
||||
if (torrent.rdSeeders < 1 && torrent.type !== 1) {
|
||||
return 'Torrent stalled';
|
||||
}
|
||||
|
||||
return `${prefix} downloading (${torrent.rdProgress}% - ${fileSizePipe.transform(torrent.rdSpeed, 'filesize') as string}/s)`;
|
||||
case RealDebridStatus.Processing:
|
||||
return `${prefix} processing`;
|
||||
case RealDebridStatus.WaitingForFileSelection:
|
||||
return `${prefix} waiting for file selection`;
|
||||
case RealDebridStatus.Error:
|
||||
return `${prefix} error: ${torrent.rdStatusRaw}`;
|
||||
case RealDebridStatus.Finished:
|
||||
return `${prefix} finished, waiting for download links`;
|
||||
case RealDebridStatus.Uploading:
|
||||
return `${prefix} uploading`;
|
||||
default:
|
||||
return 'Unknown status';
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({ name: 'status' })
|
||||
export class TorrentStatusPipe implements PipeTransform {
|
||||
transform(torrent: Torrent): string {
|
||||
return getTorrentStatus(torrent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,10 +47,14 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (torrent of sortedTorrents; track torrent.torrentId) {
|
||||
@for (torrent of torrents | sort: sortProperty : sortDirection; track torrent.torrentId) {
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" (click)="toggleSelect(torrent.torrentId)" [checked]="isSelected(torrent.torrentId)" />
|
||||
<input
|
||||
type="checkbox"
|
||||
(click)="toggleSelect(torrent.torrentId)"
|
||||
[checked]="selectedTorrents.includes(torrent.torrentId)"
|
||||
/>
|
||||
</td>
|
||||
<td (click)="openTorrent(torrent.torrentId)" class="break-all">
|
||||
{{ torrent.rdName }}
|
||||
|
|
@ -65,10 +69,10 @@
|
|||
{{ torrent.rdSeeders }}
|
||||
</td>
|
||||
<td>
|
||||
{{ (torrent.filesCount ?? torrent.files?.length ?? 0) | number }}
|
||||
{{ torrent.files.length | number }}
|
||||
</td>
|
||||
<td>
|
||||
{{ (torrent.downloadsCount ?? torrent.downloads?.length ?? 0) | number }}
|
||||
{{ torrent.downloads.length | number }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.rdSize | filesize }}
|
||||
|
|
@ -77,7 +81,7 @@
|
|||
{{ torrent.added | date: "medium" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.statusText ?? (torrent | status) }}
|
||||
{{ torrent | status }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
|
@ -85,19 +89,19 @@
|
|||
</table>
|
||||
} @else {
|
||||
<div class="mobile-cards">
|
||||
@for (torrent of sortedTorrents; track torrent.torrentId) {
|
||||
<div class="box mobile-card" [class.is-selected]="isSelected(torrent.torrentId)">
|
||||
@for (torrent of torrents | sort: sortProperty : sortDirection; track torrent.torrentId) {
|
||||
<div class="box mobile-card" [class.is-selected]="selectedTorrents.includes(torrent.torrentId)">
|
||||
<div class="mobile-card-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
(click)="toggleSelect(torrent.torrentId)"
|
||||
[checked]="isSelected(torrent.torrentId)"
|
||||
[checked]="selectedTorrents.includes(torrent.torrentId)"
|
||||
/>
|
||||
</div>
|
||||
<div class="mobile-card-body" (click)="openTorrent(torrent.torrentId)">
|
||||
<p class="mobile-card-name">{{ torrent.rdName }}</p>
|
||||
<div class="tags mb-2">
|
||||
<span class="tag is-info is-light">{{ torrent.statusText ?? (torrent | status) }}</span>
|
||||
<span class="tag is-info is-light">{{ torrent | status }}</span>
|
||||
@if (torrent.category) {
|
||||
<span class="tag is-light">{{ torrent.category }}</span>
|
||||
}
|
||||
|
|
@ -108,10 +112,10 @@
|
|||
>
|
||||
<span class="mobile-card-detail"><span class="detail-label">Seeders</span> {{ torrent.rdSeeders }}</span>
|
||||
<span class="mobile-card-detail"
|
||||
><span class="detail-label">Files</span> {{ (torrent.filesCount ?? torrent.files?.length ?? 0) | number }}</span
|
||||
><span class="detail-label">Files</span> {{ torrent.files.length | number }}</span
|
||||
>
|
||||
<span class="mobile-card-detail"
|
||||
><span class="detail-label">Downloads</span> {{ (torrent.downloadsCount ?? torrent.downloads?.length ?? 0) | number }}</span
|
||||
><span class="detail-label">Downloads</span> {{ torrent.downloads.length | number }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Torrent } from '../models/torrent.model';
|
||||
import { DiskSpaceStatus } from '../models/disk-space-status.model';
|
||||
|
|
@ -8,29 +7,26 @@ import { TorrentService } from '../torrent.service';
|
|||
import { forkJoin, Observable } from 'rxjs';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgClass, DecimalPipe, DatePipe } from '@angular/common';
|
||||
import { getTorrentStatus, TorrentStatusPipe } from '../torrent-status.pipe';
|
||||
import { SortDirection, getSortFieldValue, sortItems } from '../sort.pipe';
|
||||
import { TorrentStatusPipe } from '../torrent-status.pipe';
|
||||
import { SortPipe } from '../sort.pipe';
|
||||
import { FileSizePipe } from '../filesize.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-torrent-table',
|
||||
templateUrl: './torrent-table.component.html',
|
||||
styleUrls: ['./torrent-table.component.scss'],
|
||||
imports: [FormsModule, NgClass, DecimalPipe, DatePipe, TorrentStatusPipe, FileSizePipe],
|
||||
imports: [FormsModule, NgClass, DecimalPipe, DatePipe, TorrentStatusPipe, SortPipe, FileSizePipe],
|
||||
standalone: true,
|
||||
})
|
||||
export class TorrentTableComponent implements OnInit, OnDestroy {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private router = inject(Router);
|
||||
private torrentService = inject(TorrentService);
|
||||
private selectedTorrentIds = new Set<string>();
|
||||
|
||||
public torrents: Torrent[] = [];
|
||||
public sortedTorrents: Torrent[] = [];
|
||||
public selectedTorrents: string[] = [];
|
||||
public error: string;
|
||||
public sortProperty = 'added';
|
||||
public sortDirection: SortDirection = 'desc';
|
||||
public sortDirection: 'asc' | 'desc' = 'desc';
|
||||
|
||||
public isDeleteModalActive: boolean;
|
||||
public deleteError: string;
|
||||
|
|
@ -86,47 +82,38 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
// Ignore storage errors (e.g., disabled storage)
|
||||
}
|
||||
|
||||
this.torrentService
|
||||
.getDiskSpaceStatus()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (status) => {
|
||||
this.diskSpaceStatus = status;
|
||||
},
|
||||
});
|
||||
this.torrentService.getDiskSpaceStatus().subscribe({
|
||||
next: (status) => {
|
||||
this.diskSpaceStatus = status;
|
||||
},
|
||||
});
|
||||
|
||||
this.torrentService.diskSpaceStatus$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((status) => {
|
||||
this.torrentService.diskSpaceStatus$.subscribe((status) => {
|
||||
this.diskSpaceStatus = status;
|
||||
});
|
||||
|
||||
this.torrentService
|
||||
.getRateLimitStatus()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (status) => {
|
||||
this.rateLimitStatus = status;
|
||||
},
|
||||
});
|
||||
this.torrentService.getRateLimitStatus().subscribe({
|
||||
next: (status) => {
|
||||
this.rateLimitStatus = status;
|
||||
},
|
||||
});
|
||||
|
||||
this.torrentService.rateLimitStatus$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((status) => {
|
||||
this.torrentService.rateLimitStatus$.subscribe((status) => {
|
||||
this.rateLimitStatus = status;
|
||||
});
|
||||
|
||||
this.torrentService.update$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((result) => {
|
||||
this.setTorrents(result);
|
||||
this.torrentService.update$.subscribe((result) => {
|
||||
this.torrents = result;
|
||||
});
|
||||
|
||||
this.torrentService
|
||||
.getList()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.setTorrents(result);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
},
|
||||
});
|
||||
this.torrentService.getList().subscribe({
|
||||
next: (result) => {
|
||||
this.torrents = result;
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = err.error;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public sort(property: string): void {
|
||||
|
|
@ -144,33 +131,30 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
} catch (_) {
|
||||
// Ignore storage errors
|
||||
}
|
||||
|
||||
this.applySorting();
|
||||
}
|
||||
|
||||
public openTorrent(torrentId: string): void {
|
||||
this.router.navigate([`/torrent/${torrentId}`]);
|
||||
}
|
||||
|
||||
public toggleDeleteSelectAll(event: Event) {
|
||||
const checked = (event.target as HTMLInputElement).checked;
|
||||
public toggleDeleteSelectAll(event: any) {
|
||||
this.selectedTorrents = [];
|
||||
|
||||
this.selectedTorrentIds = checked ? new Set(this.torrents.map((torrent) => torrent.torrentId)) : new Set<string>();
|
||||
this.syncSelectedTorrents();
|
||||
if (event.target.checked) {
|
||||
this.torrents.map((torrent) => {
|
||||
this.selectedTorrents.push(torrent.torrentId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public toggleSelect(torrentId: string) {
|
||||
if (this.selectedTorrentIds.has(torrentId)) {
|
||||
this.selectedTorrentIds.delete(torrentId);
|
||||
const index = this.selectedTorrents.indexOf(torrentId);
|
||||
|
||||
if (index > -1) {
|
||||
this.selectedTorrents.splice(index, 1);
|
||||
} else {
|
||||
this.selectedTorrentIds.add(torrentId);
|
||||
this.selectedTorrents.push(torrentId);
|
||||
}
|
||||
|
||||
this.syncSelectedTorrents();
|
||||
}
|
||||
|
||||
public isSelected(torrentId: string): boolean {
|
||||
return this.selectedTorrentIds.has(torrentId);
|
||||
}
|
||||
|
||||
public showDeleteModal(): void {
|
||||
|
|
@ -200,7 +184,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
this.isDeleteModalActive = false;
|
||||
this.deleting = false;
|
||||
|
||||
this.clearSelectedTorrents();
|
||||
this.selectedTorrents = [];
|
||||
},
|
||||
error: (err) => {
|
||||
this.deleteError = err.error;
|
||||
|
|
@ -233,7 +217,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
this.isRetryModalActive = false;
|
||||
this.retrying = false;
|
||||
|
||||
this.clearSelectedTorrents();
|
||||
this.selectedTorrents = [];
|
||||
},
|
||||
error: (err) => {
|
||||
this.retryError = err.error;
|
||||
|
|
@ -245,7 +229,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
public changeSettingsModal(): void {
|
||||
this.changeSettingsError = null;
|
||||
|
||||
const selectedTorrents = this.getSelectedTorrentModels();
|
||||
const selectedTorrents = this.torrents.filter((m) => this.selectedTorrents.indexOf(m.torrentId) > -1);
|
||||
|
||||
this.updateSettingsDownloadClient = selectedTorrents.every(
|
||||
(m, _, arr) => m.downloadClient === arr[0].downloadClient,
|
||||
|
|
@ -292,7 +276,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
|
||||
const calls: Observable<void>[] = [];
|
||||
|
||||
const selectedTorrents = this.getSelectedTorrentModels();
|
||||
const selectedTorrents = this.torrents.filter((m) => this.selectedTorrents.indexOf(m.torrentId) > -1);
|
||||
|
||||
selectedTorrents.forEach((torrent) => {
|
||||
if (this.updateSettingsDownloadClient != null) {
|
||||
|
|
@ -308,7 +292,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
torrent.priority = this.updateSettingsPriority;
|
||||
}
|
||||
if (this.updateSettingsDownloadRetryAttempts != null) {
|
||||
torrent.downloadRetryAttempts = this.updateSettingsDownloadRetryAttempts;
|
||||
torrent.retryCount = this.updateSettingsDownloadRetryAttempts;
|
||||
}
|
||||
if (this.updateSettingsTorrentRetryAttempts != null) {
|
||||
torrent.torrentRetryAttempts = this.updateSettingsTorrentRetryAttempts;
|
||||
|
|
@ -328,7 +312,7 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
this.isChangeSettingsModalActive = false;
|
||||
this.changingSettings = false;
|
||||
|
||||
this.clearSelectedTorrents();
|
||||
this.selectedTorrents = [];
|
||||
},
|
||||
error: (err) => {
|
||||
this.changeSettingsError = err.error;
|
||||
|
|
@ -349,52 +333,4 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
ngOnDestroy(): void {
|
||||
this.mobileQuery?.removeEventListener('change', this.mobileQueryListener);
|
||||
}
|
||||
|
||||
private setTorrents(torrents: Torrent[]): void {
|
||||
this.torrents = torrents;
|
||||
this.pruneSelectedTorrents();
|
||||
this.applySorting();
|
||||
}
|
||||
|
||||
private applySorting(): void {
|
||||
this.sortedTorrents = sortItems(this.torrents, this.sortProperty, this.sortDirection, (torrent, field) => {
|
||||
switch (field) {
|
||||
case 'files.length':
|
||||
return torrent.filesCount ?? torrent.files?.length ?? 0;
|
||||
case 'downloads.length':
|
||||
return torrent.downloadsCount ?? torrent.downloads?.length ?? 0;
|
||||
case 'status':
|
||||
return torrent.statusText ?? getTorrentStatus(torrent);
|
||||
default:
|
||||
return getSortFieldValue(torrent, field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getSelectedTorrentModels(): Torrent[] {
|
||||
return this.torrents.filter((torrent) => this.selectedTorrentIds.has(torrent.torrentId));
|
||||
}
|
||||
|
||||
private pruneSelectedTorrents(): void {
|
||||
const torrentIds = new Set(this.torrents.map((torrent) => torrent.torrentId));
|
||||
|
||||
for (const torrentId of this.selectedTorrentIds) {
|
||||
if (!torrentIds.has(torrentId)) {
|
||||
this.selectedTorrentIds.delete(torrentId);
|
||||
}
|
||||
}
|
||||
|
||||
this.syncSelectedTorrents();
|
||||
}
|
||||
|
||||
private clearSelectedTorrents(): void {
|
||||
this.selectedTorrentIds.clear();
|
||||
this.syncSelectedTorrents();
|
||||
}
|
||||
|
||||
private syncSelectedTorrents(): void {
|
||||
this.selectedTorrents = this.torrents
|
||||
.filter((torrent) => this.selectedTorrentIds.has(torrent.torrentId))
|
||||
.map((torrent) => torrent.torrentId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { saveAs } from 'file-saver-es';
|
||||
import { Torrent } from '../models/torrent.model';
|
||||
|
|
@ -11,7 +10,6 @@ import { TorrentStatusPipe } from '../torrent-status.pipe';
|
|||
import { DownloadStatusPipe } from '../download-status.pipe';
|
||||
import { DecodeURIPipe } from '../decode-uri.pipe';
|
||||
import { FileSizePipe } from '../filesize.pipe';
|
||||
import { EMPTY, distinctUntilChanged, map, switchMap, tap, catchError } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-torrent',
|
||||
|
|
@ -30,11 +28,9 @@ import { EMPTY, distinctUntilChanged, map, switchMap, tap, catchError } from 'rx
|
|||
standalone: true,
|
||||
})
|
||||
export class TorrentComponent implements OnInit {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private torrentService = inject(TorrentService);
|
||||
private currentTorrentId: string | null = null;
|
||||
|
||||
public torrent: Torrent;
|
||||
|
||||
|
|
@ -75,65 +71,41 @@ export class TorrentComponent implements OnInit {
|
|||
public updating: boolean;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activatedRoute.params
|
||||
.pipe(
|
||||
map((params) => params['id'] as string),
|
||||
distinctUntilChanged(),
|
||||
tap((torrentId) => {
|
||||
this.currentTorrentId = torrentId;
|
||||
}),
|
||||
switchMap((torrentId) =>
|
||||
this.torrentService.get(torrentId).pipe(
|
||||
catchError(() => {
|
||||
this.router.navigate(['/']);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((torrent) => {
|
||||
this.torrent = torrent;
|
||||
this.currentTorrentId = torrent.torrentId;
|
||||
});
|
||||
this.activatedRoute.params.subscribe((params) => {
|
||||
const torrentId = params['id'];
|
||||
|
||||
this.torrentService.update$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((result) => {
|
||||
this.update(result);
|
||||
this.torrentService.get(torrentId).subscribe({
|
||||
next: (torrent) => {
|
||||
this.torrent = torrent;
|
||||
|
||||
this.torrentService.update$.subscribe((result) => {
|
||||
this.update(result);
|
||||
});
|
||||
},
|
||||
error: () => this.router.navigate(['/']),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public update(torrents: Torrent[]): void {
|
||||
const torrentId = this.currentTorrentId ?? this.torrent?.torrentId;
|
||||
|
||||
if (!torrentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedTorrent = torrents.find((m) => m.torrentId === torrentId);
|
||||
const updatedTorrent = torrents.find((m) => m.torrentId === this.torrent.torrentId);
|
||||
|
||||
if (updatedTorrent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTorrent = this.torrent;
|
||||
const hasIncomingFiles = Array.isArray(updatedTorrent.files) && updatedTorrent.files.length > 0;
|
||||
|
||||
this.torrent = {
|
||||
...currentTorrent,
|
||||
...updatedTorrent,
|
||||
fileOrMagnet: updatedTorrent.fileOrMagnet ?? currentTorrent?.fileOrMagnet,
|
||||
files: hasIncomingFiles ? updatedTorrent.files : currentTorrent?.files ?? [],
|
||||
downloads: updatedTorrent.downloads ?? currentTorrent?.downloads ?? [],
|
||||
};
|
||||
this.torrent = updatedTorrent;
|
||||
}
|
||||
|
||||
public download(): void {
|
||||
const binaryString = window.atob(this.torrent.fileOrMagnet);
|
||||
const byteArray = new Uint8Array(binaryString.length);
|
||||
|
||||
for (let index = 0; index < binaryString.length; index += 1) {
|
||||
byteArray[index] = binaryString.charCodeAt(index);
|
||||
}
|
||||
const byteArray = new Uint8Array(
|
||||
window
|
||||
.atob(this.torrent.fileOrMagnet)
|
||||
.split('')
|
||||
.map(function (c) {
|
||||
return c.charCodeAt(0);
|
||||
}),
|
||||
);
|
||||
|
||||
const blob = new Blob([byteArray], { type: 'application/x-bittorrent' });
|
||||
saveAs(blob, `${this.torrent.rdName}.torrent`);
|
||||
|
|
|
|||
|
|
@ -16,14 +16,6 @@ public class DataContext(DbContextOptions options) : IdentityDbContext(options)
|
|||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
builder.Entity<Download>()
|
||||
.HasIndex(m => new
|
||||
{
|
||||
m.TorrentId,
|
||||
m.Path
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
var cascadeFKs = builder.Model.GetEntityTypes()
|
||||
.SelectMany(t => t.GetForeignKeys())
|
||||
.Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade);
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
namespace RdtClient.Data.Data;
|
||||
|
||||
public enum DownloadAddResult
|
||||
{
|
||||
Added,
|
||||
AlreadyExists,
|
||||
TorrentMissing,
|
||||
InvalidInput
|
||||
}
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using Download = RdtClient.Data.Models.Data.Download;
|
||||
|
||||
namespace RdtClient.Data.Data;
|
||||
|
||||
public class DownloadData(DataContext dataContext, ILogger<DownloadData>? logger = null)
|
||||
public class DownloadData(DataContext dataContext)
|
||||
{
|
||||
public async Task<List<Download>> GetForTorrent(Guid torrentId)
|
||||
{
|
||||
|
|
@ -32,29 +30,8 @@ public class DownloadData(DataContext dataContext, ILogger<DownloadData>? logger
|
|||
.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
|
||||
}
|
||||
|
||||
public async Task<DownloadAddResult> TryAddForTorrent(Guid torrentId, DownloadInfo downloadInfo)
|
||||
public async Task<Download> Add(Guid torrentId, DownloadInfo downloadInfo)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(downloadInfo.RestrictedLink))
|
||||
{
|
||||
logger?.LogDebug("Skipped download creation because the restricted link was blank. TorrentId: {torrentId}", torrentId);
|
||||
|
||||
return DownloadAddResult.InvalidInput;
|
||||
}
|
||||
|
||||
if (!await dataContext.Torrents.AsNoTracking().AnyAsync(m => m.TorrentId == torrentId))
|
||||
{
|
||||
logger?.LogDebug("Skipped download creation because the torrent no longer exists. TorrentId: {torrentId}, Path: {path}", torrentId, downloadInfo.RestrictedLink);
|
||||
|
||||
return DownloadAddResult.TorrentMissing;
|
||||
}
|
||||
|
||||
if (await dataContext.Downloads.AsNoTracking().AnyAsync(m => m.TorrentId == torrentId && m.Path == downloadInfo.RestrictedLink))
|
||||
{
|
||||
logger?.LogDebug("Skipped download creation because it already exists. TorrentId: {torrentId}, Path: {path}", torrentId, downloadInfo.RestrictedLink);
|
||||
|
||||
return DownloadAddResult.AlreadyExists;
|
||||
}
|
||||
|
||||
var download = new Download
|
||||
{
|
||||
DownloadId = Guid.NewGuid(),
|
||||
|
|
@ -68,36 +45,9 @@ public class DownloadData(DataContext dataContext, ILogger<DownloadData>? logger
|
|||
|
||||
await dataContext.Downloads.AddAsync(download);
|
||||
|
||||
try
|
||||
{
|
||||
await dataContext.SaveChangesAsync();
|
||||
await dataContext.SaveChangesAsync();
|
||||
|
||||
return DownloadAddResult.Added;
|
||||
}
|
||||
|
||||
// These shouldn't be possible any longer, but added for safety and until confirmed.
|
||||
catch (DbUpdateException ex)
|
||||
{
|
||||
dataContext.Entry(download).State = EntityState.Detached;
|
||||
|
||||
if (IsDuplicateDownloadViolation(ex))
|
||||
{
|
||||
logger?.LogDebug("Skipped download creation after a concurrent duplicate insert. TorrentId: {torrentId}, Path: {path}", torrentId, downloadInfo.RestrictedLink);
|
||||
|
||||
return DownloadAddResult.AlreadyExists;
|
||||
}
|
||||
|
||||
if (IsForeignKeyViolation(ex) && !await dataContext.Torrents.AsNoTracking().AnyAsync(m => m.TorrentId == torrentId))
|
||||
{
|
||||
logger?.LogDebug("Skipped download creation after the torrent was deleted concurrently. TorrentId: {torrentId}, Path: {path}",
|
||||
torrentId,
|
||||
downloadInfo.RestrictedLink);
|
||||
|
||||
return DownloadAddResult.TorrentMissing;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
return download;
|
||||
}
|
||||
|
||||
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
|
||||
|
|
@ -276,7 +226,7 @@ public class DownloadData(DataContext dataContext, ILogger<DownloadData>? logger
|
|||
await dataContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task Reset(Guid downloadId, DateTimeOffset? downloadQueued = null)
|
||||
public async Task Reset(Guid downloadId)
|
||||
{
|
||||
var dbDownload = await dataContext.Downloads
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId)
|
||||
|
|
@ -285,7 +235,7 @@ public class DownloadData(DataContext dataContext, ILogger<DownloadData>? logger
|
|||
dbDownload.RetryCount = 0;
|
||||
dbDownload.Link = null;
|
||||
dbDownload.Added = DateTimeOffset.UtcNow;
|
||||
dbDownload.DownloadQueued = downloadQueued ?? DateTimeOffset.UtcNow;
|
||||
dbDownload.DownloadQueued = DateTimeOffset.UtcNow;
|
||||
dbDownload.DownloadStarted = null;
|
||||
dbDownload.DownloadFinished = null;
|
||||
dbDownload.UnpackingQueued = null;
|
||||
|
|
@ -296,20 +246,4 @@ public class DownloadData(DataContext dataContext, ILogger<DownloadData>? logger
|
|||
|
||||
await dataContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static Boolean IsDuplicateDownloadViolation(DbUpdateException exception)
|
||||
{
|
||||
var sqliteException = exception.InnerException as SqliteException;
|
||||
|
||||
return sqliteException?.SqliteExtendedErrorCode == 2067
|
||||
|| sqliteException?.Message.Contains("UNIQUE constraint failed: Downloads.TorrentId, Downloads.Path", StringComparison.Ordinal) == true;
|
||||
}
|
||||
|
||||
private static Boolean IsForeignKeyViolation(DbUpdateException exception)
|
||||
{
|
||||
var sqliteException = exception.InnerException as SqliteException;
|
||||
|
||||
return sqliteException?.SqliteExtendedErrorCode == 787
|
||||
|| sqliteException?.Message.Contains("FOREIGN KEY constraint failed", StringComparison.Ordinal) == true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
|
||||
namespace RdtClient.Data.Data;
|
||||
|
||||
public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger = null) : ITorrentData
|
||||
public class TorrentData(DataContext dataContext) : ITorrentData
|
||||
{
|
||||
public async Task<IList<Torrent>> Get()
|
||||
{
|
||||
|
|
@ -13,7 +12,6 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
|||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Include(m => m.Downloads)
|
||||
.OrderBy(m => m.Priority ?? 9999)
|
||||
.ToListAsync();
|
||||
|
||||
return torrents.OrderBy(m => m.Priority ?? 9999)
|
||||
|
|
@ -288,23 +286,15 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
|
|||
|
||||
public async Task Delete(Guid torrentId)
|
||||
{
|
||||
await using var transaction = await dataContext.Database.BeginTransactionAsync();
|
||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||
|
||||
await dataContext.Downloads
|
||||
.Where(m => m.TorrentId == torrentId)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
var deletedTorrents = await dataContext.Torrents
|
||||
.Where(m => m.TorrentId == torrentId)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
if (deletedTorrents == 0)
|
||||
if (dbTorrent == null)
|
||||
{
|
||||
logger?.LogDebug("Skipped torrent graph deletion because the torrent was not found. TorrentId: {torrentId}", torrentId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dataContext.Torrents.Remove(dbTorrent);
|
||||
|
||||
await dataContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using RdtClient.Data.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RdtClient.Data.Migrations;
|
||||
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20260423031719_Downloads_Add_TorrentIdPath_Unique")]
|
||||
public partial class Downloads_Add_TorrentIdPath_Unique : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Orphaned downloads from previous failures.
|
||||
migrationBuilder.Sql("""
|
||||
DELETE FROM Downloads
|
||||
WHERE rowid NOT IN (
|
||||
SELECT MIN(rowid)
|
||||
FROM Downloads
|
||||
GROUP BY TorrentId, Path
|
||||
);
|
||||
""");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Downloads_TorrentId",
|
||||
table: "Downloads");
|
||||
|
||||
// Prevent accidental duplicates, provides idempotency at the DB level for downloads
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Downloads_TorrentId_Path",
|
||||
table: "Downloads",
|
||||
columns: ["TorrentId", "Path"],
|
||||
unique: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Downloads_TorrentId_Path",
|
||||
table: "Downloads");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Downloads_TorrentId",
|
||||
table: "Downloads",
|
||||
column: "TorrentId");
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ namespace RdtClient.Data.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
|
|
@ -263,8 +263,7 @@ namespace RdtClient.Data.Migrations
|
|||
|
||||
b.HasKey("DownloadId");
|
||||
|
||||
b.HasIndex("TorrentId", "Path")
|
||||
.IsUnique();
|
||||
b.HasIndex("TorrentId");
|
||||
|
||||
b.ToTable("Downloads");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@ namespace RdtClient.Data.Models.Data;
|
|||
|
||||
public class Torrent
|
||||
{
|
||||
private String? _rdFiles;
|
||||
private IList<DebridClientFile> _filesCache = [];
|
||||
private Boolean _filesCacheInitialized;
|
||||
|
||||
[Key]
|
||||
public Guid TorrentId { get; set; }
|
||||
|
||||
|
|
@ -63,52 +59,26 @@ public class Torrent
|
|||
public DateTimeOffset? RdEnded { get; set; }
|
||||
public Int64? RdSpeed { get; set; }
|
||||
public Int64? RdSeeders { get; set; }
|
||||
|
||||
public String? RdFiles
|
||||
{
|
||||
get => _rdFiles;
|
||||
set
|
||||
{
|
||||
if (_rdFiles == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_rdFiles = value;
|
||||
_filesCache = [];
|
||||
_filesCacheInitialized = false;
|
||||
}
|
||||
}
|
||||
public String? RdFiles { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public IList<DebridClientFile> Files
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filesCacheInitialized)
|
||||
{
|
||||
return _filesCache;
|
||||
}
|
||||
|
||||
_filesCacheInitialized = true;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(RdFiles))
|
||||
{
|
||||
_filesCache = [];
|
||||
|
||||
return _filesCache;
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_filesCache = JsonSerializer.Deserialize<List<DebridClientFile>>(RdFiles) ?? [];
|
||||
return JsonSerializer.Deserialize<List<DebridClientFile>>(RdFiles) ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
_filesCache = [];
|
||||
return [];
|
||||
}
|
||||
|
||||
return _filesCache;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ public class TorrentDto
|
|||
public DateTimeOffset? FilesSelected { get; set; }
|
||||
public DateTimeOffset? Completed { get; set; }
|
||||
public DownloadType Type { get; set; }
|
||||
public String? FileOrMagnet { get; set; }
|
||||
public Boolean IsFile { get; set; }
|
||||
public Int32? Priority { get; set; }
|
||||
public Int32 RetryCount { get; set; }
|
||||
|
|
@ -42,9 +41,6 @@ public class TorrentDto
|
|||
public DateTimeOffset? RdEnded { get; set; }
|
||||
public Int64? RdSpeed { get; set; }
|
||||
public Int64? RdSeeders { get; set; }
|
||||
public String StatusText { get; set; } = null!;
|
||||
public Int32 FilesCount { get; set; }
|
||||
public Int32 DownloadsCount { get; set; }
|
||||
public IList<DebridClientFile> Files { get; set; } = [];
|
||||
public IList<DownloadDto> Downloads { get; set; } = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,21 +4,6 @@ namespace RdtClient.Data.Models.QBittorrent;
|
|||
|
||||
public class TorrentFileItem
|
||||
{
|
||||
[JsonPropertyName("index")]
|
||||
public Int32 Index { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public String? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public Int64 Size { get; set; }
|
||||
|
||||
[JsonPropertyName("progress")]
|
||||
public Single Progress { get; set; }
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public Int32 Priority { get; set; }
|
||||
|
||||
[JsonPropertyName("is_seed")]
|
||||
public Boolean IsSeed { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,6 @@ public class TorrentProperties
|
|||
[JsonPropertyName("completion_date")]
|
||||
public Int64? CompletionDate { get; set; }
|
||||
|
||||
[JsonPropertyName("is_private")]
|
||||
public Boolean IsPrivate { get; set; }
|
||||
|
||||
[JsonPropertyName("created_by")]
|
||||
public String? CreatedBy { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ using System.Reflection;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.BackgroundServices;
|
||||
using RdtClient.Service.Services;
|
||||
|
||||
|
|
@ -15,7 +17,6 @@ public class WatchFolderCheckerTests : IDisposable
|
|||
private readonly Mock<IServiceProvider> _serviceProviderMock;
|
||||
private readonly Mock<IServiceScope> _serviceScopeMock;
|
||||
private readonly String _testPath;
|
||||
private readonly TestSettings _settings;
|
||||
private readonly Mock<Torrents> _torrentsServiceMock;
|
||||
|
||||
public WatchFolderCheckerTests()
|
||||
|
|
@ -24,18 +25,7 @@ public class WatchFolderCheckerTests : IDisposable
|
|||
_serviceProviderMock = new();
|
||||
_serviceScopeMock = new();
|
||||
_scopeServiceProviderMock = new();
|
||||
|
||||
_settings = new(new()
|
||||
{
|
||||
Watch = new()
|
||||
{
|
||||
Interval = 0,
|
||||
Default = new()
|
||||
},
|
||||
DownloadClient = new()
|
||||
});
|
||||
|
||||
_torrentsServiceMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState());
|
||||
_torrentsServiceMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
|
||||
|
||||
_serviceProviderMock
|
||||
.Setup(x => x.GetService(typeof(IServiceScopeFactory)))
|
||||
|
|
@ -52,8 +42,10 @@ public class WatchFolderCheckerTests : IDisposable
|
|||
_testPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(_testPath);
|
||||
|
||||
// Reset Settings and Startup
|
||||
SetStartupReady(true);
|
||||
_settings.Current.Watch.Path = _testPath;
|
||||
ResetSettings();
|
||||
Settings.Get.Watch.Path = _testPath;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -70,6 +62,22 @@ public class WatchFolderCheckerTests : IDisposable
|
|||
property?.SetValue(null, ready);
|
||||
}
|
||||
|
||||
private static void ResetSettings()
|
||||
{
|
||||
var settings = new DbSettings
|
||||
{
|
||||
Watch = new()
|
||||
{
|
||||
Interval = 0,
|
||||
Default = new()
|
||||
},
|
||||
DownloadClient = new()
|
||||
};
|
||||
|
||||
var property = typeof(SettingData).GetProperty("Get", BindingFlags.Public | BindingFlags.Static);
|
||||
property?.SetValue(null, settings);
|
||||
}
|
||||
|
||||
private static void ResetPrevCheck(WatchFolderChecker checker)
|
||||
{
|
||||
var field = typeof(WatchFolderChecker).GetField("_prevCheck", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
|
@ -84,7 +92,7 @@ public class WatchFolderCheckerTests : IDisposable
|
|||
var content = "torrent content"u8.ToArray();
|
||||
await File.WriteAllBytesAsync(filePath, content);
|
||||
|
||||
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object, _settings);
|
||||
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object);
|
||||
ResetPrevCheck(checker);
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
|
|
@ -123,7 +131,7 @@ public class WatchFolderCheckerTests : IDisposable
|
|||
var content = "magnet:?xt=urn:btih:123";
|
||||
await File.WriteAllTextAsync(filePath, content);
|
||||
|
||||
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object, _settings);
|
||||
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object);
|
||||
ResetPrevCheck(checker);
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
|
|
@ -162,7 +170,7 @@ public class WatchFolderCheckerTests : IDisposable
|
|||
var content = "nzb content"u8.ToArray();
|
||||
await File.WriteAllBytesAsync(filePath, content);
|
||||
|
||||
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object, _settings);
|
||||
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object);
|
||||
ResetPrevCheck(checker);
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
|
|
@ -201,7 +209,7 @@ public class WatchFolderCheckerTests : IDisposable
|
|||
var filePath = Path.Combine(_testPath, "test.txt");
|
||||
await File.WriteAllTextAsync(filePath, "ignore me");
|
||||
|
||||
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object, _settings);
|
||||
var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object);
|
||||
ResetPrevCheck(checker);
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
|
|
@ -181,7 +181,16 @@ public class DownloadHelperTest
|
|||
FileName = "file.txt"
|
||||
};
|
||||
|
||||
var fileRelativePath = Path.Combine("inside", "lots", "of", "subdirectories", "file.txt");
|
||||
String fileRelativePath;
|
||||
|
||||
if (OSHelper.IsLinux)
|
||||
{
|
||||
fileRelativePath = "inside/lots/of/subdirectories/file.txt";
|
||||
}
|
||||
else
|
||||
{
|
||||
fileRelativePath = @"inside\lots\of\subdirectories\file.txt";
|
||||
}
|
||||
|
||||
IList<DebridClientFile> files =
|
||||
[
|
||||
|
|
@ -203,7 +212,7 @@ public class DownloadHelperTest
|
|||
var path = DownloadHelper.GetDownloadPath("/data/downloads", torrent, download, fileSystem);
|
||||
|
||||
// Assert
|
||||
var expectedPath = Path.Combine("/data/downloads", torrent.RdName, "inside", "lots", "of", "subdirectories", "file.txt");
|
||||
var expectedPath = Path.Combine("/data/downloads", torrent.RdName, fileRelativePath);
|
||||
Assert.Equal(expectedPath, path);
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +226,16 @@ public class DownloadHelperTest
|
|||
FileName = "file.txt"
|
||||
};
|
||||
|
||||
var fileRelativePath = Path.Combine("inside", "lots", "of", "subdirectories", "file.txt");
|
||||
String fileRelativePath;
|
||||
|
||||
if (OSHelper.IsLinux)
|
||||
{
|
||||
fileRelativePath = "inside/lots/of/subdirectories/file.txt";
|
||||
}
|
||||
else
|
||||
{
|
||||
fileRelativePath = @"inside\lots\of\subdirectories\file.txt";
|
||||
}
|
||||
|
||||
IList<DebridClientFile> files =
|
||||
[
|
||||
|
|
@ -237,7 +255,7 @@ public class DownloadHelperTest
|
|||
var path = DownloadHelper.GetDownloadPath(torrent, download);
|
||||
|
||||
// Assert
|
||||
var expectedPath = Path.Combine(torrent.RdName, "inside", "lots", "of", "subdirectories", "file.txt");
|
||||
var expectedPath = Path.Combine(torrent.RdName, fileRelativePath);
|
||||
Assert.Equal(expectedPath, path);
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +269,16 @@ public class DownloadHelperTest
|
|||
FileName = "file.txt"
|
||||
};
|
||||
|
||||
var fileRelativePath = Path.Combine("Torrent Name", "Saison 1", "file.txt");
|
||||
String fileRelativePath;
|
||||
|
||||
if (OSHelper.IsLinux)
|
||||
{
|
||||
fileRelativePath = "Torrent Name/Saison 1/file.txt";
|
||||
}
|
||||
else
|
||||
{
|
||||
fileRelativePath = @"Torrent Name\Saison 1\file.txt";
|
||||
}
|
||||
|
||||
IList<DebridClientFile> files =
|
||||
[
|
||||
|
|
@ -288,7 +315,16 @@ public class DownloadHelperTest
|
|||
FileName = "file.txt"
|
||||
};
|
||||
|
||||
var fileRelativePath = Path.Combine("Torrent Name", "Saison 1", "file.txt");
|
||||
String fileRelativePath;
|
||||
|
||||
if (OSHelper.IsLinux)
|
||||
{
|
||||
fileRelativePath = "Torrent Name/Saison 1/file.txt";
|
||||
}
|
||||
else
|
||||
{
|
||||
fileRelativePath = @"Torrent Name\Saison 1\file.txt";
|
||||
}
|
||||
|
||||
IList<DebridClientFile> files =
|
||||
[
|
||||
|
|
|
|||
8
server/RdtClient.Service.Test/Helpers/OSHelper.cs
Normal file
8
server/RdtClient.Service.Test/Helpers/OSHelper.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace RdtClient.Service.Test.Helpers;
|
||||
|
||||
public static class OSHelper
|
||||
{
|
||||
public static Boolean IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||
}
|
||||
|
|
@ -5,24 +5,23 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<AssemblyName>RdtClient.Service.Test</AssemblyName>
|
||||
<RootNamespace>RdtClient.Service.Test</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AllDebrid.NET" Version="1.0.18" />
|
||||
<PackageReference Include="coverlet.collector" Version="10.0.1">
|
||||
<PackageReference Include="coverlet.collector" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="SharpCompress" Version="[0.42.1]" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.1" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.1.1" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.1" />
|
||||
<PackageReference Include="TorBox.NET" Version="2.1.0" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.0" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.1.0" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" />
|
||||
<PackageReference Include="TorBox.NET" Version="1.6.2" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
@ -38,4 +37,10 @@
|
|||
<ProjectReference Include="..\RdtClient.Service\RdtClient.Service.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="TorBoxNET">
|
||||
<HintPath>..\..\..\torbox-net\TorBoxNET\bin\Release\netstandard2.0\TorBoxNET.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,186 +0,0 @@
|
|||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Models.Data;
|
||||
|
||||
namespace RdtClient.Service.Test.Regression;
|
||||
|
||||
public class TorrentDownloadRaceTests : IAsyncLifetime
|
||||
{
|
||||
private readonly String _databasePath = Path.Combine(Path.GetTempPath(), $"rdt-client-race-{Guid.NewGuid():N}.sqlite");
|
||||
|
||||
[Fact]
|
||||
public async Task Add_WhenTorrentIsDeletedAfterItWasRead_DoesNotThrowAndDoesNotInsertDownload()
|
||||
{
|
||||
var torrentId = await SeedTorrentAsync();
|
||||
|
||||
await using (var readerContext = CreateContext())
|
||||
{
|
||||
var torrentData = new TorrentData(readerContext);
|
||||
var torrent = await torrentData.GetById(torrentId);
|
||||
|
||||
Assert.NotNull(torrent);
|
||||
}
|
||||
|
||||
await using (var deleteContext = CreateContext())
|
||||
{
|
||||
var torrentData = new TorrentData(deleteContext);
|
||||
await torrentData.Delete(torrentId);
|
||||
}
|
||||
|
||||
DownloadAddResult result;
|
||||
|
||||
await using (var addContext = CreateContext())
|
||||
{
|
||||
var downloadData = new DownloadData(addContext);
|
||||
result = await downloadData.TryAddForTorrent(torrentId, CreateDownloadInfo("race-a"));
|
||||
}
|
||||
|
||||
Assert.Equal(DownloadAddResult.TorrentMissing, result);
|
||||
|
||||
await using var verifyContext = CreateContext();
|
||||
Assert.False(await verifyContext.Torrents.AnyAsync(m => m.TorrentId == torrentId));
|
||||
Assert.False(await verifyContext.Downloads.AnyAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenDownloadIsInsertedBetweenChildAndParentDelete_DoesNotThrowAndRemovesTorrentGraph()
|
||||
{
|
||||
var torrentId = await SeedTorrentAsync();
|
||||
|
||||
await using (var deleteChildrenContext = CreateContext())
|
||||
{
|
||||
var downloadData = new DownloadData(deleteChildrenContext);
|
||||
await downloadData.DeleteForTorrent(torrentId);
|
||||
}
|
||||
|
||||
await using (var addContext = CreateContext())
|
||||
{
|
||||
var downloadData = new DownloadData(addContext);
|
||||
var result = await downloadData.TryAddForTorrent(torrentId, CreateDownloadInfo("race-b"));
|
||||
Assert.Equal(DownloadAddResult.Added, result);
|
||||
}
|
||||
|
||||
Exception? exception;
|
||||
|
||||
await using (var deleteParentContext = CreateContext())
|
||||
{
|
||||
var torrentData = new TorrentData(deleteParentContext);
|
||||
exception = await Record.ExceptionAsync(() => torrentData.Delete(torrentId));
|
||||
}
|
||||
|
||||
Assert.Null(exception);
|
||||
|
||||
await using var verifyContext = CreateContext();
|
||||
Assert.False(await verifyContext.Torrents.AnyAsync(m => m.TorrentId == torrentId));
|
||||
Assert.False(await verifyContext.Downloads.AnyAsync(m => m.TorrentId == torrentId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryAddForTorrent_WhenDownloadAlreadyExists_DoesNotInsertDuplicate()
|
||||
{
|
||||
var torrentId = await SeedTorrentAsync();
|
||||
|
||||
await using (var firstContext = CreateContext())
|
||||
{
|
||||
var downloadData = new DownloadData(firstContext);
|
||||
var result = await downloadData.TryAddForTorrent(torrentId, CreateDownloadInfo("race-c"));
|
||||
|
||||
Assert.Equal(DownloadAddResult.Added, result);
|
||||
}
|
||||
|
||||
await using (var secondContext = CreateContext())
|
||||
{
|
||||
var downloadData = new DownloadData(secondContext);
|
||||
var result = await downloadData.TryAddForTorrent(torrentId, CreateDownloadInfo("race-c"));
|
||||
|
||||
Assert.Equal(DownloadAddResult.AlreadyExists, result);
|
||||
}
|
||||
|
||||
await using var verifyContext = CreateContext();
|
||||
Assert.Equal(1, await verifyContext.Downloads.CountAsync(m => m.TorrentId == torrentId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenTorrentWasAlreadyDeleted_DoesNotThrow()
|
||||
{
|
||||
var torrentId = await SeedTorrentAsync();
|
||||
|
||||
await using (var firstDeleteContext = CreateContext())
|
||||
{
|
||||
var torrentData = new TorrentData(firstDeleteContext);
|
||||
await torrentData.Delete(torrentId);
|
||||
}
|
||||
|
||||
Exception? exception;
|
||||
|
||||
await using (var secondDeleteContext = CreateContext())
|
||||
{
|
||||
var torrentData = new TorrentData(secondDeleteContext);
|
||||
exception = await Record.ExceptionAsync(() => torrentData.Delete(torrentId));
|
||||
}
|
||||
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
await context.Database.EnsureDeletedAsync();
|
||||
await context.Database.EnsureCreatedAsync();
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
if (File.Exists(_databasePath))
|
||||
{
|
||||
File.Delete(_databasePath);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private DataContext CreateContext()
|
||||
{
|
||||
var connectionString = new SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = _databasePath,
|
||||
ForeignKeys = true
|
||||
}.ToString();
|
||||
|
||||
var options = new DbContextOptionsBuilder<DataContext>()
|
||||
.UseSqlite(connectionString)
|
||||
.Options;
|
||||
|
||||
return new(options);
|
||||
}
|
||||
|
||||
private async Task<Guid> SeedTorrentAsync()
|
||||
{
|
||||
var torrentId = Guid.NewGuid();
|
||||
|
||||
await using var context = CreateContext();
|
||||
|
||||
context.Torrents.Add(new()
|
||||
{
|
||||
TorrentId = torrentId,
|
||||
Hash = Guid.NewGuid().ToString("N"),
|
||||
Added = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return torrentId;
|
||||
}
|
||||
|
||||
private static DownloadInfo CreateDownloadInfo(String suffix)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
FileName = $"download-{suffix}.bin",
|
||||
RestrictedLink = $"https://example.invalid/{suffix}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ using System.Text;
|
|||
using Moq;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using TorrentRunnerState = RdtClient.Service.Services.TorrentRunnerState;
|
||||
using TorrentsService = RdtClient.Service.Services.Torrents;
|
||||
|
||||
namespace RdtClient.Service.Test.Services;
|
||||
|
|
@ -29,9 +28,7 @@ public class NzbTorrentsTest
|
|||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
new TestSettings(),
|
||||
new TorrentRunnerState());
|
||||
null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using System.Text.Json;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.DebridClient;
|
||||
using RdtClient.Service.Services;
|
||||
|
||||
namespace RdtClient.Service.Test.Services;
|
||||
|
|
@ -13,19 +11,15 @@ public class QBittorrentTest
|
|||
private readonly Mock<Authentication> _authenticationMock;
|
||||
private readonly Mock<ILogger<QBittorrent>> _loggerMock;
|
||||
private readonly QBittorrent _qBittorrent;
|
||||
private readonly TestSettings _settings;
|
||||
private readonly TorrentRunnerState _runnerState;
|
||||
private readonly Mock<Torrents> _torrentsMock;
|
||||
|
||||
public QBittorrentTest()
|
||||
{
|
||||
_loggerMock = new();
|
||||
_settings = new();
|
||||
_runnerState = new();
|
||||
_torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, _runnerState);
|
||||
_torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
|
||||
_authenticationMock = new(null!, null!, null!);
|
||||
|
||||
_qBittorrent = new(_loggerMock.Object, _settings, _authenticationMock.Object, _torrentsMock.Object, null!, _runnerState);
|
||||
_qBittorrent = new(_loggerMock.Object, null!, _authenticationMock.Object, _torrentsMock.Object, null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -142,268 +136,4 @@ public class QBittorrentTest
|
|||
// Current behavior is (1.0 + 0.8) / 2 = 0.9
|
||||
Assert.Equal(0.9f, result[0].Progress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TorrentFileContents_ShouldReturnAllFiles_WithIndexesAndPriority()
|
||||
{
|
||||
// Arrange
|
||||
var torrent = new Torrent
|
||||
{
|
||||
Hash = "hash1",
|
||||
Type = DownloadType.Torrent,
|
||||
RdFiles = JsonSerializer.Serialize(new List<DebridClientFile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Path = "good.mkv",
|
||||
Bytes = 1000,
|
||||
Selected = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
Path = "dangerous.exe",
|
||||
Bytes = 10,
|
||||
Selected = false
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
_torrentsMock.Setup(m => m.GetByHash("hash1")).ReturnsAsync(torrent);
|
||||
|
||||
// Act
|
||||
var result = await _qBittorrent.TorrentFileContents("hash1");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
|
||||
Assert.Collection(result!,
|
||||
first =>
|
||||
{
|
||||
Assert.Equal(0, first.Index);
|
||||
Assert.Equal("good.mkv", first.Name);
|
||||
Assert.Equal(1, first.Priority);
|
||||
},
|
||||
second =>
|
||||
{
|
||||
Assert.Equal(1, second.Index);
|
||||
Assert.Equal("dangerous.exe", second.Name);
|
||||
Assert.Equal(0, second.Priority);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TorrentProperties_ShouldExposePublicTorrentFlag()
|
||||
{
|
||||
// Arrange
|
||||
var torrent = new Torrent
|
||||
{
|
||||
Hash = "hash1",
|
||||
Type = DownloadType.Torrent,
|
||||
Added = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
_torrentsMock.Setup(m => m.GetByHash("hash1")).ReturnsAsync(torrent);
|
||||
|
||||
// Act
|
||||
var result = await _qBittorrent.TorrentProperties("hash1");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result!.IsPrivate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TorrentInfo_ShouldUseSelectedTopLevelFileName_WhenRootFileAddsOnlyTheExtension()
|
||||
{
|
||||
// Arrange
|
||||
var previousMappedPath = _settings.Current.DownloadClient.MappedPath;
|
||||
_settings.Current.DownloadClient.MappedPath = "/data/downloads";
|
||||
|
||||
try
|
||||
{
|
||||
var torrentRootName = "Sample.Show.S01E01.1080p.WEB-DL-GROUP";
|
||||
var selectedFileName = $"{torrentRootName}.mkv";
|
||||
|
||||
var torrent = new Torrent
|
||||
{
|
||||
Hash = "hash1",
|
||||
Category = "tv",
|
||||
Completed = DateTimeOffset.UtcNow,
|
||||
RdName = torrentRootName,
|
||||
RdFiles = JsonSerializer.Serialize(new List<DebridClientFile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Path = $"/{selectedFileName}",
|
||||
Bytes = 1000,
|
||||
Selected = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
Path = $"/{torrentRootName}.nfo",
|
||||
Bytes = 10,
|
||||
Selected = false
|
||||
}
|
||||
}),
|
||||
Type = DownloadType.Torrent
|
||||
};
|
||||
|
||||
_torrentsMock.Setup(m => m.Get())
|
||||
.ReturnsAsync(new List<Torrent>
|
||||
{
|
||||
torrent
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _qBittorrent.TorrentInfo();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(Path.Combine("/data/downloads", "tv", selectedFileName) + Path.DirectorySeparatorChar, result[0].ContentPath);
|
||||
Assert.Equal(torrentRootName, result[0].Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_settings.Current.DownloadClient.MappedPath = previousMappedPath;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TorrentInfo_ShouldUseExistingCompletedContentRoot_WhenSingleFileDirectoryDiffersFromRdName()
|
||||
{
|
||||
var previousMappedPath = _settings.Current.DownloadClient.MappedPath;
|
||||
var mappedPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||
_settings.Current.DownloadClient.MappedPath = mappedPath;
|
||||
|
||||
try
|
||||
{
|
||||
var category = "tv";
|
||||
var selectedFileName = "Sample.Show.S01E02.1080p.WEB-DL-GROUP.mkv";
|
||||
var contentRoot = Path.Combine(mappedPath, category, selectedFileName);
|
||||
Directory.CreateDirectory(contentRoot);
|
||||
await File.WriteAllTextAsync(Path.Combine(contentRoot, selectedFileName), "test");
|
||||
|
||||
var torrent = new Torrent
|
||||
{
|
||||
Hash = "hash1",
|
||||
Category = category,
|
||||
Completed = DateTimeOffset.UtcNow,
|
||||
Downloads = new List<Download>
|
||||
{
|
||||
new()
|
||||
{
|
||||
DownloadId = Guid.NewGuid(),
|
||||
FileName = selectedFileName,
|
||||
Link = "https://fake.url/Sample.Show.S01E02.1080p.WEB-DL-GROUP.mkv",
|
||||
Completed = DateTimeOffset.UtcNow
|
||||
}
|
||||
},
|
||||
RdName = "tracker.example - Sample.Show.S01E02.1080p.WEB-DL-GROUP",
|
||||
RdFiles = JsonSerializer.Serialize(new List<DebridClientFile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Path = $"/{selectedFileName}",
|
||||
Bytes = 1000,
|
||||
Selected = true
|
||||
}
|
||||
}),
|
||||
Type = DownloadType.Torrent
|
||||
};
|
||||
|
||||
_torrentsMock.Setup(m => m.Get())
|
||||
.ReturnsAsync(new List<Torrent>
|
||||
{
|
||||
torrent
|
||||
});
|
||||
|
||||
var result = await _qBittorrent.TorrentInfo();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(contentRoot + Path.DirectorySeparatorChar, result[0].ContentPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_settings.Current.DownloadClient.MappedPath = previousMappedPath;
|
||||
|
||||
if (Directory.Exists(mappedPath))
|
||||
{
|
||||
Directory.Delete(mappedPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TorrentInfo_ShouldUseExistingCompletedContentRoot_WhenSelectedFileIsNestedUnderDifferentRoot()
|
||||
{
|
||||
var previousMappedPath = _settings.Current.DownloadClient.MappedPath;
|
||||
var mappedPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||
_settings.Current.DownloadClient.MappedPath = mappedPath;
|
||||
|
||||
try
|
||||
{
|
||||
var category = "tv";
|
||||
var rootDirectoryName = "tracker.example - Sample.Show.S01E03.1080p.x265-GROUP.mkv";
|
||||
var nestedDirectoryName = "Sample.Show.S01E03.1080p.x265-GROUP";
|
||||
var fileName = "Sample.Show.S01E03.1080p.x265-GROUP.mkv";
|
||||
var contentRoot = Path.Combine(mappedPath, category, rootDirectoryName);
|
||||
var nestedDirectory = Path.Combine(contentRoot, nestedDirectoryName);
|
||||
Directory.CreateDirectory(nestedDirectory);
|
||||
await File.WriteAllTextAsync(Path.Combine(nestedDirectory, fileName), "test");
|
||||
|
||||
var torrent = new Torrent
|
||||
{
|
||||
Hash = "hash1",
|
||||
Category = category,
|
||||
Completed = DateTimeOffset.UtcNow,
|
||||
Downloads = new List<Download>
|
||||
{
|
||||
new()
|
||||
{
|
||||
DownloadId = Guid.NewGuid(),
|
||||
FileName = fileName,
|
||||
Link = "https://fake.url/Sample.Show.S01E03.1080p.x265-GROUP.mkv",
|
||||
Completed = DateTimeOffset.UtcNow
|
||||
}
|
||||
},
|
||||
RdName = "tracker.example - Sample.Show.S01E03.1080p.x265-GROUP",
|
||||
RdFiles = JsonSerializer.Serialize(new List<DebridClientFile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Path = $"/{nestedDirectoryName}/{fileName}",
|
||||
Bytes = 1000,
|
||||
Selected = true
|
||||
}
|
||||
}),
|
||||
Type = DownloadType.Torrent
|
||||
};
|
||||
|
||||
_torrentsMock.Setup(m => m.Get())
|
||||
.ReturnsAsync(new List<Torrent>
|
||||
{
|
||||
torrent
|
||||
});
|
||||
|
||||
var result = await _qBittorrent.TorrentInfo();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(contentRoot + Path.DirectorySeparatorChar, result[0].ContentPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_settings.Current.DownloadClient.MappedPath = previousMappedPath;
|
||||
|
||||
if (Directory.Exists(mappedPath))
|
||||
{
|
||||
Directory.Delete(mappedPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
|
|
@ -15,13 +16,11 @@ public class SabnzbdTest
|
|||
};
|
||||
|
||||
private readonly Mock<ILogger<Sabnzbd>> _loggerMock = new();
|
||||
private readonly TestSettings _settings;
|
||||
private readonly Mock<Torrents> _torrentsMock;
|
||||
|
||||
public SabnzbdTest()
|
||||
{
|
||||
_settings = new();
|
||||
_torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState());
|
||||
_torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
|
||||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(new List<Torrent>());
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +50,7 @@ public class SabnzbdTest
|
|||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
|
||||
_torrentsMock.Setup(t => t.GetDownloadStats(It.IsAny<Guid>())).Returns((0, 1000, 500));
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = await sabnzbd.GetQueue();
|
||||
|
|
@ -91,7 +90,7 @@ public class SabnzbdTest
|
|||
|
||||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = await sabnzbd.GetQueue();
|
||||
|
|
@ -135,7 +134,7 @@ public class SabnzbdTest
|
|||
|
||||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = await sabnzbd.GetQueue();
|
||||
|
|
@ -187,7 +186,7 @@ public class SabnzbdTest
|
|||
|
||||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = await sabnzbd.GetQueue();
|
||||
|
|
@ -225,7 +224,7 @@ public class SabnzbdTest
|
|||
|
||||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = await sabnzbd.GetQueue();
|
||||
|
|
@ -261,7 +260,7 @@ public class SabnzbdTest
|
|||
|
||||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = await sabnzbd.GetHistory();
|
||||
|
|
@ -276,7 +275,7 @@ public class SabnzbdTest
|
|||
{
|
||||
// Arrange
|
||||
var savePath = @"C:\Downloads";
|
||||
_settings.Current.DownloadClient.MappedPath = savePath;
|
||||
SettingData.Get.DownloadClient.MappedPath = savePath;
|
||||
|
||||
var torrentList = new List<Torrent>
|
||||
{
|
||||
|
|
@ -293,7 +292,7 @@ public class SabnzbdTest
|
|||
|
||||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = await sabnzbd.GetHistory();
|
||||
|
|
@ -309,7 +308,7 @@ public class SabnzbdTest
|
|||
{
|
||||
// Arrange
|
||||
var savePath = @"C:\Downloads";
|
||||
_settings.Current.DownloadClient.MappedPath = savePath;
|
||||
SettingData.Get.DownloadClient.MappedPath = savePath;
|
||||
|
||||
var torrentList = new List<Torrent>
|
||||
{
|
||||
|
|
@ -326,7 +325,7 @@ public class SabnzbdTest
|
|||
|
||||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = await sabnzbd.GetHistory();
|
||||
|
|
@ -356,7 +355,7 @@ public class SabnzbdTest
|
|||
|
||||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = await sabnzbd.GetHistory();
|
||||
|
|
@ -366,69 +365,11 @@ public class SabnzbdTest
|
|||
Assert.Equal("Failed", result.Slots[0].Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TorrentFinishedAction.RemoveAllTorrents, true, true, true, true)]
|
||||
[InlineData(TorrentFinishedAction.RemoveAllTorrents, false, true, true, false)]
|
||||
[InlineData(TorrentFinishedAction.RemoveRealDebrid, true, false, true, true)]
|
||||
[InlineData(TorrentFinishedAction.RemoveRealDebrid, false, false, true, false)]
|
||||
[InlineData(TorrentFinishedAction.RemoveClient, true, true, false, true)]
|
||||
[InlineData(TorrentFinishedAction.RemoveClient, false, true, false, false)]
|
||||
public async Task Delete_ShouldRespectFinishedActionAndDeleteFiles(
|
||||
TorrentFinishedAction finishedAction,
|
||||
Boolean deleteFiles,
|
||||
Boolean expectedDeleteData,
|
||||
Boolean expectedDeleteRdTorrent,
|
||||
Boolean expectedDeleteLocalFiles)
|
||||
{
|
||||
// Arrange
|
||||
var torrentId = Guid.NewGuid();
|
||||
_settings.Current.Integrations.Default.FinishedAction = finishedAction;
|
||||
|
||||
_torrentsMock.Setup(t => t.GetByHash("hash1"))
|
||||
.ReturnsAsync(new Torrent
|
||||
{
|
||||
TorrentId = torrentId,
|
||||
Hash = "hash1",
|
||||
Type = DownloadType.Nzb
|
||||
});
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
|
||||
// Act
|
||||
await sabnzbd.Delete("hash1", deleteFiles);
|
||||
|
||||
// Assert
|
||||
_torrentsMock.Verify(t => t.Delete(torrentId, expectedDeleteData, expectedDeleteRdTorrent, expectedDeleteLocalFiles), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_ShouldNotDelete_WhenFinishedActionIsNone()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.Integrations.Default.FinishedAction = TorrentFinishedAction.None;
|
||||
|
||||
_torrentsMock.Setup(t => t.GetByHash("hash1"))
|
||||
.ReturnsAsync(new Torrent
|
||||
{
|
||||
TorrentId = Guid.NewGuid(),
|
||||
Hash = "hash1",
|
||||
Type = DownloadType.Nzb
|
||||
});
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
|
||||
// Act
|
||||
await sabnzbd.Delete("hash1", true);
|
||||
|
||||
// Assert
|
||||
_torrentsMock.Verify(t => t.Delete(It.IsAny<Guid>(), It.IsAny<Boolean>(), It.IsAny<Boolean>(), It.IsAny<Boolean>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConfig_ShouldReturnCorrectConfig()
|
||||
{
|
||||
// Arrange
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = sabnzbd.GetConfig();
|
||||
|
|
@ -458,9 +399,9 @@ public class SabnzbdTest
|
|||
|
||||
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
|
||||
|
||||
_settings.Current.General.Categories = "TV, Music, *";
|
||||
SettingData.Get.General.Categories = "TV, Music, *";
|
||||
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings);
|
||||
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);
|
||||
|
||||
// Act
|
||||
var result = sabnzbd.GetCategories();
|
||||
|
|
|
|||
|
|
@ -1,225 +0,0 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.Services;
|
||||
using RdtClient.Service.Services.DebridClients;
|
||||
|
||||
namespace RdtClient.Service.Test.Services.TorrentClients;
|
||||
|
||||
public class PremiumizeDebridClientTest
|
||||
{
|
||||
private readonly Mock<IDownloadableFileFilter> _fileFilterMock;
|
||||
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
|
||||
private readonly Mock<ILogger<PremiumizeDebridClient>> _loggerMock;
|
||||
private readonly TestSettings _settings;
|
||||
|
||||
public PremiumizeDebridClientTest()
|
||||
{
|
||||
_loggerMock = new();
|
||||
_httpClientFactoryMock = new();
|
||||
_fileFilterMock = new();
|
||||
_settings = new();
|
||||
_settings.Current.Provider.ApiKey = "test-api-key";
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddNzbLink_UsesPremiumizeNetTransferCreate_ReturnsTransferId()
|
||||
{
|
||||
// Arrange
|
||||
var handler = new RecordingHttpMessageHandler(_ => JsonResponse("""{"status":"success","id":"transfer-id","name":"test.nzb"}"""));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
// Act
|
||||
var result = await client.AddNzbLink("http://example.com/test.nzb");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("transfer-id", result);
|
||||
Assert.Equal(HttpMethod.Post, handler.Request!.Method);
|
||||
Assert.Equal("https://www.premiumize.me/api/transfer/create?apikey=test-api-key", handler.Request.RequestUri!.ToString());
|
||||
Assert.Null(handler.Request.Headers.Authorization);
|
||||
Assert.Equal("application/x-www-form-urlencoded", handler.Request.Content!.Headers.ContentType!.MediaType);
|
||||
Assert.Equal("src=http%3A%2F%2Fexample.com%2Ftest.nzb&folder_id=", handler.RequestBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddNzbFile_SubmitsMultipartTransferCreateRequest_ReturnsTransferId()
|
||||
{
|
||||
// Arrange
|
||||
var handler = new RecordingHttpMessageHandler(_ => JsonResponse("""{"status":"success","id":"file-transfer-id","name":"test.nzb"}"""));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
// Act
|
||||
var result = await client.AddNzbFile(Encoding.UTF8.GetBytes("nzb body"), "test.nzb");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("file-transfer-id", result);
|
||||
Assert.Equal(HttpMethod.Post, handler.Request!.Method);
|
||||
Assert.Equal("https://www.premiumize.me/api/transfer/create", handler.Request.RequestUri!.ToString());
|
||||
Assert.Equal("Bearer", handler.Request.Headers.Authorization!.Scheme);
|
||||
Assert.Equal("test-api-key", handler.Request.Headers.Authorization.Parameter);
|
||||
Assert.Equal("multipart/form-data", handler.Request.Content!.Headers.ContentType!.MediaType);
|
||||
Assert.Contains("Content-Disposition: form-data", handler.RequestBody);
|
||||
Assert.Contains("src", handler.RequestBody);
|
||||
Assert.Contains("test.nzb", handler.RequestBody);
|
||||
Assert.Contains("nzb body", handler.RequestBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddNzbFile_WhenNameIsMissing_UsesDefaultFilename()
|
||||
{
|
||||
// Arrange
|
||||
var handler = new RecordingHttpMessageHandler(_ => JsonResponse("""{"status":"success","id":"file-transfer-id","name":"upload.nzb"}"""));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
// Act
|
||||
await client.AddNzbFile(Encoding.UTF8.GetBytes("nzb body"), null);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("upload.nzb", handler.RequestBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddNzbFile_WhenNameHasNoNzbExtension_AppendsNzbExtension()
|
||||
{
|
||||
// Arrange
|
||||
var handler = new RecordingHttpMessageHandler(_ => JsonResponse("""{"status":"success","id":"file-transfer-id","name":"release-title.nzb"}"""));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
// Act
|
||||
await client.AddNzbFile(Encoding.UTF8.GetBytes("nzb body"), "release-title");
|
||||
|
||||
// Assert
|
||||
Assert.Contains("release-title.nzb", handler.RequestBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddNzbLink_WhenSuccessResponseHasNoId_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var handler = new RecordingHttpMessageHandler(_ => JsonResponse("""{"status":"success","name":"test.nzb"}"""));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<Exception>(() => client.AddNzbLink("http://example.com/test.nzb"));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Unable to add NZB link", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddNzbLink_WhenPremiumizeReturnsRateLimitError_ThrowsRateLimitException()
|
||||
{
|
||||
// Arrange
|
||||
var handler = new RecordingHttpMessageHandler(_ => JsonResponse("""{"status":"error","code":"slow_down","message":"rate limit exceeded"}"""));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<RateLimitException>(() => client.AddNzbLink("http://example.com/test.nzb"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimeSpan.FromMinutes(2), ex.RetryAfter);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("You've made too many API requests too quickly.")]
|
||||
[InlineData("Your fair-use points, booster points, or active-job count is exhausted.")]
|
||||
[InlineData("This account's usage limit for this service has been reached.")]
|
||||
[InlineData("The target service is unreachable right now. Retry after a delay.")]
|
||||
public async Task AddNzbLink_WhenPremiumizeNetReturnsDocumentedRetryMessage_ThrowsRateLimitException(String message)
|
||||
{
|
||||
// Arrange
|
||||
var handler = new RecordingHttpMessageHandler(_ => JsonResponse($$"""{"status":"error","message":"{{message}}","code":"rate_limit_reached"}"""));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<RateLimitException>(() => client.AddNzbLink("http://example.com/test.nzb"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimeSpan.FromMinutes(2), ex.RetryAfter);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("rate_limit_reached")]
|
||||
[InlineData("account_limit_reached")]
|
||||
[InlineData("service_limit_reached")]
|
||||
[InlineData("service_down")]
|
||||
[InlineData("semi_permanent_error")]
|
||||
public async Task AddNzbFile_WhenPremiumizeReturnsDocumentedRetryCode_ThrowsRateLimitException(String code)
|
||||
{
|
||||
// Arrange
|
||||
var handler = new RecordingHttpMessageHandler(_ => JsonResponse($$"""{"status":"error","code":"{{code}}","message":"retry later"}"""));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<RateLimitException>(() => client.AddNzbFile(Encoding.UTF8.GetBytes("nzb body"), "test.nzb"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimeSpan.FromMinutes(2), ex.RetryAfter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddTorrentMagnet_UsesPremiumizeNetTransferCreate_ReturnsTransferId()
|
||||
{
|
||||
// Arrange
|
||||
var handler = new RecordingHttpMessageHandler(_ => JsonResponse("""{"status":"success","id":"magnet-transfer-id","name":"test"}"""));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
// Act
|
||||
var result = await client.AddTorrentMagnet("magnet:?xt=urn:btih:test");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("magnet-transfer-id", result);
|
||||
Assert.Equal("https://www.premiumize.me/api/transfer/create?apikey=test-api-key", handler.Request!.RequestUri!.ToString());
|
||||
Assert.Equal("src=magnet%3A%3Fxt%3Durn%3Abtih%3Atest&folder_id=", handler.RequestBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddTorrentFile_UsesPremiumizeNetFileUpload_ReturnsTransferId()
|
||||
{
|
||||
// Arrange
|
||||
var handler = new RecordingHttpMessageHandler(_ => JsonResponse("""{"status":"success","id":"torrent-transfer-id","name":"test"}"""));
|
||||
var client = CreateClient(handler);
|
||||
|
||||
// Act
|
||||
var result = await client.AddTorrentFile(Encoding.UTF8.GetBytes("torrent body"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("torrent-transfer-id", result);
|
||||
Assert.Equal("https://www.premiumize.me/api/transfer/create?apikey=test-api-key", handler.Request!.RequestUri!.ToString());
|
||||
Assert.Contains("name=file", handler.RequestBody);
|
||||
Assert.Contains("filename=1.torrent", handler.RequestBody);
|
||||
Assert.Contains("application/x-bittorrent", handler.RequestBody);
|
||||
Assert.Contains("torrent body", handler.RequestBody);
|
||||
}
|
||||
|
||||
private PremiumizeDebridClient CreateClient(RecordingHttpMessageHandler handler)
|
||||
{
|
||||
_httpClientFactoryMock.Setup(m => m.CreateClient(It.IsAny<String>())).Returns(new HttpClient(handler));
|
||||
|
||||
return new(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _settings);
|
||||
}
|
||||
|
||||
private static HttpResponseMessage JsonResponse(String json, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
return new(statusCode)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
}
|
||||
|
||||
private class RecordingHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler) : HttpMessageHandler
|
||||
{
|
||||
public HttpRequestMessage? Request { get; private set; }
|
||||
public String? RequestBody { get; private set; }
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Request = request;
|
||||
RequestBody = request.Content == null ? null : await request.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
return handler(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ public class TorBoxDebridClientTest
|
|||
private readonly Mock<IDownloadableFileFilter> _fileFilterMock;
|
||||
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
|
||||
private readonly Mock<ILogger<TorBoxDebridClient>> _loggerMock;
|
||||
private readonly TestSettings _settings;
|
||||
|
||||
public TorBoxDebridClientTest()
|
||||
{
|
||||
|
|
@ -29,13 +28,12 @@ public class TorBoxDebridClientTest
|
|||
_httpClientFactoryMock = new();
|
||||
_fileFilterMock = new();
|
||||
_coordinatorMock = new();
|
||||
_settings = new();
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
_httpClientFactoryMock.Setup(m => m.CreateClient(It.IsAny<String>())).Returns(httpClient);
|
||||
|
||||
_settings.Current.Provider.ApiKey = "test-api-key";
|
||||
_settings.Current.Provider.Timeout = 100;
|
||||
Settings.Get.Provider.ApiKey = "test-api-key";
|
||||
Settings.Get.Provider.Timeout = 100;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -46,7 +44,6 @@ public class TorBoxDebridClientTest
|
|||
{
|
||||
new()
|
||||
{
|
||||
Id = 12345,
|
||||
Hash = "hash1",
|
||||
Name = "torrent1",
|
||||
Size = 1000,
|
||||
|
|
@ -67,7 +64,7 @@ public class TorBoxDebridClientTest
|
|||
}
|
||||
};
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
clientMock.Protected().Setup<Task<IEnumerable<TorrentInfoResult>?>>("GetCurrentTorrents").ReturnsAsync(torrents);
|
||||
clientMock.Protected().Setup<Task<IEnumerable<TorrentInfoResult>?>>("GetQueuedTorrents").ReturnsAsync(new List<TorrentInfoResult>());
|
||||
clientMock.Protected().Setup<Task<IEnumerable<UsenetInfoResult>?>>("GetCurrentUsenet").ReturnsAsync(nzbs);
|
||||
|
|
@ -79,7 +76,7 @@ public class TorBoxDebridClientTest
|
|||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
|
||||
var torrentResult = result.FirstOrDefault(r => r.Id == "12345");
|
||||
var torrentResult = result.FirstOrDefault(r => r.Id == "hash1");
|
||||
Assert.NotNull(torrentResult);
|
||||
Assert.Equal(DownloadType.Torrent, torrentResult.Type);
|
||||
|
||||
|
|
@ -117,7 +114,7 @@ public class TorBoxDebridClientTest
|
|||
}
|
||||
};
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
clientMock.Protected().Setup<Task<Response<List<AvailableTorrent?>>>>("GetTorrentAvailability", hash).ReturnsAsync(availability);
|
||||
|
||||
// Act
|
||||
|
|
@ -160,7 +157,7 @@ public class TorBoxDebridClientTest
|
|||
}
|
||||
};
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
clientMock.Protected().Setup<Task<Response<List<AvailableTorrent?>>>>("GetTorrentAvailability", hash).ReturnsAsync(torrentAvailability);
|
||||
clientMock.Protected().Setup<Task<Response<List<AvailableUsenet?>>>>("GetUsenetAvailability", hash).ReturnsAsync(usenetAvailability);
|
||||
|
||||
|
|
@ -189,7 +186,7 @@ public class TorBoxDebridClientTest
|
|||
Data = new()
|
||||
};
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
clientMock.Protected().Setup<Task<Response<List<AvailableTorrent?>>>>("GetTorrentAvailability", hash).ReturnsAsync(torrentAvailability);
|
||||
clientMock.Protected().Setup<Task<Response<List<AvailableUsenet?>>>>("GetUsenetAvailability", hash).ReturnsAsync(usenetAvailability);
|
||||
|
||||
|
|
@ -201,17 +198,17 @@ public class TorBoxDebridClientTest
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_CallsTorrentsControlById_WhenTypeIsTorrent()
|
||||
public async Task Delete_CallsTorrentsControl_WhenTypeIsTorrent()
|
||||
{
|
||||
// Arrange
|
||||
var torrent = new Torrent
|
||||
{
|
||||
RdId = "12345",
|
||||
RdId = "torrent-id",
|
||||
Type = DownloadType.Torrent
|
||||
};
|
||||
|
||||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
var torBoxClientMock = new Mock<ITorBoxNetClient>();
|
||||
|
||||
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
|
||||
|
|
@ -221,7 +218,7 @@ public class TorBoxDebridClientTest
|
|||
await clientMock.Object.Delete(torrent);
|
||||
|
||||
// Assert
|
||||
torrentsApiMock.Verify(m => m.ControlByIdAsync(12345, "delete", It.IsAny<CancellationToken>()), Times.Once);
|
||||
torrentsApiMock.Verify(m => m.ControlAsync("torrent-id", "delete", It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -235,7 +232,7 @@ public class TorBoxDebridClientTest
|
|||
};
|
||||
|
||||
var usenetApiMock = new Mock<IUsenetApi>();
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
var torBoxClientMock = new Mock<ITorBoxNetClient>();
|
||||
|
||||
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
|
||||
|
|
@ -261,7 +258,7 @@ public class TorBoxDebridClientTest
|
|||
var link = "https://torbox.app/d/123/456";
|
||||
|
||||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
var torBoxClientMock = new Mock<ITorBoxNetClient>();
|
||||
|
||||
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
|
||||
|
|
@ -294,7 +291,7 @@ public class TorBoxDebridClientTest
|
|||
var link = "https://torbox.app/d/123/456";
|
||||
|
||||
var usenetApiMock = new Mock<IUsenetApi>();
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
var torBoxClientMock = new Mock<ITorBoxNetClient>();
|
||||
|
||||
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
|
||||
|
|
@ -326,7 +323,7 @@ public class TorBoxDebridClientTest
|
|||
var name = "test-nzb";
|
||||
var usenetApiMock = new Mock<IUsenetApi>();
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings)
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
|
||||
{
|
||||
CallBase = true
|
||||
};
|
||||
|
|
@ -376,20 +373,25 @@ public class TorBoxDebridClientTest
|
|||
var torrent = new Torrent
|
||||
{
|
||||
Hash = "test-hash",
|
||||
RdId = "12345",
|
||||
RdFiles = JsonConvert.SerializeObject(files)
|
||||
};
|
||||
|
||||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
var torBoxClientMock = new Mock<ITorBoxNetClient>();
|
||||
|
||||
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
|
||||
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
|
||||
|
||||
torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new TorrentInfoResult
|
||||
{
|
||||
Id = 12345
|
||||
});
|
||||
|
||||
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
|
||||
|
||||
_settings.Current.Provider.PreferZippedDownloads = false;
|
||||
Settings.Get.Provider.PreferZippedDownloads = false;
|
||||
|
||||
// Act
|
||||
var result = await clientMock.Object.GetDownloadInfos(torrent);
|
||||
|
|
@ -418,21 +420,26 @@ public class TorBoxDebridClientTest
|
|||
var torrent = new Torrent
|
||||
{
|
||||
Hash = "test-hash",
|
||||
RdId = "12345",
|
||||
RdName = "TestTorrent",
|
||||
RdFiles = JsonConvert.SerializeObject(files),
|
||||
DownloadClient = DownloadClient.Aria2c
|
||||
};
|
||||
|
||||
_settings.Current.Provider.PreferZippedDownloads = true;
|
||||
Settings.Get.Provider.PreferZippedDownloads = true;
|
||||
|
||||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
var torBoxClientMock = new Mock<ITorBoxNetClient>();
|
||||
|
||||
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
|
||||
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
|
||||
|
||||
torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new TorrentInfoResult
|
||||
{
|
||||
Id = 12345
|
||||
});
|
||||
|
||||
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
|
||||
|
||||
// Act
|
||||
|
|
@ -457,7 +464,7 @@ public class TorBoxDebridClientTest
|
|||
var link = "https://torbox.app/fakedl/12345/6789";
|
||||
|
||||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
var torBoxClientMock = new Mock<ITorBoxNetClient>();
|
||||
|
||||
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
|
||||
|
|
@ -489,7 +496,7 @@ public class TorBoxDebridClientTest
|
|||
var link = "https://torbox.app/fakedl/12345/zip";
|
||||
|
||||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
var torBoxClientMock = new Mock<ITorBoxNetClient>();
|
||||
|
||||
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
|
||||
|
|
@ -531,13 +538,13 @@ public class TorBoxDebridClientTest
|
|||
};
|
||||
|
||||
var usenetApiMock = new Mock<IUsenetApi>();
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
var torBoxClientMock = new Mock<ITorBoxNetClient>();
|
||||
|
||||
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
|
||||
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
|
||||
|
||||
usenetApiMock.Setup(m => m.GetCurrentAsync(true, 1000, It.IsAny<CancellationToken>()))
|
||||
usenetApiMock.Setup(m => m.GetCurrentAsync(true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<UsenetInfoResult>
|
||||
{
|
||||
new()
|
||||
|
|
@ -549,7 +556,7 @@ public class TorBoxDebridClientTest
|
|||
|
||||
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
|
||||
|
||||
_settings.Current.Provider.PreferZippedDownloads = false;
|
||||
Settings.Get.Provider.PreferZippedDownloads = false;
|
||||
|
||||
// Act
|
||||
var result = await clientMock.Object.GetDownloadInfos(torrent);
|
||||
|
|
@ -572,7 +579,7 @@ public class TorBoxDebridClientTest
|
|||
var link = "https://torbox.app/fakedl/98765/4321";
|
||||
|
||||
var usenetApiMock = new Mock<IUsenetApi>();
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
var torBoxClientMock = new Mock<ITorBoxNetClient>();
|
||||
|
||||
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
|
||||
|
|
@ -600,7 +607,7 @@ public class TorBoxDebridClientTest
|
|||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var userApiMock = new Mock<IUserApi>();
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings)
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
|
||||
{
|
||||
CallBase = true
|
||||
};
|
||||
|
|
@ -640,7 +647,7 @@ public class TorBoxDebridClientTest
|
|||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var userApiMock = new Mock<IUserApi>();
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings)
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
|
||||
{
|
||||
CallBase = true
|
||||
};
|
||||
|
|
@ -678,7 +685,7 @@ public class TorBoxDebridClientTest
|
|||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var userApiMock = new Mock<IUserApi>();
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings)
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
|
||||
{
|
||||
CallBase = true
|
||||
};
|
||||
|
|
@ -717,7 +724,7 @@ public class TorBoxDebridClientTest
|
|||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var userApiMock = new Mock<IUserApi>();
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings)
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
|
||||
{
|
||||
CallBase = true
|
||||
};
|
||||
|
|
@ -760,7 +767,7 @@ public class TorBoxDebridClientTest
|
|||
var torrentsApiMock = new Mock<ITorrentsApi>();
|
||||
var userApiMock = new Mock<IUserApi>();
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings)
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
|
||||
{
|
||||
CallBase = true
|
||||
};
|
||||
|
|
@ -799,7 +806,7 @@ public class TorBoxDebridClientTest
|
|||
var nzbLink = "https://example.com/test.nzb";
|
||||
var usenetApiMock = new Mock<IUsenetApi>();
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings)
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
|
||||
{
|
||||
CallBase = true
|
||||
};
|
||||
|
|
@ -830,7 +837,7 @@ public class TorBoxDebridClientTest
|
|||
var name = "test.nzb";
|
||||
var usenetApiMock = new Mock<IUsenetApi>();
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings)
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
|
||||
{
|
||||
CallBase = true
|
||||
};
|
||||
|
|
@ -865,7 +872,7 @@ public class TorBoxDebridClientTest
|
|||
Filename = "test-file"
|
||||
};
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
|
||||
// Act
|
||||
var result = await clientMock.Object.UpdateData(torrent, torrentClientTorrent);
|
||||
|
|
@ -891,7 +898,7 @@ public class TorBoxDebridClientTest
|
|||
Filename = "test-torrent"
|
||||
};
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object);
|
||||
|
||||
// Act
|
||||
await clientMock.Object.UpdateData(torrent, torrentClientTorrent);
|
||||
|
|
@ -904,30 +911,4 @@ public class TorBoxDebridClientTest
|
|||
It.Is<Func<It.IsAnyType, Exception?, String>>((v, t) => true)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateData_SetsProcessingStatus_WhenTorBoxStatusIsUnmappedAndTorrentWasQueued()
|
||||
{
|
||||
// Arrange
|
||||
var torrent = new Torrent
|
||||
{
|
||||
RdId = "test-rd-id",
|
||||
RdStatus = TorrentStatus.Queued,
|
||||
RdName = "test-torrent"
|
||||
};
|
||||
|
||||
var torrentClientTorrent = new DebridClientTorrent
|
||||
{
|
||||
Status = "some-unknown-status",
|
||||
Filename = "test-torrent"
|
||||
};
|
||||
|
||||
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings);
|
||||
|
||||
// Act
|
||||
var result = await clientMock.Object.UpdateData(torrent, torrentClientTorrent);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TorrentStatus.Processing, result.RdStatus);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Service.Helpers;
|
||||
using RdtClient.Service.Services;
|
||||
|
||||
namespace RdtClient.Service.Test.Services;
|
||||
|
||||
public class TorrentRunnerTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task Tick_ShouldNotRequeueCompletedErrorTorrent()
|
||||
{
|
||||
var testSettings = new TestSettings();
|
||||
var runnerState = new TorrentRunnerState();
|
||||
|
||||
testSettings.Current.Provider.ApiKey = "test-api-key";
|
||||
testSettings.Current.Provider.Provider = Provider.RealDebrid;
|
||||
testSettings.Current.Provider.MaxParallelDownloads = 1;
|
||||
testSettings.Current.DownloadClient.DownloadPath = "/downloads";
|
||||
|
||||
var erroredTorrent = new Torrent
|
||||
{
|
||||
TorrentId = Guid.NewGuid(),
|
||||
Hash = "hash-1",
|
||||
RdName = "Torrent 1",
|
||||
FileOrMagnet = "magnet:?xt=urn:btih:hash-1",
|
||||
Type = DownloadType.Torrent,
|
||||
RdStatus = TorrentStatus.Queued,
|
||||
DeleteOnError = 10,
|
||||
Error = "Could not add to provider: Infringing file",
|
||||
Completed = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
Downloads = new List<Download>()
|
||||
};
|
||||
|
||||
var torrentDataMock = new Mock<ITorrentData>(MockBehavior.Strict);
|
||||
|
||||
torrentDataMock.Setup(m => m.Get())
|
||||
.ReturnsAsync(new List<Torrent>
|
||||
{
|
||||
erroredTorrent
|
||||
});
|
||||
|
||||
var torrents = new Torrents(Mock.Of<ILogger<Torrents>>(),
|
||||
torrentDataMock.Object,
|
||||
Mock.Of<IDownloads>(),
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
testSettings,
|
||||
runnerState);
|
||||
|
||||
var torrentRunner = new TorrentRunner(Mock.Of<ILogger<TorrentRunner>>(),
|
||||
torrents,
|
||||
new(null!),
|
||||
new(null!, torrents),
|
||||
Mock.Of<IHttpClientFactory>(),
|
||||
new RateLimitCoordinator(),
|
||||
testSettings,
|
||||
runnerState);
|
||||
|
||||
await torrentRunner.Tick();
|
||||
|
||||
torrentDataMock.Verify(m => m.UpdateComplete(It.IsAny<Guid>(),
|
||||
It.IsAny<String?>(),
|
||||
It.IsAny<DateTimeOffset?>(),
|
||||
It.IsAny<Boolean>()),
|
||||
Times.Never);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -10,6 +9,7 @@ using RdtClient.Data.Enums;
|
|||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.Services;
|
||||
using RdtClient.Service.Test.Helpers;
|
||||
using RdtClient.Service.Wrappers;
|
||||
using DownloadClient = RdtClient.Data.Enums.DownloadClient;
|
||||
using TorrentsService = RdtClient.Service.Services.Torrents;
|
||||
|
|
@ -90,18 +90,23 @@ public class TorrentsTest
|
|||
mocks.TorrentDataMock.Setup(t => t.GetById(torrent.TorrentId)).Returns(Task.FromResult<Torrent?>(torrent));
|
||||
mocks.DownloadsMock.Setup(d => d.GetForTorrent(torrent.TorrentId)).ReturnsAsync(downloads);
|
||||
|
||||
var category = torrent.Category!;
|
||||
var torrentName = torrent.RdName!;
|
||||
var fileName = downloads[0].FileName!;
|
||||
String downloadPath;
|
||||
String torrentPath;
|
||||
String filePath;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
if (OSHelper.IsLinux)
|
||||
{
|
||||
settings.DownloadClient.DownloadPath = @"C:\Downloads";
|
||||
downloadPath = $"{settings.DownloadClient.DownloadPath}/{torrent.Category}";
|
||||
torrentPath = $"{downloadPath}/{torrent.RdName}";
|
||||
filePath = $"{torrentPath}/{downloads[0].FileName}";
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings.Get.DownloadClient.DownloadPath = settings.DownloadClient.DownloadPath = @"C:\Downloads";
|
||||
downloadPath = @$"{settings.DownloadClient.DownloadPath}\{torrent.Category}";
|
||||
torrentPath = @$"{downloadPath}\{torrent.RdName}";
|
||||
filePath = @$"{torrentPath}\{downloads[0].FileName}";
|
||||
}
|
||||
|
||||
var downloadPath = Path.Combine(settings.DownloadClient.DownloadPath, category);
|
||||
var torrentPath = Path.Combine(downloadPath, torrentName);
|
||||
var filePath = Path.Combine(torrentPath, fileName);
|
||||
|
||||
var fileSystemMock = new MockFileSystem(new Dictionary<String, MockFileData>
|
||||
{
|
||||
|
|
@ -120,9 +125,7 @@ public class TorrentsTest
|
|||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
new TestSettings(),
|
||||
new TorrentRunnerState());
|
||||
null!);
|
||||
|
||||
mocks.ProcessMock.Setup(p => p.WaitForExit(It.IsAny<Int32>())).Returns(true);
|
||||
|
||||
|
|
@ -167,9 +170,9 @@ public class TorrentsTest
|
|||
mocks.TorrentDataMock.Setup(t => t.GetById(torrent.TorrentId)).Returns(Task.FromResult<Torrent?>(torrent));
|
||||
mocks.DownloadsMock.Setup(d => d.GetForTorrent(torrent.TorrentId)).ReturnsAsync(downloads);
|
||||
|
||||
var downloadPath = Path.Combine(settings.DownloadClient.DownloadPath, torrent.Category ?? "");
|
||||
var torrentPath = Path.Combine(downloadPath, torrent.RdName ?? "");
|
||||
var filePath = Path.Combine(torrentPath, downloads[0].FileName ?? "");
|
||||
var downloadPath = $"{settings.DownloadClient.DownloadPath}/{torrent.Category}";
|
||||
var torrentPath = $"{downloadPath}/{torrent.RdName}";
|
||||
var filePath = $"{torrentPath}/{downloads[0].FileName}";
|
||||
|
||||
var fileSystemMock = new MockFileSystem(new Dictionary<String, MockFileData>
|
||||
{
|
||||
|
|
@ -188,9 +191,7 @@ public class TorrentsTest
|
|||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
new TestSettings(),
|
||||
new TorrentRunnerState());
|
||||
null!);
|
||||
|
||||
//Act
|
||||
await torrents.RunTorrentComplete(torrent.TorrentId, settings);
|
||||
|
|
@ -217,9 +218,9 @@ public class TorrentsTest
|
|||
mocks.TorrentDataMock.Setup(t => t.GetById(torrent.TorrentId)).Returns(Task.FromResult<Torrent?>(torrent));
|
||||
mocks.DownloadsMock.Setup(d => d.GetForTorrent(torrent.TorrentId)).ReturnsAsync(downloads);
|
||||
|
||||
var downloadPath = Path.Combine(settings.DownloadClient.DownloadPath, torrent.Category ?? "");
|
||||
var torrentPath = Path.Combine(downloadPath, torrent.RdName ?? "");
|
||||
var filePath = Path.Combine(torrentPath, downloads[0].FileName ?? "");
|
||||
var downloadPath = $"{settings.DownloadClient.DownloadPath}/{torrent.Category}";
|
||||
var torrentPath = $"{downloadPath}/{torrent.RdName}";
|
||||
var filePath = $"{torrentPath}/{downloads[0].FileName}";
|
||||
|
||||
var fileSystemMock = new MockFileSystem(new Dictionary<String, MockFileData>
|
||||
{
|
||||
|
|
@ -238,9 +239,7 @@ public class TorrentsTest
|
|||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
new TestSettings(),
|
||||
new TorrentRunnerState());
|
||||
null!);
|
||||
|
||||
mocks.ProcessMock.Setup(p => p.WaitForExit(It.IsAny<Int32>()))
|
||||
.Callback(() =>
|
||||
|
|
@ -286,9 +285,9 @@ public class TorrentsTest
|
|||
mocks.TorrentDataMock.Setup(t => t.GetById(torrent.TorrentId)).Returns(Task.FromResult<Torrent?>(torrent));
|
||||
mocks.DownloadsMock.Setup(d => d.GetForTorrent(torrent.TorrentId)).ReturnsAsync(downloads);
|
||||
|
||||
var downloadPath = Path.Combine(settings.DownloadClient.DownloadPath, torrent.Category ?? "");
|
||||
var torrentPath = Path.Combine(downloadPath, torrent.RdName ?? "");
|
||||
var filePath = Path.Combine(torrentPath, downloads[0].FileName ?? "");
|
||||
var downloadPath = $"{settings.DownloadClient.DownloadPath}/{torrent.Category}";
|
||||
var torrentPath = $"{downloadPath}/{torrent.RdName}";
|
||||
var filePath = $"{torrentPath}/{downloads[0].FileName}";
|
||||
|
||||
var fileSystemMock = new MockFileSystem(new Dictionary<String, MockFileData>
|
||||
{
|
||||
|
|
@ -307,9 +306,7 @@ public class TorrentsTest
|
|||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
new TestSettings(),
|
||||
new TorrentRunnerState());
|
||||
null!);
|
||||
|
||||
mocks.ProcessMock.Setup(p => p.WaitForExit(It.IsAny<Int32>()))
|
||||
.Callback(() =>
|
||||
|
|
@ -372,9 +369,7 @@ public class TorrentsTest
|
|||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
new TestSettings(),
|
||||
new TorrentRunnerState());
|
||||
null!);
|
||||
|
||||
// Act
|
||||
await torrents.AddNzbFileToDebridQueue(bytes, "filename.nzb", torrent);
|
||||
|
|
@ -422,9 +417,7 @@ public class TorrentsTest
|
|||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
new TestSettings(),
|
||||
new TorrentRunnerState());
|
||||
null!);
|
||||
|
||||
// Act
|
||||
await torrents.AddNzbLinkToDebridQueue(link, torrent);
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.Services;
|
||||
|
||||
namespace RdtClient.Service.Test;
|
||||
|
||||
internal sealed class TestSettings(DbSettings? settings = null) : ISettings
|
||||
{
|
||||
public DbSettings Current { get; } = settings ?? new();
|
||||
|
||||
public String DefaultSavePath
|
||||
{
|
||||
get
|
||||
{
|
||||
var downloadPath = Current.DownloadClient.MappedPath.TrimEnd('\\').TrimEnd('/');
|
||||
|
||||
return downloadPath + Path.DirectorySeparatorChar;
|
||||
}
|
||||
}
|
||||
|
||||
public IList<SettingProperty> GetAll()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public Task Update(IList<SettingProperty> settings)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Update(String settingId, Object? value)
|
||||
{
|
||||
SetValue(Current, settingId.Split(':'), 0, value);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void SetValue(Object target, IReadOnlyList<String> path, Int32 index, Object? value)
|
||||
{
|
||||
var property = target.GetType().GetProperty(path[index]) ?? throw new($"Unknown setting {String.Join(":", path)}");
|
||||
|
||||
if (index < path.Count - 1)
|
||||
{
|
||||
SetValue(property.GetValue(target)!, path, index + 1, value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
property.SetValue(target, null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||
var convertedValue = propertyType.IsEnum ? Enum.Parse(propertyType, value.ToString()!) : Convert.ChangeType(value, propertyType);
|
||||
property.SetValue(target, convertedValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ using DownloadClient = RdtClient.Data.Enums.DownloadClient;
|
|||
|
||||
namespace RdtClient.Service.BackgroundServices;
|
||||
|
||||
public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider serviceProvider, ISettings settings, ITorrentRunnerState runnerState) : BackgroundService
|
||||
public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider serviceProvider) : BackgroundService
|
||||
{
|
||||
private static DiskSpaceStatus? _lastStatus;
|
||||
private Boolean _isPausedForLowDiskSpace;
|
||||
|
|
@ -34,7 +34,7 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
|
|||
{
|
||||
try
|
||||
{
|
||||
var minimumFreeSpaceGB = settings.Current.DownloadClient.MinimumFreeSpaceGB;
|
||||
var minimumFreeSpaceGB = Settings.Get.DownloadClient.MinimumFreeSpaceGB;
|
||||
|
||||
if (minimumFreeSpaceGB <= 0)
|
||||
{
|
||||
|
|
@ -43,14 +43,14 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
|
|||
continue;
|
||||
}
|
||||
|
||||
var intervalMinutes = settings.Current.DownloadClient.DiskSpaceCheckIntervalMinutes;
|
||||
var intervalMinutes = Settings.Get.DownloadClient.DiskSpaceCheckIntervalMinutes;
|
||||
|
||||
if (intervalMinutes < 1)
|
||||
{
|
||||
intervalMinutes = 1;
|
||||
}
|
||||
|
||||
var downloadPath = settings.Current.DownloadClient.DownloadPath;
|
||||
var downloadPath = Settings.Get.DownloadClient.DownloadPath;
|
||||
logger.LogDebug($"Checking disk space for path: {downloadPath}");
|
||||
|
||||
if (!Directory.Exists(downloadPath))
|
||||
|
|
@ -78,7 +78,7 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
|
|||
|
||||
var pausedCount = 0;
|
||||
|
||||
foreach (var download in runnerState.ActiveDownloadClients)
|
||||
foreach (var download in TorrentRunner.ActiveDownloadClients)
|
||||
{
|
||||
if (download.Value.Type == DownloadClient.Bezzad)
|
||||
{
|
||||
|
|
@ -90,7 +90,7 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
|
|||
|
||||
logger.LogInformation($"Paused {pausedCount} active Bezzad downloads");
|
||||
|
||||
runnerState.IsPausedForLowDiskSpace = true;
|
||||
TorrentRunner.IsPausedForLowDiskSpace = true;
|
||||
_isPausedForLowDiskSpace = true;
|
||||
|
||||
var status = new DiskSpaceStatus
|
||||
|
|
@ -125,7 +125,7 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
|
|||
|
||||
var resumedCount = 0;
|
||||
|
||||
foreach (var download in runnerState.ActiveDownloadClients)
|
||||
foreach (var download in TorrentRunner.ActiveDownloadClients)
|
||||
{
|
||||
if (download.Value.Type == DownloadClient.Bezzad)
|
||||
{
|
||||
|
|
@ -137,7 +137,7 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
|
|||
|
||||
logger.LogInformation($"Resumed {resumedCount} Bezzad downloads");
|
||||
|
||||
runnerState.IsPausedForLowDiskSpace = false;
|
||||
TorrentRunner.IsPausedForLowDiskSpace = false;
|
||||
_isPausedForLowDiskSpace = false;
|
||||
|
||||
var status = new DiskSpaceStatus
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using RdtClient.Service.Services;
|
|||
|
||||
namespace RdtClient.Service.BackgroundServices;
|
||||
|
||||
public class ProviderUpdater(ILogger<ProviderUpdater> logger, IServiceProvider serviceProvider, ISettings settings) : BackgroundService
|
||||
public class ProviderUpdater(ILogger<ProviderUpdater> logger, IServiceProvider serviceProvider) : BackgroundService
|
||||
{
|
||||
private static DateTime _nextUpdate = DateTime.UtcNow;
|
||||
|
||||
|
|
@ -18,23 +18,22 @@ public class ProviderUpdater(ILogger<ProviderUpdater> logger, IServiceProvider s
|
|||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var torrentService = scope.ServiceProvider.GetRequiredService<Torrents>();
|
||||
var torrentRunner = scope.ServiceProvider.GetRequiredService<TorrentRunner>();
|
||||
logger.LogInformation("ProviderUpdater started.");
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var torrentService = scope.ServiceProvider.GetRequiredService<Torrents>();
|
||||
var torrentRunner = scope.ServiceProvider.GetRequiredService<TorrentRunner>();
|
||||
|
||||
try
|
||||
{
|
||||
var torrents = await torrentService.Get();
|
||||
|
||||
if (_nextUpdate < DateTime.UtcNow && (settings.Current.Provider.AutoImport || torrents.Any(t => t.RdStatus != TorrentStatus.Finished) || RdtHub.HasConnections))
|
||||
if (_nextUpdate < DateTime.UtcNow && (Settings.Get.Provider.AutoImport || torrents.Any(t => t.RdStatus != TorrentStatus.Finished) || RdtHub.HasConnections))
|
||||
{
|
||||
logger.LogDebug($"Updating torrent info from debrid provider");
|
||||
|
||||
var updateTime = settings.Current.Provider.CheckInterval * 3;
|
||||
var updateTime = Settings.Get.Provider.CheckInterval * 3;
|
||||
|
||||
if (updateTime < 30)
|
||||
{
|
||||
|
|
@ -43,7 +42,7 @@ public class ProviderUpdater(ILogger<ProviderUpdater> logger, IServiceProvider s
|
|||
|
||||
if (RdtHub.HasConnections)
|
||||
{
|
||||
updateTime = settings.Current.Provider.CheckInterval;
|
||||
updateTime = Settings.Get.Provider.CheckInterval;
|
||||
|
||||
if (updateTime < 5)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Reflection;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
|
@ -40,9 +39,6 @@ public class Startup(IServiceProvider serviceProvider) : IHostedService
|
|||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Ready = false;
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,19 +16,15 @@ public class TaskRunner(ILogger<TaskRunner> logger, IServiceProvider serviceProv
|
|||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var torrentRunner = scope.ServiceProvider.GetRequiredService<TorrentRunner>();
|
||||
|
||||
logger.LogInformation("TaskRunner started.");
|
||||
|
||||
using (var startupScope = serviceProvider.CreateScope())
|
||||
{
|
||||
var startupRunner = startupScope.ServiceProvider.GetRequiredService<TorrentRunner>();
|
||||
await startupRunner.Initialize();
|
||||
}
|
||||
await torrentRunner.Initialize();
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var torrentRunner = scope.ServiceProvider.GetRequiredService<TorrentRunner>();
|
||||
|
||||
try
|
||||
{
|
||||
await torrentRunner.Tick();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using LogLevel = Microsoft.Extensions.Logging.LogLevel;
|
|||
|
||||
namespace RdtClient.Service.BackgroundServices;
|
||||
|
||||
public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProvider serviceProvider, ISettings settings) : BackgroundService
|
||||
public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProvider serviceProvider) : BackgroundService
|
||||
{
|
||||
private DateTime _prevCheck = DateTime.MinValue;
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
|
|||
{
|
||||
try
|
||||
{
|
||||
var nextCheck = _prevCheck.AddSeconds(Math.Max(settings.Current.Watch.Interval, 10));
|
||||
var nextCheck = _prevCheck.AddSeconds(Math.Max(Settings.Get.Watch.Interval, 10));
|
||||
|
||||
if (DateTime.Now < nextCheck)
|
||||
{
|
||||
|
|
@ -38,25 +38,25 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
|
|||
|
||||
_prevCheck = DateTime.Now;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(settings.Current.Watch.Path))
|
||||
if (String.IsNullOrWhiteSpace(Settings.Get.Watch.Path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var processedStorePath = Path.Combine(settings.Current.Watch.Path, "processed");
|
||||
var errorStorePath = Path.Combine(settings.Current.Watch.Path, "error");
|
||||
var processedStorePath = Path.Combine(Settings.Get.Watch.Path, "processed");
|
||||
var errorStorePath = Path.Combine(Settings.Get.Watch.Path, "error");
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(settings.Current.Watch.ProcessedPath))
|
||||
if (!String.IsNullOrWhiteSpace(Settings.Get.Watch.ProcessedPath))
|
||||
{
|
||||
processedStorePath = settings.Current.Watch.ProcessedPath;
|
||||
processedStorePath = Settings.Get.Watch.ProcessedPath;
|
||||
}
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(settings.Current.Watch.ErrorPath))
|
||||
if (!String.IsNullOrWhiteSpace(Settings.Get.Watch.ErrorPath))
|
||||
{
|
||||
errorStorePath = settings.Current.Watch.ErrorPath;
|
||||
errorStorePath = Settings.Get.Watch.ErrorPath;
|
||||
}
|
||||
|
||||
var torrentFiles = Directory.GetFiles(settings.Current.Watch.Path, "*.*", SearchOption.TopDirectoryOnly);
|
||||
var torrentFiles = Directory.GetFiles(Settings.Get.Watch.Path, "*.*", SearchOption.TopDirectoryOnly);
|
||||
|
||||
foreach (var torrentFile in torrentFiles)
|
||||
{
|
||||
|
|
@ -78,22 +78,22 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
|
|||
|
||||
var torrent = new Torrent
|
||||
{
|
||||
DownloadClient = settings.Current.DownloadClient.Client,
|
||||
Category = settings.Current.Watch.Default.Category,
|
||||
HostDownloadAction = settings.Current.Watch.Default.HostDownloadAction,
|
||||
FinishedActionDelay = settings.Current.Watch.Default.FinishedActionDelay,
|
||||
DownloadAction = settings.Current.Watch.Default.OnlyDownloadAvailableFiles
|
||||
DownloadClient = Settings.Get.DownloadClient.Client,
|
||||
Category = Settings.Get.Watch.Default.Category,
|
||||
HostDownloadAction = Settings.Get.Watch.Default.HostDownloadAction,
|
||||
FinishedActionDelay = Settings.Get.Watch.Default.FinishedActionDelay,
|
||||
DownloadAction = Settings.Get.Watch.Default.OnlyDownloadAvailableFiles
|
||||
? TorrentDownloadAction.DownloadAvailableFiles
|
||||
: TorrentDownloadAction.DownloadAll,
|
||||
FinishedAction = settings.Current.Watch.Default.FinishedAction,
|
||||
DownloadMinSize = settings.Current.Watch.Default.MinFileSize,
|
||||
IncludeRegex = settings.Current.Watch.Default.IncludeRegex,
|
||||
ExcludeRegex = settings.Current.Watch.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = settings.Current.Watch.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = settings.Current.Watch.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = settings.Current.Watch.Default.DeleteOnError,
|
||||
Lifetime = settings.Current.Watch.Default.TorrentLifetime,
|
||||
Priority = settings.Current.Watch.Default.Priority > 0 ? settings.Current.Watch.Default.Priority : null
|
||||
FinishedAction = Settings.Get.Watch.Default.FinishedAction,
|
||||
DownloadMinSize = Settings.Get.Watch.Default.MinFileSize,
|
||||
IncludeRegex = Settings.Get.Watch.Default.IncludeRegex,
|
||||
ExcludeRegex = Settings.Get.Watch.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = Settings.Get.Watch.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = Settings.Get.Watch.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = Settings.Get.Watch.Default.DeleteOnError,
|
||||
Lifetime = Settings.Get.Watch.Default.TorrentLifetime,
|
||||
Priority = Settings.Get.Watch.Default.Priority > 0 ? Settings.Get.Watch.Default.Priority : null
|
||||
};
|
||||
|
||||
if (fileInfo.Extension == ".torrent")
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ public static class DiConfig
|
|||
services.AddScoped<AllDebridDebridClient>();
|
||||
|
||||
services.AddSingleton<IRateLimitCoordinator, RateLimitCoordinator>();
|
||||
services.AddSingleton(TorrentRunner.SharedState);
|
||||
services.AddSingleton<IProcessFactory, ProcessFactory>();
|
||||
services.AddSingleton<IFileSystem, FileSystem>();
|
||||
|
||||
|
|
@ -42,8 +41,7 @@ public static class DiConfig
|
|||
services.AddScoped<Sabnzbd>();
|
||||
services.AddScoped<RemoteService>();
|
||||
services.AddScoped<RealDebridDebridClient>();
|
||||
services.AddSingleton<Settings>();
|
||||
services.AddSingleton<ISettings>(serviceProvider => serviceProvider.GetRequiredService<Settings>());
|
||||
services.AddScoped<Settings>();
|
||||
services.AddScoped<TorBoxDebridClient>();
|
||||
services.AddScoped<Torrents>();
|
||||
services.AddScoped<TorrentRunner>();
|
||||
|
|
@ -53,7 +51,7 @@ public static class DiConfig
|
|||
services.AddSingleton<ITrackerListGrabber, TrackerListGrabber>();
|
||||
services.AddSingleton<IEnricher, Enricher>();
|
||||
|
||||
services.AddScoped<IAuthorizationHandler, AuthSettingHandler>();
|
||||
services.AddSingleton<IAuthorizationHandler, AuthSettingHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SabnzbdHandler>();
|
||||
|
||||
services.AddHostedService<DiskSpaceMonitor>();
|
||||
|
|
|
|||
|
|
@ -1,232 +0,0 @@
|
|||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
|
||||
namespace RdtClient.Service.Helpers;
|
||||
|
||||
public static class TorrentDtoMapper
|
||||
{
|
||||
public static TorrentDto ToListDto(Torrent torrent, Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats)
|
||||
{
|
||||
return ToDto(torrent, getDownloadStats, includeDownloads: false, includeFiles: false, includeFileOrMagnet: false);
|
||||
}
|
||||
|
||||
public static TorrentDto ToUpdateDto(Torrent torrent, Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats)
|
||||
{
|
||||
return ToDto(torrent, getDownloadStats, includeDownloads: true, includeFiles: false, includeFileOrMagnet: false);
|
||||
}
|
||||
|
||||
public static TorrentDto ToDetailDto(Torrent torrent, Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats)
|
||||
{
|
||||
return ToDto(torrent, getDownloadStats, includeDownloads: true, includeFiles: true, includeFileOrMagnet: true);
|
||||
}
|
||||
|
||||
private static TorrentDto ToDto(Torrent torrent,
|
||||
Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats,
|
||||
Boolean includeDownloads,
|
||||
Boolean includeFiles,
|
||||
Boolean includeFileOrMagnet)
|
||||
{
|
||||
var downloads = includeDownloads ? torrent.Downloads.Select(download => ToDto(download, getDownloadStats)).ToList() : [];
|
||||
|
||||
return new()
|
||||
{
|
||||
TorrentId = torrent.TorrentId,
|
||||
Hash = torrent.Hash,
|
||||
Category = torrent.Category,
|
||||
DownloadAction = torrent.DownloadAction,
|
||||
FinishedAction = torrent.FinishedAction,
|
||||
FinishedActionDelay = torrent.FinishedActionDelay,
|
||||
HostDownloadAction = torrent.HostDownloadAction,
|
||||
DownloadMinSize = torrent.DownloadMinSize,
|
||||
IncludeRegex = torrent.IncludeRegex,
|
||||
ExcludeRegex = torrent.ExcludeRegex,
|
||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||
DownloadClient = torrent.DownloadClient,
|
||||
Added = torrent.Added,
|
||||
FilesSelected = torrent.FilesSelected,
|
||||
Completed = torrent.Completed,
|
||||
Type = torrent.Type,
|
||||
FileOrMagnet = includeFileOrMagnet ? torrent.FileOrMagnet : null,
|
||||
IsFile = torrent.IsFile,
|
||||
Priority = torrent.Priority,
|
||||
RetryCount = torrent.RetryCount,
|
||||
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
|
||||
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||
DeleteOnError = torrent.DeleteOnError,
|
||||
Lifetime = torrent.Lifetime,
|
||||
Error = torrent.Error,
|
||||
RdId = torrent.RdId,
|
||||
RdName = torrent.RdName,
|
||||
RdSize = torrent.RdSize,
|
||||
RdHost = torrent.RdHost,
|
||||
RdSplit = torrent.RdSplit,
|
||||
RdProgress = torrent.RdProgress,
|
||||
RdStatus = torrent.RdStatus,
|
||||
RdStatusRaw = torrent.RdStatusRaw,
|
||||
RdAdded = torrent.RdAdded,
|
||||
RdEnded = torrent.RdEnded,
|
||||
RdSpeed = torrent.RdSpeed,
|
||||
RdSeeders = torrent.RdSeeders,
|
||||
StatusText = GetStatusText(torrent, getDownloadStats),
|
||||
FilesCount = torrent.Files.Count,
|
||||
DownloadsCount = torrent.Downloads.Count,
|
||||
Files = includeFiles ? torrent.Files : [],
|
||||
Downloads = downloads
|
||||
};
|
||||
}
|
||||
|
||||
private static DownloadDto ToDto(Download download, Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats)
|
||||
{
|
||||
var (speed, bytesTotal, bytesDone) = getDownloadStats(download.DownloadId);
|
||||
|
||||
return new()
|
||||
{
|
||||
DownloadId = download.DownloadId,
|
||||
TorrentId = download.TorrentId,
|
||||
Path = download.Path,
|
||||
Link = download.Link,
|
||||
Added = download.Added,
|
||||
DownloadQueued = download.DownloadQueued,
|
||||
DownloadStarted = download.DownloadStarted,
|
||||
DownloadFinished = download.DownloadFinished,
|
||||
UnpackingQueued = download.UnpackingQueued,
|
||||
UnpackingStarted = download.UnpackingStarted,
|
||||
UnpackingFinished = download.UnpackingFinished,
|
||||
Completed = download.Completed,
|
||||
RetryCount = download.RetryCount,
|
||||
Error = download.Error,
|
||||
BytesTotal = bytesTotal,
|
||||
BytesDone = bytesDone,
|
||||
Speed = speed
|
||||
};
|
||||
}
|
||||
|
||||
private static String GetStatusText(Torrent torrent, Func<Guid, (Int64 Speed, Int64 BytesTotal, Int64 BytesDone)> getDownloadStats)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(torrent.Error))
|
||||
{
|
||||
return torrent.Error;
|
||||
}
|
||||
|
||||
if (torrent.Downloads.Count > 0)
|
||||
{
|
||||
var allFinished = true;
|
||||
var downloadingCount = 0;
|
||||
var downloadedCount = 0;
|
||||
Int64 downloadingBytesDone = 0;
|
||||
Int64 downloadingBytesTotal = 0;
|
||||
Int64 downloadingSpeed = 0;
|
||||
var unpackingCount = 0;
|
||||
var unpackedCount = 0;
|
||||
Int64 unpackingBytesDone = 0;
|
||||
Int64 unpackingBytesTotal = 0;
|
||||
var queuedForUnpackingCount = 0;
|
||||
var queuedForDownloadingCount = 0;
|
||||
|
||||
foreach (var download in torrent.Downloads)
|
||||
{
|
||||
if (download.Completed == null)
|
||||
{
|
||||
allFinished = false;
|
||||
}
|
||||
|
||||
var (speed, bytesTotal, bytesDone) = getDownloadStats(download.DownloadId);
|
||||
|
||||
if (download.DownloadFinished != null)
|
||||
{
|
||||
downloadedCount += 1;
|
||||
}
|
||||
|
||||
if (download.DownloadStarted != null && download.DownloadFinished == null && bytesDone > 0)
|
||||
{
|
||||
downloadingCount += 1;
|
||||
downloadingBytesDone += bytesDone;
|
||||
downloadingBytesTotal += bytesTotal;
|
||||
downloadingSpeed += speed;
|
||||
}
|
||||
|
||||
if (download.UnpackingFinished != null)
|
||||
{
|
||||
unpackedCount += 1;
|
||||
}
|
||||
|
||||
if (download.UnpackingStarted != null && download.UnpackingFinished == null && bytesDone > 0)
|
||||
{
|
||||
unpackingCount += 1;
|
||||
unpackingBytesDone += bytesDone;
|
||||
unpackingBytesTotal += bytesTotal;
|
||||
}
|
||||
|
||||
if (download.UnpackingQueued != null && download.UnpackingStarted == null)
|
||||
{
|
||||
queuedForUnpackingCount += 1;
|
||||
}
|
||||
|
||||
if (download.DownloadStarted == null && download.DownloadFinished == null)
|
||||
{
|
||||
queuedForDownloadingCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (allFinished)
|
||||
{
|
||||
return "Finished";
|
||||
}
|
||||
|
||||
if (downloadingCount > 0)
|
||||
{
|
||||
var progress = downloadingBytesTotal == 0 ? 0 : (Double)downloadingBytesDone / downloadingBytesTotal * 100;
|
||||
|
||||
return $"Downloading file {downloadingCount + downloadedCount}/{torrent.Downloads.Count} ({progress:0.00}% - {FileSizeHelper.FormatSize(downloadingSpeed)}/s)";
|
||||
}
|
||||
|
||||
if (unpackingCount > 0)
|
||||
{
|
||||
var progress = unpackingBytesTotal == 0 ? 0 : (Double)unpackingBytesDone / unpackingBytesTotal * 100;
|
||||
|
||||
return $"Extracting file {unpackingCount + unpackedCount}/{torrent.Downloads.Count} ({progress:0.00}%)";
|
||||
}
|
||||
|
||||
if (queuedForUnpackingCount > 0)
|
||||
{
|
||||
return "Queued for unpacking";
|
||||
}
|
||||
|
||||
if (queuedForDownloadingCount > 0)
|
||||
{
|
||||
return "Queued for downloading";
|
||||
}
|
||||
|
||||
if (unpackedCount > 0)
|
||||
{
|
||||
return "Files unpacked";
|
||||
}
|
||||
|
||||
if (downloadedCount > 0)
|
||||
{
|
||||
return "Files downloaded to host";
|
||||
}
|
||||
}
|
||||
|
||||
if (torrent.Completed != null)
|
||||
{
|
||||
return "Finished";
|
||||
}
|
||||
|
||||
var prefix = torrent.Type == DownloadType.Nzb ? "NZB" : "Torrent";
|
||||
|
||||
return torrent.RdStatus switch
|
||||
{
|
||||
TorrentStatus.Queued => "Not Yet Added to Provider",
|
||||
TorrentStatus.Downloading when torrent.RdSeeders < 1 && torrent.Type != DownloadType.Nzb => "Torrent stalled",
|
||||
TorrentStatus.Downloading => $"{prefix} downloading ({torrent.RdProgress}% - {FileSizeHelper.FormatSize(torrent.RdSpeed)}/s)",
|
||||
TorrentStatus.Processing => $"{prefix} processing",
|
||||
TorrentStatus.WaitingForFileSelection => $"{prefix} waiting for file selection",
|
||||
TorrentStatus.Error => $"{prefix} error: {torrent.RdStatusRaw}",
|
||||
TorrentStatus.Finished => $"{prefix} finished, waiting for download links",
|
||||
TorrentStatus.Uploading => $"{prefix} uploading",
|
||||
_ => "Unknown status"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ public class AuthSettingRequirement : IAuthorizationRequirement
|
|||
{
|
||||
}
|
||||
|
||||
public class AuthSettingHandler(ISettings settings) : AuthorizationHandler<AuthSettingRequirement>
|
||||
public class AuthSettingHandler : AuthorizationHandler<AuthSettingRequirement>
|
||||
{
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthSettingRequirement requirement)
|
||||
{
|
||||
|
|
@ -17,7 +17,7 @@ public class AuthSettingHandler(ISettings settings) : AuthorizationHandler<AuthS
|
|||
context.Succeed(requirement);
|
||||
}
|
||||
|
||||
if (settings.Current.General.AuthenticationType == AuthenticationType.None)
|
||||
if (Settings.Get.General.AuthenticationType == AuthenticationType.None)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public class SabnzbdRequirement : IAuthorizationRequirement
|
|||
{
|
||||
}
|
||||
|
||||
public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor httpContextAccessor, ISettings settings) : AuthorizationHandler<SabnzbdRequirement>
|
||||
public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor httpContextAccessor) : AuthorizationHandler<SabnzbdRequirement>
|
||||
{
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, SabnzbdRequirement requirement)
|
||||
{
|
||||
|
|
@ -22,7 +22,7 @@ public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor
|
|||
return;
|
||||
}
|
||||
|
||||
if (settings.Current.General.AuthenticationType == AuthenticationType.None)
|
||||
if (Settings.Get.General.AuthenticationType == AuthenticationType.None)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
|
||||
|
|
@ -65,36 +65,6 @@ public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor
|
|||
return;
|
||||
}
|
||||
|
||||
var apiKey = GetParam("apikey");
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
var separatorIndex = apiKey.IndexOf(':');
|
||||
|
||||
if (separatorIndex <= 0 || separatorIndex == apiKey.Length - 1)
|
||||
{
|
||||
context.Fail();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var username = apiKey[..separatorIndex];
|
||||
var password = apiKey[(separatorIndex + 1)..];
|
||||
|
||||
var loginResult = await authentication.Login(username, password);
|
||||
|
||||
if (loginResult.Succeeded)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
context.Fail();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Authentication required but missing credentials
|
||||
context.Fail();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@
|
|||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="AllDebrid.NET" Version="1.0.18" />
|
||||
<PackageReference Include="Aria2.NET" Version="1.0.6" />
|
||||
<PackageReference Include="DebridLinkFr.NET" Version="2.0.0" />
|
||||
<PackageReference Include="Downloader" Version="5.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.6.0" />
|
||||
<PackageReference Include="DebridLinkFr.NET" Version="1.0.4" />
|
||||
<PackageReference Include="Downloader" Version="5.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.4.0" />
|
||||
<PackageReference Include="MonoTorrent" Version="3.0.2" />
|
||||
<PackageReference Include="Polly" Version="8.6.6" />
|
||||
<PackageReference Include="Premiumize.NET" Version="1.0.10" />
|
||||
|
|
@ -24,9 +24,9 @@
|
|||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="SharpCompress" Version="[0.42.1]" />
|
||||
<PackageReference Include="Synology.Api.Client" Version="[0.3.93]" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.1" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.1" />
|
||||
<PackageReference Include="TorBox.NET" Version="2.1.0" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.1.0" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" />
|
||||
<PackageReference Include="TorBox.NET" Version="1.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ public interface IAllDebridNetClientFactory
|
|||
public IAllDebridNETClient GetClient();
|
||||
}
|
||||
|
||||
public class AllDebridNetClientFactory(ILogger<AllDebridNetClientFactory> logger, IHttpClientFactory httpClientFactory, ISettings settings) : IAllDebridNetClientFactory
|
||||
public class AllDebridNetClientFactory(ILogger<AllDebridNetClientFactory> logger, IHttpClientFactory httpClientFactory) : IAllDebridNetClientFactory
|
||||
{
|
||||
public IAllDebridNETClient GetClient()
|
||||
{
|
||||
try
|
||||
{
|
||||
var apiKey = settings.Current.Provider.ApiKey;
|
||||
var apiKey = Settings.Get.Provider.ApiKey;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using Torrent = DebridLinkFrNET.Models.Torrent;
|
|||
|
||||
namespace RdtClient.Service.Services.DebridClients;
|
||||
|
||||
public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings) : IDebridClient
|
||||
public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
|
||||
{
|
||||
public async Task<IList<DebridClientTorrent>> GetDownloads()
|
||||
{
|
||||
|
|
@ -228,7 +228,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
|
|||
{
|
||||
try
|
||||
{
|
||||
var apiKey = settings.Current.Provider.ApiKey;
|
||||
var apiKey = Settings.Get.Provider.ApiKey;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
|
|
@ -236,7 +236,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
|
|||
}
|
||||
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(settings.Current.Provider.Timeout);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
|
||||
|
||||
var debridLinkClient = new DebridLinkFrNETClient(apiKey, httpClient);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using PremiumizeNET;
|
||||
|
|
@ -12,11 +11,8 @@ using Torrent = RdtClient.Data.Models.Data.Torrent;
|
|||
|
||||
namespace RdtClient.Service.Services.DebridClients;
|
||||
|
||||
public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings)
|
||||
: IDebridClient
|
||||
public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
|
||||
{
|
||||
private const String TransferCreateUrl = "https://www.premiumize.me/api/transfer/create";
|
||||
|
||||
public async Task<IList<DebridClientTorrent>> GetDownloads()
|
||||
{
|
||||
var results = await GetClient().Transfers.ListAsync();
|
||||
|
|
@ -37,22 +33,56 @@ public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHtt
|
|||
|
||||
public async Task<String> AddTorrentMagnet(String magnetLink)
|
||||
{
|
||||
return await CreatePremiumizeNetTransfer(() => GetClient().Transfers.CreateAsync(magnetLink, ""), "magnet link");
|
||||
try
|
||||
{
|
||||
var result = await GetClient().Transfers.CreateAsync(magnetLink, "");
|
||||
|
||||
if (result?.Id == null)
|
||||
{
|
||||
throw new("Unable to add magnet link");
|
||||
}
|
||||
|
||||
var resultId = result.Id ?? throw new($"Invalid responseID {result.Id}");
|
||||
|
||||
return resultId;
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
|
||||
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<String> AddTorrentFile(Byte[] bytes)
|
||||
{
|
||||
return await CreatePremiumizeNetTransfer(() => GetClient().Transfers.CreateAsync(bytes, ""), "torrent file");
|
||||
try
|
||||
{
|
||||
var result = await GetClient().Transfers.CreateAsync(bytes, "");
|
||||
|
||||
if (result?.Id == null)
|
||||
{
|
||||
throw new("Unable to add torrent file");
|
||||
}
|
||||
|
||||
var resultId = result.Id ?? throw new($"Invalid responseID {result.Id}");
|
||||
|
||||
return resultId;
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
|
||||
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<String> AddNzbLink(String nzbLink)
|
||||
public Task<String> AddNzbLink(String nzbLink)
|
||||
{
|
||||
return await CreatePremiumizeNetTransfer(() => GetClient().Transfers.CreateAsync(nzbLink, ""), "NZB link");
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public async Task<String> AddNzbFile(Byte[] bytes, String? name)
|
||||
public Task<String> AddNzbFile(Byte[] bytes, String? name)
|
||||
{
|
||||
return await CreateTransferFromNzbFile(bytes, GetNzbFileName(name));
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash)
|
||||
|
|
@ -195,7 +225,13 @@ public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHtt
|
|||
{
|
||||
try
|
||||
{
|
||||
var apiKey = GetApiKey();
|
||||
var apiKey = Settings.Get.Provider.ApiKey;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
throw new("Premiumize API Key not set in the settings");
|
||||
}
|
||||
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
|
|
@ -217,146 +253,6 @@ public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHtt
|
|||
}
|
||||
}
|
||||
|
||||
private String GetApiKey()
|
||||
{
|
||||
var apiKey = settings.Current.Provider.ApiKey;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
throw new("Premiumize API Key not set in the settings");
|
||||
}
|
||||
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
private async Task<String> CreatePremiumizeNetTransfer(Func<Task<TransferCreateResponse>> createTransfer, String description)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await createTransfer();
|
||||
|
||||
if (String.IsNullOrWhiteSpace(result?.Id))
|
||||
{
|
||||
throw new($"Unable to add {description}");
|
||||
}
|
||||
|
||||
return result.Id;
|
||||
}
|
||||
catch (Exception ex) when (IsRateLimitMessage(ex.Message))
|
||||
{
|
||||
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<String> CreateTransferFromNzbFile(Byte[] bytes, String fileName)
|
||||
{
|
||||
var content = new MultipartFormDataContent();
|
||||
var fileContent = new ByteArrayContent(bytes);
|
||||
content.Add(fileContent, "src", fileName);
|
||||
|
||||
return await CreateTransfer(content, "NZB file");
|
||||
}
|
||||
|
||||
private static String GetNzbFileName(String? name)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return "upload.nzb";
|
||||
}
|
||||
|
||||
return name.EndsWith(".nzb", StringComparison.OrdinalIgnoreCase) ? name : $"{name}.nzb";
|
||||
}
|
||||
|
||||
private async Task<String> CreateTransfer(HttpContent content, String description)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (content)
|
||||
{
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, TransferCreateUrl)
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
|
||||
request.Headers.Authorization = new("Bearer", GetApiKey());
|
||||
|
||||
using var response = await httpClient.SendAsync(request);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
var retryAfter = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromMinutes(2);
|
||||
|
||||
throw new RateLimitException($"Unable to add {description}: Premiumize rate limit exceeded", retryAfter);
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new($"Unable to add {description}: Premiumize returned {(Int32)response.StatusCode} {response.ReasonPhrase}. {responseBody}");
|
||||
}
|
||||
|
||||
var result = JsonConvert.DeserializeObject<RawTransferCreateResponse>(responseBody) ?? throw new($"Unable to add {description}: invalid Premiumize response");
|
||||
|
||||
if (!String.Equals(result.Status, "success", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var error = FormatPremiumizeError(result);
|
||||
|
||||
if (IsRateLimitMessage(error))
|
||||
{
|
||||
throw new RateLimitException(error, TimeSpan.FromMinutes(2));
|
||||
}
|
||||
|
||||
throw new($"Unable to add {description}: {error}");
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(result.Id))
|
||||
{
|
||||
throw new($"Unable to add {description}: Premiumize did not return a transfer id");
|
||||
}
|
||||
|
||||
return result.Id;
|
||||
}
|
||||
}
|
||||
catch (RateLimitException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (IsRateLimitMessage(ex.Message))
|
||||
{
|
||||
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
|
||||
}
|
||||
}
|
||||
|
||||
private static String FormatPremiumizeError(RawTransferCreateResponse result)
|
||||
{
|
||||
return String.Join(": ",
|
||||
new[]
|
||||
{
|
||||
result.Code, result.Message
|
||||
}.Where(m => !String.IsNullOrWhiteSpace(m)));
|
||||
}
|
||||
|
||||
private static Boolean IsRateLimitMessage(String message)
|
||||
{
|
||||
return message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("rate_limit_reached", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("account_limit_reached", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("service_limit_reached", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("service_down", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("semi_permanent_error", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("too many API requests", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("fair-use points", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("booster points", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("active-job count", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("usage limit for this service", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("target service is unreachable", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("retry after a delay", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static DebridClientTorrent Map(Transfer transfer)
|
||||
{
|
||||
return new()
|
||||
|
|
@ -467,19 +363,4 @@ public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHtt
|
|||
|
||||
logger.LogDebug(message);
|
||||
}
|
||||
|
||||
private class RawTransferCreateResponse
|
||||
{
|
||||
[JsonProperty("status")]
|
||||
public String? Status { get; set; }
|
||||
|
||||
[JsonProperty("id")]
|
||||
public String? Id { get; set; }
|
||||
|
||||
[JsonProperty("message")]
|
||||
public String? Message { get; set; }
|
||||
|
||||
[JsonProperty("code")]
|
||||
public String? Code { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ using Torrent = RDNET.Torrent;
|
|||
|
||||
namespace RdtClient.Service.Services.DebridClients;
|
||||
|
||||
public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings)
|
||||
: IDebridClient
|
||||
public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
|
||||
{
|
||||
private TimeSpan? _offset;
|
||||
|
||||
|
|
@ -54,7 +53,7 @@ public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHtt
|
|||
{
|
||||
try
|
||||
{
|
||||
var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(settings.Current.Provider.Timeout));
|
||||
var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout));
|
||||
|
||||
var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, timeoutCancellationToken.Token);
|
||||
|
||||
|
|
@ -71,7 +70,7 @@ public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHtt
|
|||
{
|
||||
try
|
||||
{
|
||||
var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(settings.Current.Provider.Timeout));
|
||||
var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout));
|
||||
|
||||
var result = await GetClient().Torrents.AddFileAsync(bytes, timeoutCancellationToken.Token);
|
||||
|
||||
|
|
@ -315,7 +314,7 @@ public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHtt
|
|||
{
|
||||
try
|
||||
{
|
||||
var apiKey = settings.Current.Provider.ApiKey;
|
||||
var apiKey = Settings.Get.Provider.ApiKey;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
|
|
@ -323,9 +322,9 @@ public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHtt
|
|||
}
|
||||
|
||||
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(settings.Current.Provider.Timeout);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
|
||||
|
||||
var rdtNetClient = new RdNetClient(null, httpClient, 5, settings.Current.Provider.ApiHostname);
|
||||
var rdtNetClient = new RdNetClient(null, httpClient, 5, Settings.Get.Provider.ApiHostname);
|
||||
rdtNetClient.UseApiAuthentication(apiKey);
|
||||
|
||||
// Get the server time to fix up the timezones on results
|
||||
|
|
|
|||
|
|
@ -11,12 +11,7 @@ using Torrent = RdtClient.Data.Models.Data.Torrent;
|
|||
|
||||
namespace RdtClient.Service.Services.DebridClients;
|
||||
|
||||
public class TorBoxDebridClient(
|
||||
ILogger<TorBoxDebridClient> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IDownloadableFileFilter fileFilter,
|
||||
IRateLimitCoordinator coordinator,
|
||||
ISettings settings)
|
||||
public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, IRateLimitCoordinator coordinator)
|
||||
: IDebridClient
|
||||
{
|
||||
private const String TorBoxApiHost = "api.torbox.app";
|
||||
|
|
@ -74,14 +69,9 @@ public class TorBoxDebridClient(
|
|||
return await HandleAddTorrentErrors(async asQueued =>
|
||||
{
|
||||
var user = await GetClient().User.GetAsync(true);
|
||||
var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddMagnetAsync(magnetLink, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued);
|
||||
|
||||
var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW)
|
||||
.Torrents.AddMagnetAsync(magnetLink,
|
||||
user.Data?.Settings?.SeedTorrents ?? 3,
|
||||
allowZip: Settings.Get.Provider.PreferZippedDownloads,
|
||||
as_queued: asQueued);
|
||||
|
||||
return result.Data?.TorrentId?.ToString() ?? throw new InvalidOperationException("TorBox API did not return torrent ID.");
|
||||
return result.Data!.Hash!;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -90,14 +80,9 @@ public class TorBoxDebridClient(
|
|||
return await HandleAddTorrentErrors(async asQueued =>
|
||||
{
|
||||
var user = await GetClient().User.GetAsync(true);
|
||||
var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddFileAsync(bytes, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued);
|
||||
|
||||
var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW)
|
||||
.Torrents.AddFileAsync(bytes,
|
||||
user.Data?.Settings?.SeedTorrents ?? 3,
|
||||
allowZip: Settings.Get.Provider.PreferZippedDownloads,
|
||||
as_queued: asQueued);
|
||||
|
||||
return result.Data?.TorrentId?.ToString() ?? throw new InvalidOperationException("TorBox API did not return torrent ID");
|
||||
return result.Data!.Hash!;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +156,7 @@ public class TorBoxDebridClient(
|
|||
}
|
||||
else
|
||||
{
|
||||
await GetClient().Torrents.ControlByIdAsync(Int32.Parse(torrent.RdId), "delete");
|
||||
await GetClient().Torrents.ControlAsync(torrent.RdId, "delete");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -331,12 +316,8 @@ public class TorBoxDebridClient(
|
|||
}
|
||||
else
|
||||
{
|
||||
if (torrent.RdId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
id = Int32.Parse(torrent.RdId);
|
||||
var torrentId = await HandleErrors(() => GetClient().Torrents.GetHashInfoAsync(torrent.Hash, true));
|
||||
id = torrentId?.Id;
|
||||
}
|
||||
|
||||
if (id == null)
|
||||
|
|
@ -346,7 +327,7 @@ public class TorBoxDebridClient(
|
|||
|
||||
var downloadableFiles = torrent.Files.Where(file => fileFilter.IsDownloadable(torrent, file.Path, file.Bytes)).ToList();
|
||||
|
||||
if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && settings.Current.Provider.PreferZippedDownloads)
|
||||
if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && Settings.Get.Provider.PreferZippedDownloads)
|
||||
{
|
||||
logger.LogDebug("Downloading files from TorBox as a zip.");
|
||||
|
||||
|
|
@ -383,7 +364,7 @@ public class TorBoxDebridClient(
|
|||
{
|
||||
try
|
||||
{
|
||||
var apiKey = settings.Current.Provider.ApiKey;
|
||||
var apiKey = Settings.Get.Provider.ApiKey;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
|
|
@ -391,7 +372,7 @@ public class TorBoxDebridClient(
|
|||
}
|
||||
|
||||
var httpClient = httpClientFactory.CreateClient(clientId);
|
||||
var torBoxNetClient = new TorBoxNetClient(null, httpClient, retryCount: 5);
|
||||
var torBoxNetClient = new TorBoxNetClient(null, httpClient);
|
||||
torBoxNetClient.UseApiAuthentication(apiKey);
|
||||
|
||||
// Get the server time to fix up the timezones on results
|
||||
|
|
@ -460,7 +441,7 @@ public class TorBoxDebridClient(
|
|||
{
|
||||
return new()
|
||||
{
|
||||
Id = torrent.Id.ToString(),
|
||||
Id = torrent.Hash,
|
||||
Filename = torrent.Name,
|
||||
OriginalFilename = torrent.Name,
|
||||
Hash = torrent.Hash,
|
||||
|
|
@ -532,7 +513,7 @@ public class TorBoxDebridClient(
|
|||
{
|
||||
throw rateLimitException;
|
||||
}
|
||||
catch (TorBoxException ex) when (IsRateLimit(ex))
|
||||
catch (TorBoxException ex) when ("active_limit".Equals(ex.Error, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2));
|
||||
|
||||
|
|
@ -560,7 +541,7 @@ public class TorBoxDebridClient(
|
|||
{
|
||||
throw rateLimitException;
|
||||
}
|
||||
catch (TorBoxException ex) when (IsRateLimit(ex))
|
||||
catch (TorBoxException ex) when ("active_limit".Equals(ex.Error, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2));
|
||||
|
||||
|
|
@ -579,12 +560,6 @@ public class TorBoxDebridClient(
|
|||
return await HandleErrors(() => action(false));
|
||||
}
|
||||
|
||||
private static Boolean IsRateLimit(TorBoxException exception)
|
||||
{
|
||||
return exception.Error.Equals("RATE_LIMIT", StringComparison.OrdinalIgnoreCase)
|
||||
|| exception.Error.Equals("ACTIVE_LIMIT", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task<String> HandleAddUsenetErrors(Func<Boolean, Task<String>> action)
|
||||
{
|
||||
return await HandleErrors(() => action(false));
|
||||
|
|
@ -615,7 +590,7 @@ public class TorBoxDebridClient(
|
|||
}
|
||||
else
|
||||
{
|
||||
var result = await GetClient().Torrents.GetIdInfoAsync(Int32.Parse(id), true);
|
||||
var result = await GetClient().Torrents.GetHashInfoAsync(id, true);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
|
|
@ -669,16 +644,10 @@ public class TorBoxDebridClient(
|
|||
{
|
||||
if (!String.IsNullOrWhiteSpace(status))
|
||||
{
|
||||
logger.LogInformation("TorBoxDebridClient encountered an unmapped status: {Status} for torrent {TorrentName} with previous status {PreviousStatus}",
|
||||
status,
|
||||
torrent.RdName,
|
||||
torrent.RdStatus);
|
||||
logger.LogInformation("TorBoxDebridClient encountered an unmapped status: {Status} for torrent {TorrentName}", status, torrent.RdName);
|
||||
}
|
||||
|
||||
// Once TorBox has acknowledged the torrent, an unknown provider-side status should not fall all the way back to
|
||||
// the local queue state. Treat it as provider-side processing so the UI does not incorrectly show
|
||||
// "Not Yet Added to Provider" while TorBox is already handling it.
|
||||
return torrent.RdStatus is null or TorrentStatus.Queued ? TorrentStatus.Processing : torrent.RdStatus.Value;
|
||||
return torrent.RdStatus ?? TorrentStatus.Processing;
|
||||
}
|
||||
|
||||
private void Log(String message, Torrent? torrent = null)
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ public class Downloads(DownloadData downloadData) : IDownloads
|
|||
return await downloadData.Get(torrentId, path);
|
||||
}
|
||||
|
||||
public async Task<DownloadAddResult> TryAddForTorrent(Guid torrentId, DownloadInfo downloadInfo)
|
||||
public async Task<Download> Add(Guid torrentId, DownloadInfo downloadInfo)
|
||||
{
|
||||
return await downloadData.TryAddForTorrent(torrentId, downloadInfo);
|
||||
return await downloadData.Add(torrentId, downloadInfo);
|
||||
}
|
||||
|
||||
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
|
||||
|
|
@ -86,8 +86,8 @@ public class Downloads(DownloadData downloadData) : IDownloads
|
|||
await downloadData.DeleteForTorrent(torrentId);
|
||||
}
|
||||
|
||||
public async Task Reset(Guid downloadId, DateTimeOffset? downloadQueued = null)
|
||||
public async Task Reset(Guid downloadId)
|
||||
{
|
||||
await downloadData.Reset(downloadId, downloadQueued);
|
||||
await downloadData.Reset(downloadId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Models.Data;
|
||||
|
||||
namespace RdtClient.Service.Services;
|
||||
|
|
@ -8,7 +7,7 @@ public interface IDownloads
|
|||
Task<List<Download>> GetForTorrent(Guid torrentId);
|
||||
Task<Download?> GetById(Guid downloadId);
|
||||
Task<Download?> Get(Guid torrentId, String path);
|
||||
Task<DownloadAddResult> TryAddForTorrent(Guid torrentId, DownloadInfo downloadInfo);
|
||||
Task<Download> Add(Guid torrentId, DownloadInfo downloadInfo);
|
||||
Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink);
|
||||
Task UpdateFileName(Guid downloadId, String fileName);
|
||||
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);
|
||||
|
|
@ -21,5 +20,5 @@ public interface IDownloads
|
|||
Task UpdateRetryCount(Guid downloadId, Int32 retryCount);
|
||||
Task UpdateRemoteId(Guid downloadId, String remoteId);
|
||||
Task DeleteForTorrent(Guid torrentId);
|
||||
Task Reset(Guid downloadId, DateTimeOffset? downloadQueued = null);
|
||||
Task Reset(Guid downloadId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using RdtClient.Data.Models.QBittorrent;
|
|||
|
||||
namespace RdtClient.Service.Services;
|
||||
|
||||
public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authentication authentication, Torrents torrents, Downloads downloads, ITorrentRunnerState runnerState)
|
||||
public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authentication authentication, Torrents torrents, Downloads downloads)
|
||||
{
|
||||
public async Task<Boolean> AuthLogin(String userName, String password)
|
||||
{
|
||||
|
|
@ -168,7 +168,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
WebUiUsername = ""
|
||||
};
|
||||
|
||||
var savePath = settings.DefaultSavePath;
|
||||
var savePath = Settings.AppDefaultSavePath;
|
||||
|
||||
preferences.SavePath = savePath;
|
||||
preferences.TempPath = $"{savePath}temp{Path.DirectorySeparatorChar}";
|
||||
|
|
@ -183,9 +183,9 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
return preferences;
|
||||
}
|
||||
|
||||
public virtual async Task<IList<TorrentInfo>> TorrentInfo()
|
||||
public async Task<IList<TorrentInfo>> TorrentInfo()
|
||||
{
|
||||
var savePath = settings.DefaultSavePath;
|
||||
var savePath = Settings.AppDefaultSavePath;
|
||||
|
||||
var results = new List<TorrentInfo>();
|
||||
|
||||
|
|
@ -214,17 +214,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
}
|
||||
else
|
||||
{
|
||||
var existingContentPath = GetExistingContentPath(downloadPath, torrent);
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(existingContentPath))
|
||||
{
|
||||
torrentPath = existingContentPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
var contentPathName = GetContentPathName(torrent);
|
||||
torrentPath = Path.Combine(downloadPath, contentPathName) + Path.DirectorySeparatorChar;
|
||||
}
|
||||
torrentPath = Path.Combine(downloadPath, torrent.RdName) + Path.DirectorySeparatorChar;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -338,164 +328,10 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
return results;
|
||||
}
|
||||
|
||||
private static String GetContentPathName(Torrent torrent)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(torrent.RdName))
|
||||
{
|
||||
return torrent.RdName ?? String.Empty;
|
||||
}
|
||||
|
||||
var topLevelSelectedFiles = torrent.Files
|
||||
.Where(m => m.Selected && !String.IsNullOrWhiteSpace(m.Path))
|
||||
.Select(m => m.Path.Trim('/').Trim('\\'))
|
||||
.Where(m => m.IndexOfAny(['/', '\\']) < 0)
|
||||
.Select(Path.GetFileName)
|
||||
.Where(m => !String.IsNullOrWhiteSpace(m))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (topLevelSelectedFiles.Count == 1)
|
||||
{
|
||||
var selectedFileName = topLevelSelectedFiles[0]!;
|
||||
var selectedFileBaseName = Path.GetFileNameWithoutExtension(selectedFileName);
|
||||
|
||||
if (torrent.ClientKind == Provider.TorBox)
|
||||
{
|
||||
return selectedFileBaseName;
|
||||
}
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(selectedFileBaseName) &&
|
||||
selectedFileBaseName.Equals(torrent.RdName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return selectedFileName;
|
||||
}
|
||||
}
|
||||
|
||||
return torrent.RdName;
|
||||
}
|
||||
|
||||
private static String? GetExistingContentPath(String downloadPath, Torrent torrent)
|
||||
{
|
||||
if (!torrent.Completed.HasValue || !Directory.Exists(downloadPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var selectedFilePaths = torrent.Files
|
||||
.Where(m => m.Selected && !String.IsNullOrWhiteSpace(m.Path))
|
||||
.Select(m => NormalizeRelativePath(m.Path!))
|
||||
.Where(m => !String.IsNullOrWhiteSpace(m))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var downloadFileNames = torrent.Downloads
|
||||
.Select(m => m.FileName)
|
||||
.Where(m => !String.IsNullOrWhiteSpace(m))
|
||||
.Select(m => Path.GetFileName(m!))
|
||||
.Where(m => !String.IsNullOrWhiteSpace(m))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (selectedFilePaths.Count == 0 && downloadFileNames.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var candidateRoot in GetCandidateContentRoots(downloadPath, torrent, selectedFilePaths, downloadFileNames))
|
||||
{
|
||||
if (IsMatchingContentRoot(candidateRoot, selectedFilePaths, downloadFileNames))
|
||||
{
|
||||
return candidateRoot + Path.DirectorySeparatorChar;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<String> GetCandidateContentRoots(String downloadPath,
|
||||
Torrent torrent,
|
||||
IEnumerable<String> selectedFilePaths,
|
||||
IEnumerable<String> downloadFileNames)
|
||||
{
|
||||
var yielded = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
void AddCandidate(ICollection<String> candidates, String? name)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
candidates.Add(Path.Combine(downloadPath, name));
|
||||
}
|
||||
}
|
||||
|
||||
var directCandidates = new List<String>();
|
||||
|
||||
AddCandidate(directCandidates, torrent.RdName);
|
||||
|
||||
foreach (var fileName in downloadFileNames)
|
||||
{
|
||||
AddCandidate(directCandidates, fileName);
|
||||
}
|
||||
|
||||
foreach (var selectedFilePath in selectedFilePaths)
|
||||
{
|
||||
AddCandidate(directCandidates, Path.GetFileName(selectedFilePath));
|
||||
AddCandidate(directCandidates, GetFirstPathComponent(selectedFilePath));
|
||||
}
|
||||
|
||||
foreach (var candidate in directCandidates)
|
||||
{
|
||||
if (Directory.Exists(candidate) && yielded.Add(candidate))
|
||||
{
|
||||
yield return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var directory in Directory.EnumerateDirectories(downloadPath))
|
||||
{
|
||||
if (yielded.Add(directory))
|
||||
{
|
||||
yield return directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Boolean IsMatchingContentRoot(String candidateRoot,
|
||||
IEnumerable<String> selectedFilePaths,
|
||||
IEnumerable<String> downloadFileNames)
|
||||
{
|
||||
foreach (var selectedFilePath in selectedFilePaths)
|
||||
{
|
||||
if (File.Exists(Path.Combine(candidateRoot, selectedFilePath)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var fileName in downloadFileNames)
|
||||
{
|
||||
if (File.Exists(Path.Combine(candidateRoot, fileName)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String NormalizeRelativePath(String path)
|
||||
{
|
||||
return path.Trim('/').Trim('\\').Replace('\\', Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
private static String GetFirstPathComponent(String path)
|
||||
{
|
||||
var separatorIndex = path.IndexOfAny(['/', '\\']);
|
||||
|
||||
return separatorIndex < 0 ? path : path[..separatorIndex];
|
||||
}
|
||||
|
||||
public async Task<IList<TorrentFileItem>?> TorrentFileContents(String hash)
|
||||
{
|
||||
var results = new List<TorrentFileItem>();
|
||||
|
||||
var torrent = await torrents.GetByHash(hash);
|
||||
|
||||
if (torrent == null || torrent.Type != DownloadType.Torrent)
|
||||
|
|
@ -503,24 +339,22 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
return null;
|
||||
}
|
||||
|
||||
var progress = torrent.Completed.HasValue || torrent.RdStatus == TorrentStatus.Finished ? 1f : 0f;
|
||||
foreach (var file in torrent.Files.Where(m => m.Selected))
|
||||
{
|
||||
var result = new TorrentFileItem
|
||||
{
|
||||
Name = file.Path
|
||||
};
|
||||
|
||||
return torrent.Files
|
||||
.Select((file, index) => new TorrentFileItem
|
||||
{
|
||||
Index = index,
|
||||
Name = file.Path,
|
||||
Size = file.Bytes,
|
||||
Progress = file.Selected ? progress : 0f,
|
||||
Priority = file.Selected ? 1 : 0,
|
||||
IsSeed = false
|
||||
})
|
||||
.ToList();
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<TorrentProperties?> TorrentProperties(String hash)
|
||||
{
|
||||
var savePath = settings.DefaultSavePath;
|
||||
var savePath = Settings.AppDefaultSavePath;
|
||||
|
||||
var torrent = await torrents.GetByHash(hash);
|
||||
|
||||
|
|
@ -551,7 +385,6 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
AdditionDate = torrent.Added.ToUnixTimeSeconds(),
|
||||
Comment = "RealDebridClient <https://github.com/rogerfar/rdt-client>",
|
||||
CompletionDate = torrent.Completed?.ToUnixTimeSeconds() ?? -1,
|
||||
IsPrivate = false,
|
||||
CreatedBy = "RealDebridClient <https://github.com/rogerfar/rdt-client>",
|
||||
CreationDate = torrent.Added.ToUnixTimeSeconds(),
|
||||
DlLimit = -1,
|
||||
|
|
@ -587,11 +420,6 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
return result;
|
||||
}
|
||||
|
||||
public async Task TorrentsFilePrio(String hash, IReadOnlyCollection<Int32> fileIds, Int32 priority)
|
||||
{
|
||||
await torrents.UpdateFileSelection(hash, fileIds, priority > 0);
|
||||
}
|
||||
|
||||
public async Task TorrentsDelete(String hash, Boolean deleteFiles)
|
||||
{
|
||||
if (deleteFiles)
|
||||
|
|
@ -610,7 +438,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
return;
|
||||
}
|
||||
|
||||
switch (settings.Current.Integrations.Default.FinishedAction)
|
||||
switch (Settings.Get.Integrations.Default.FinishedAction)
|
||||
{
|
||||
case TorrentFinishedAction.RemoveAllTorrents:
|
||||
logger.LogDebug("Removing torrents from debrid provider and RDT-Client, no files");
|
||||
|
|
@ -638,54 +466,54 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<Torrent> TorrentsAddMagnet(String magnetLink, String? category, Int32? priority)
|
||||
public async Task TorrentsAddMagnet(String magnetLink, String? category, Int32? priority)
|
||||
{
|
||||
logger.LogDebug($"Add magnet {category}");
|
||||
|
||||
var torrent = new Torrent
|
||||
{
|
||||
Category = category,
|
||||
DownloadClient = settings.Current.DownloadClient.Client,
|
||||
HostDownloadAction = settings.Current.Integrations.Default.HostDownloadAction,
|
||||
FinishedActionDelay = settings.Current.Integrations.Default.FinishedActionDelay,
|
||||
DownloadAction = settings.Current.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
DownloadClient = Settings.Get.DownloadClient.Client,
|
||||
HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction,
|
||||
FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay,
|
||||
DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
FinishedAction = TorrentFinishedAction.None,
|
||||
DownloadMinSize = settings.Current.Integrations.Default.MinFileSize,
|
||||
IncludeRegex = settings.Current.Integrations.Default.IncludeRegex,
|
||||
ExcludeRegex = settings.Current.Integrations.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = settings.Current.Integrations.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = settings.Current.Integrations.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = settings.Current.Integrations.Default.DeleteOnError,
|
||||
Lifetime = settings.Current.Integrations.Default.TorrentLifetime,
|
||||
Priority = priority ?? (settings.Current.Integrations.Default.Priority > 0 ? settings.Current.Integrations.Default.Priority : null)
|
||||
DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize,
|
||||
IncludeRegex = Settings.Get.Integrations.Default.IncludeRegex,
|
||||
ExcludeRegex = Settings.Get.Integrations.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError,
|
||||
Lifetime = Settings.Get.Integrations.Default.TorrentLifetime,
|
||||
Priority = priority ?? (Settings.Get.Integrations.Default.Priority > 0 ? Settings.Get.Integrations.Default.Priority : null)
|
||||
};
|
||||
|
||||
return await torrents.AddMagnetToDebridQueue(magnetLink, torrent);
|
||||
await torrents.AddMagnetToDebridQueue(magnetLink, torrent);
|
||||
}
|
||||
|
||||
public async Task<Torrent> TorrentsAddFile(Byte[] fileBytes, String? category, Int32? priority)
|
||||
public async Task TorrentsAddFile(Byte[] fileBytes, String? category, Int32? priority)
|
||||
{
|
||||
logger.LogDebug($"Add file {category}");
|
||||
|
||||
var torrent = new Torrent
|
||||
{
|
||||
Category = category,
|
||||
DownloadClient = settings.Current.DownloadClient.Client,
|
||||
HostDownloadAction = settings.Current.Integrations.Default.HostDownloadAction,
|
||||
FinishedActionDelay = settings.Current.Integrations.Default.FinishedActionDelay,
|
||||
DownloadAction = settings.Current.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
DownloadClient = Settings.Get.DownloadClient.Client,
|
||||
HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction,
|
||||
FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay,
|
||||
DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
FinishedAction = TorrentFinishedAction.None,
|
||||
DownloadMinSize = settings.Current.Integrations.Default.MinFileSize,
|
||||
IncludeRegex = settings.Current.Integrations.Default.IncludeRegex,
|
||||
ExcludeRegex = settings.Current.Integrations.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = settings.Current.Integrations.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = settings.Current.Integrations.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = settings.Current.Integrations.Default.DeleteOnError,
|
||||
Lifetime = settings.Current.Integrations.Default.TorrentLifetime,
|
||||
Priority = priority ?? (settings.Current.Integrations.Default.Priority > 0 ? settings.Current.Integrations.Default.Priority : null)
|
||||
DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize,
|
||||
IncludeRegex = Settings.Get.Integrations.Default.IncludeRegex,
|
||||
ExcludeRegex = Settings.Get.Integrations.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError,
|
||||
Lifetime = Settings.Get.Integrations.Default.TorrentLifetime,
|
||||
Priority = priority ?? (Settings.Get.Integrations.Default.Priority > 0 ? Settings.Get.Integrations.Default.Priority : null)
|
||||
};
|
||||
|
||||
return await torrents.AddFileToDebridQueue(fileBytes, torrent);
|
||||
await torrents.AddFileToDebridQueue(fileBytes, torrent);
|
||||
}
|
||||
|
||||
public async Task TorrentsSetCategory(String hash, String? category)
|
||||
|
|
@ -701,7 +529,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
.Select(m => m.Category!.ToLower())
|
||||
.ToList();
|
||||
|
||||
var categoryList = (settings.Current.General.Categories ?? "")
|
||||
var categoryList = (Settings.Get.General.Categories ?? "")
|
||||
.Split(",", StringSplitOptions.RemoveEmptyEntries)
|
||||
.Distinct(StringComparer.CurrentCultureIgnoreCase)
|
||||
.Select(m => m.Trim())
|
||||
|
|
@ -718,7 +546,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
m => new TorrentCategory
|
||||
{
|
||||
Name = m,
|
||||
SavePath = Path.Combine(settings.DefaultSavePath, m)
|
||||
SavePath = Path.Combine(Settings.AppDefaultSavePath, m)
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -734,7 +562,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
|
||||
category = category.Trim();
|
||||
|
||||
var categoriesSetting = settings.Current.General.Categories;
|
||||
var categoriesSetting = Settings.Get.General.Categories;
|
||||
|
||||
var categoryList = (categoriesSetting ?? "")
|
||||
.Split(",", StringSplitOptions.RemoveEmptyEntries)
|
||||
|
|
@ -761,7 +589,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
|
||||
category = category.Trim();
|
||||
|
||||
var categoriesSetting = settings.Current.General.Categories;
|
||||
var categoriesSetting = Settings.Get.General.Categories;
|
||||
|
||||
var categoryList = (categoriesSetting ?? "")
|
||||
.Split(",", StringSplitOptions.RemoveEmptyEntries)
|
||||
|
|
@ -801,7 +629,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
|
||||
foreach (var download in downloadsForTorrent)
|
||||
{
|
||||
if (runnerState.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
|
||||
if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
|
||||
{
|
||||
await downloadClient.Pause();
|
||||
}
|
||||
|
|
@ -821,7 +649,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
|
||||
foreach (var download in downloadsForTorrent)
|
||||
{
|
||||
if (runnerState.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
|
||||
if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
|
||||
{
|
||||
await downloadClient.Resume();
|
||||
}
|
||||
|
|
@ -834,7 +662,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
|
||||
var categories = await TorrentsCategories();
|
||||
|
||||
var activeDownloads = runnerState.ActiveDownloadClients.Sum(m => m.Value.Speed);
|
||||
var activeDownloads = TorrentRunner.ActiveDownloadClients.Sum(m => m.Value.Speed);
|
||||
|
||||
return new()
|
||||
{
|
||||
|
|
@ -852,16 +680,16 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
|
|||
};
|
||||
}
|
||||
|
||||
public virtual TransferInfo TransferInfo()
|
||||
public static TransferInfo TransferInfo()
|
||||
{
|
||||
var activeDownloads = runnerState.ActiveDownloadClients.Sum(m => m.Value.Speed);
|
||||
var activeDownloads = TorrentRunner.ActiveDownloadClients.Sum(m => m.Value.Speed);
|
||||
|
||||
return new()
|
||||
{
|
||||
ConnectionStatus = "connected",
|
||||
DlInfoData = DownloadClient.GetTotalBytesDownloadedThisSession(),
|
||||
DlInfoSpeed = activeDownloads,
|
||||
DlRateLimit = settings.Current.DownloadClient.MaxSpeed
|
||||
DlRateLimit = Settings.Get.DownloadClient.MaxSpeed
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Microsoft.AspNetCore.SignalR;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.Helpers;
|
||||
|
||||
namespace RdtClient.Service.Services;
|
||||
|
||||
|
|
@ -10,7 +9,72 @@ public class RemoteService(IHubContext<RdtHub> hub, Torrents torrents)
|
|||
{
|
||||
var allTorrents = await torrents.Get();
|
||||
|
||||
var torrentDtos = allTorrents.Select(torrent => TorrentDtoMapper.ToUpdateDto(torrent, torrents.GetDownloadStats))
|
||||
var torrentDtos = allTorrents.Select(torrent => new TorrentDto
|
||||
{
|
||||
TorrentId = torrent.TorrentId,
|
||||
Hash = torrent.Hash,
|
||||
Category = torrent.Category,
|
||||
DownloadAction = torrent.DownloadAction,
|
||||
FinishedAction = torrent.FinishedAction,
|
||||
FinishedActionDelay = torrent.FinishedActionDelay,
|
||||
HostDownloadAction = torrent.HostDownloadAction,
|
||||
DownloadMinSize = torrent.DownloadMinSize,
|
||||
IncludeRegex = torrent.IncludeRegex,
|
||||
ExcludeRegex = torrent.ExcludeRegex,
|
||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||
DownloadClient = torrent.DownloadClient,
|
||||
Added = torrent.Added,
|
||||
FilesSelected = torrent.FilesSelected,
|
||||
Completed = torrent.Completed,
|
||||
Type = torrent.Type,
|
||||
IsFile = torrent.IsFile,
|
||||
Priority = torrent.Priority,
|
||||
RetryCount = torrent.RetryCount,
|
||||
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
|
||||
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||
DeleteOnError = torrent.DeleteOnError,
|
||||
Lifetime = torrent.Lifetime,
|
||||
Error = torrent.Error,
|
||||
RdId = torrent.RdId,
|
||||
RdName = torrent.RdName,
|
||||
RdSize = torrent.RdSize,
|
||||
RdHost = torrent.RdHost,
|
||||
RdSplit = torrent.RdSplit,
|
||||
RdProgress = torrent.RdProgress,
|
||||
RdStatus = torrent.RdStatus,
|
||||
RdStatusRaw = torrent.RdStatusRaw,
|
||||
RdAdded = torrent.RdAdded,
|
||||
RdEnded = torrent.RdEnded,
|
||||
RdSpeed = torrent.RdSpeed,
|
||||
RdSeeders = torrent.RdSeeders,
|
||||
Files = torrent.Files,
|
||||
Downloads = torrent.Downloads.Select(download =>
|
||||
{
|
||||
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
|
||||
|
||||
return new DownloadDto
|
||||
{
|
||||
DownloadId = download.DownloadId,
|
||||
TorrentId = download.TorrentId,
|
||||
Path = download.Path,
|
||||
Link = download.Link,
|
||||
Added = download.Added,
|
||||
DownloadQueued = download.DownloadQueued,
|
||||
DownloadStarted = download.DownloadStarted,
|
||||
DownloadFinished = download.DownloadFinished,
|
||||
UnpackingQueued = download.UnpackingQueued,
|
||||
UnpackingStarted = download.UnpackingStarted,
|
||||
UnpackingFinished = download.UnpackingFinished,
|
||||
Completed = download.Completed,
|
||||
RetryCount = download.RetryCount,
|
||||
Error = download.Error,
|
||||
BytesTotal = bytesTotal,
|
||||
BytesDone = bytesDone,
|
||||
Speed = speed
|
||||
};
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
await hub.Clients.All.SendCoreAsync("update",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using RdtClient.Service.Helpers;
|
|||
|
||||
namespace RdtClient.Service.Services;
|
||||
|
||||
public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings appSettings, ISettings settings)
|
||||
public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings appSettings)
|
||||
{
|
||||
public virtual async Task<SabnzbdQueue> GetQueue()
|
||||
{
|
||||
|
|
@ -87,7 +87,7 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
|
|||
var allTorrents = await torrents.Get();
|
||||
var completedTorrents = allTorrents.Where(t => t.Type == DownloadType.Nzb && t.Completed != null).ToList();
|
||||
|
||||
var savePath = settings.DefaultSavePath;
|
||||
var savePath = Settings.AppDefaultSavePath;
|
||||
|
||||
var history = new SabnzbdHistory
|
||||
{
|
||||
|
|
@ -130,19 +130,19 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
|
|||
var torrent = new Torrent
|
||||
{
|
||||
Category = category,
|
||||
DownloadClient = settings.Current.DownloadClient.Client,
|
||||
HostDownloadAction = settings.Current.Integrations.Default.HostDownloadAction,
|
||||
FinishedActionDelay = settings.Current.Integrations.Default.FinishedActionDelay,
|
||||
DownloadAction = settings.Current.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
DownloadClient = Settings.Get.DownloadClient.Client,
|
||||
HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction,
|
||||
FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay,
|
||||
DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
FinishedAction = TorrentFinishedAction.None,
|
||||
DownloadMinSize = settings.Current.Integrations.Default.MinFileSize,
|
||||
IncludeRegex = settings.Current.Integrations.Default.IncludeRegex,
|
||||
ExcludeRegex = settings.Current.Integrations.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = settings.Current.Integrations.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = settings.Current.Integrations.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = settings.Current.Integrations.Default.DeleteOnError,
|
||||
Lifetime = settings.Current.Integrations.Default.TorrentLifetime,
|
||||
Priority = (priority ?? settings.Current.Integrations.Default.Priority) > 0 ? 1 : null
|
||||
DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize,
|
||||
IncludeRegex = Settings.Get.Integrations.Default.IncludeRegex,
|
||||
ExcludeRegex = Settings.Get.Integrations.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError,
|
||||
Lifetime = Settings.Get.Integrations.Default.TorrentLifetime,
|
||||
Priority = (priority ?? Settings.Get.Integrations.Default.Priority) > 0 ? 1 : null
|
||||
};
|
||||
|
||||
var result = await torrents.AddNzbFileToDebridQueue(fileBytes, fileName, torrent);
|
||||
|
|
@ -157,19 +157,19 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
|
|||
var torrent = new Torrent
|
||||
{
|
||||
Category = category,
|
||||
DownloadClient = settings.Current.DownloadClient.Client,
|
||||
HostDownloadAction = settings.Current.Integrations.Default.HostDownloadAction,
|
||||
FinishedActionDelay = settings.Current.Integrations.Default.FinishedActionDelay,
|
||||
DownloadAction = settings.Current.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
DownloadClient = Settings.Get.DownloadClient.Client,
|
||||
HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction,
|
||||
FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay,
|
||||
DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
FinishedAction = TorrentFinishedAction.None,
|
||||
DownloadMinSize = settings.Current.Integrations.Default.MinFileSize,
|
||||
IncludeRegex = settings.Current.Integrations.Default.IncludeRegex,
|
||||
ExcludeRegex = settings.Current.Integrations.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = settings.Current.Integrations.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = settings.Current.Integrations.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = settings.Current.Integrations.Default.DeleteOnError,
|
||||
Lifetime = settings.Current.Integrations.Default.TorrentLifetime,
|
||||
Priority = priority ?? (settings.Current.Integrations.Default.Priority > 0 ? settings.Current.Integrations.Default.Priority : null)
|
||||
DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize,
|
||||
IncludeRegex = Settings.Get.Integrations.Default.IncludeRegex,
|
||||
ExcludeRegex = Settings.Get.Integrations.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError,
|
||||
Lifetime = Settings.Get.Integrations.Default.TorrentLifetime,
|
||||
Priority = priority ?? (Settings.Get.Integrations.Default.Priority > 0 ? Settings.Get.Integrations.Default.Priority : null)
|
||||
};
|
||||
|
||||
var result = await torrents.AddNzbLinkToDebridQueue(url, torrent);
|
||||
|
|
@ -177,7 +177,7 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
|
|||
return result.Hash;
|
||||
}
|
||||
|
||||
public virtual async Task Delete(String hash, Boolean deleteFiles = false)
|
||||
public virtual async Task Delete(String hash)
|
||||
{
|
||||
var torrent = await torrents.GetByHash(hash);
|
||||
|
||||
|
|
@ -186,21 +186,21 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
|
|||
return;
|
||||
}
|
||||
|
||||
switch (settings.Current.Integrations.Default.FinishedAction)
|
||||
switch (Settings.Get.Integrations.Default.FinishedAction)
|
||||
{
|
||||
case TorrentFinishedAction.RemoveAllTorrents:
|
||||
logger.LogDebug("Removing nzb from debrid provider and RDT-Client, {Files}", deleteFiles ? "with files" : "no files");
|
||||
await torrents.Delete(torrent.TorrentId, true, true, deleteFiles);
|
||||
logger.LogDebug("Removing nzb from debrid provider and RDT-Client, no files");
|
||||
await torrents.Delete(torrent.TorrentId, true, true, true);
|
||||
|
||||
break;
|
||||
case TorrentFinishedAction.RemoveRealDebrid:
|
||||
logger.LogDebug("Removing nzb from debrid provider, {Files}", deleteFiles ? "with files" : "no files");
|
||||
await torrents.Delete(torrent.TorrentId, false, true, deleteFiles);
|
||||
logger.LogDebug("Removing nzb from debrid provider, no files");
|
||||
await torrents.Delete(torrent.TorrentId, false, true, true);
|
||||
|
||||
break;
|
||||
case TorrentFinishedAction.RemoveClient:
|
||||
logger.LogDebug("Removing nzb from client, {Files}", deleteFiles ? "with files" : "no files");
|
||||
await torrents.Delete(torrent.TorrentId, true, false, deleteFiles);
|
||||
logger.LogDebug("Removing nzb from client, no files");
|
||||
await torrents.Delete(torrent.TorrentId, true, false, true);
|
||||
|
||||
break;
|
||||
case TorrentFinishedAction.None:
|
||||
|
|
@ -216,7 +216,7 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
|
|||
|
||||
public virtual List<String> GetCategories()
|
||||
{
|
||||
var categoryList = (settings.Current.General.Categories ?? "")
|
||||
var categoryList = (Settings.Get.General.Categories ?? "")
|
||||
.Split(",", StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(m => m.Trim())
|
||||
.Where(m => m != "*")
|
||||
|
|
@ -230,7 +230,7 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
|
|||
|
||||
public virtual SabnzbdConfig GetConfig()
|
||||
{
|
||||
var savePath = settings.DefaultSavePath;
|
||||
var savePath = Settings.AppDefaultSavePath;
|
||||
|
||||
var categoryList = GetCategories();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,88 +1,49 @@
|
|||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace RdtClient.Service.Services;
|
||||
|
||||
public interface ISettings
|
||||
{
|
||||
DbSettings Current { get; }
|
||||
|
||||
String DefaultSavePath { get; }
|
||||
|
||||
IList<SettingProperty> GetAll();
|
||||
|
||||
Task Update(IList<SettingProperty> settings);
|
||||
|
||||
Task Update(String settingId, Object? value);
|
||||
}
|
||||
|
||||
public class Settings(IServiceScopeFactory serviceScopeFactory) : ISettings
|
||||
public class Settings(SettingData settingData)
|
||||
{
|
||||
public static readonly LoggingLevelSwitch LoggingLevelSwitch = new(LogEventLevel.Debug);
|
||||
|
||||
public static DbSettings Get => SettingData.Get;
|
||||
|
||||
public DbSettings Current => Get;
|
||||
|
||||
public static String AppDefaultSavePath => GetDefaultSavePath(Get);
|
||||
|
||||
public String DefaultSavePath => GetDefaultSavePath(Current);
|
||||
|
||||
public IList<SettingProperty> GetAll()
|
||||
public static String AppDefaultSavePath
|
||||
{
|
||||
return SettingData.GetAll();
|
||||
}
|
||||
|
||||
private static String GetDefaultSavePath(DbSettings settings)
|
||||
{
|
||||
var downloadPath = settings.DownloadClient.MappedPath;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(downloadPath))
|
||||
get
|
||||
{
|
||||
downloadPath = settings.DownloadClient.DownloadPath;
|
||||
var downloadPath = Get.DownloadClient.MappedPath;
|
||||
|
||||
downloadPath = downloadPath.TrimEnd('\\')
|
||||
.TrimEnd('/');
|
||||
|
||||
downloadPath += Path.DirectorySeparatorChar;
|
||||
|
||||
return downloadPath;
|
||||
}
|
||||
|
||||
downloadPath = downloadPath.TrimEnd('\\')
|
||||
.TrimEnd('/');
|
||||
|
||||
downloadPath += Path.DirectorySeparatorChar;
|
||||
|
||||
return downloadPath;
|
||||
}
|
||||
|
||||
public async Task Update(IList<SettingProperty> settings)
|
||||
{
|
||||
using var scope = serviceScopeFactory.CreateScope();
|
||||
var settingData = scope.ServiceProvider.GetRequiredService<SettingData>();
|
||||
|
||||
await settingData.Update(settings);
|
||||
}
|
||||
|
||||
public async Task Update(String settingId, Object? value)
|
||||
{
|
||||
using var scope = serviceScopeFactory.CreateScope();
|
||||
var settingData = scope.ServiceProvider.GetRequiredService<SettingData>();
|
||||
|
||||
await settingData.Update(settingId, value);
|
||||
}
|
||||
|
||||
public async Task Seed()
|
||||
{
|
||||
using var scope = serviceScopeFactory.CreateScope();
|
||||
var settingData = scope.ServiceProvider.GetRequiredService<SettingData>();
|
||||
|
||||
await settingData.Seed();
|
||||
}
|
||||
|
||||
public async Task ResetCache()
|
||||
{
|
||||
using var scope = serviceScopeFactory.CreateScope();
|
||||
var settingData = scope.ServiceProvider.GetRequiredService<SettingData>();
|
||||
|
||||
await settingData.ResetCache();
|
||||
|
||||
LoggingLevelSwitch.MinimumLevel = Get.General.LogLevel switch
|
||||
|
|
|
|||
|
|
@ -17,34 +17,34 @@ public class TorrentRunner(
|
|||
Downloads downloads,
|
||||
RemoteService remoteService,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IRateLimitCoordinator coordinator,
|
||||
ISettings settings,
|
||||
ITorrentRunnerState runnerState)
|
||||
IRateLimitCoordinator coordinator)
|
||||
{
|
||||
public static readonly ITorrentRunnerState SharedState = new TorrentRunnerState();
|
||||
|
||||
public static ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients => SharedState.ActiveDownloadClients;
|
||||
|
||||
public static ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients => SharedState.ActiveUnpackClients;
|
||||
|
||||
public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new();
|
||||
public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new();
|
||||
private DateTimeOffset? _lastNextAllowedAt;
|
||||
|
||||
public static Boolean IsPausedForLowDiskSpace
|
||||
{
|
||||
get => SharedState.IsPausedForLowDiskSpace;
|
||||
set => SharedState.IsPausedForLowDiskSpace = value;
|
||||
}
|
||||
public static Boolean IsPausedForLowDiskSpace { get; set; }
|
||||
|
||||
public static (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId)
|
||||
{
|
||||
return SharedState.GetStats(downloadId);
|
||||
if (ActiveDownloadClients.TryGetValue(downloadId, out var downloadClient))
|
||||
{
|
||||
return (downloadClient.Speed, downloadClient.BytesTotal, downloadClient.BytesDone);
|
||||
}
|
||||
|
||||
if (ActiveUnpackClients.TryGetValue(downloadId, out var unpackClient))
|
||||
{
|
||||
return (0, 100, unpackClient.Progess);
|
||||
}
|
||||
|
||||
return (0, 0, 0);
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
Log("Initializing TorrentRunner");
|
||||
|
||||
var settingsCopy = JsonSerializer.Deserialize<DbSettings>(JsonSerializer.Serialize(settings.Current));
|
||||
var settingsCopy = JsonSerializer.Deserialize<DbSettings>(JsonSerializer.Serialize(Settings.Get));
|
||||
|
||||
if (settingsCopy != null)
|
||||
{
|
||||
|
|
@ -87,28 +87,28 @@ public class TorrentRunner(
|
|||
|
||||
public async Task Tick()
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(settings.Current.Provider.ApiKey))
|
||||
if (String.IsNullOrWhiteSpace(Settings.Get.Provider.ApiKey))
|
||||
{
|
||||
Log($"No RealDebridApiKey set in settings");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var settingDownloadLimit = settings.Current.General.DownloadLimit;
|
||||
var settingDownloadLimit = Settings.Get.General.DownloadLimit;
|
||||
|
||||
if (settingDownloadLimit < 1)
|
||||
{
|
||||
settingDownloadLimit = 1;
|
||||
}
|
||||
|
||||
var settingUnpackLimit = settings.Current.General.UnpackLimit;
|
||||
var settingUnpackLimit = Settings.Get.General.UnpackLimit;
|
||||
|
||||
if (settingUnpackLimit < 0)
|
||||
{
|
||||
settingUnpackLimit = 0;
|
||||
}
|
||||
|
||||
var settingDownloadPath = settings.Current.DownloadClient.DownloadPath;
|
||||
var settingDownloadPath = Settings.Get.DownloadClient.DownloadPath;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(settingDownloadPath))
|
||||
{
|
||||
|
|
@ -141,25 +141,25 @@ public class TorrentRunner(
|
|||
_lastNextAllowedAt = currentNextAllowedAt;
|
||||
}
|
||||
|
||||
if (!runnerState.ActiveDownloadClients.IsEmpty || !runnerState.ActiveUnpackClients.IsEmpty)
|
||||
if (!ActiveDownloadClients.IsEmpty || !ActiveUnpackClients.IsEmpty)
|
||||
{
|
||||
Log($"TorrentRunner Tick Start, {runnerState.ActiveDownloadClients.Count} active downloads, {runnerState.ActiveUnpackClients.Count} active unpacks");
|
||||
Log($"TorrentRunner Tick Start, {ActiveDownloadClients.Count} active downloads, {ActiveUnpackClients.Count} active unpacks");
|
||||
}
|
||||
|
||||
if (runnerState.ActiveDownloadClients.Any(m => m.Value.Type == Data.Enums.DownloadClient.Aria2c))
|
||||
if (ActiveDownloadClients.Any(m => m.Value.Type == Data.Enums.DownloadClient.Aria2c))
|
||||
{
|
||||
Log("Updating Aria2 status");
|
||||
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
var aria2NetClient = new Aria2NetClient(settings.Current.DownloadClient.Aria2cUrl, settings.Current.DownloadClient.Aria2cSecret, httpClient, 1);
|
||||
var aria2NetClient = new Aria2NetClient(Settings.Get.DownloadClient.Aria2cUrl, Settings.Get.DownloadClient.Aria2cSecret, httpClient, 1);
|
||||
|
||||
var allDownloads = await aria2NetClient.TellAllAsync();
|
||||
|
||||
Log($"Found {allDownloads.Count} Aria2 downloads");
|
||||
|
||||
foreach (var activeDownload in runnerState.ActiveDownloadClients)
|
||||
foreach (var activeDownload in ActiveDownloadClients)
|
||||
{
|
||||
if (activeDownload.Value.Downloader is Aria2cDownloader aria2Downloader)
|
||||
{
|
||||
|
|
@ -170,11 +170,11 @@ public class TorrentRunner(
|
|||
Log("Finished updating Aria2 status");
|
||||
}
|
||||
|
||||
if (runnerState.ActiveDownloadClients.Any(m => m.Value.Type == Data.Enums.DownloadClient.DownloadStation))
|
||||
if (ActiveDownloadClients.Any(m => m.Value.Type == Data.Enums.DownloadClient.DownloadStation))
|
||||
{
|
||||
Log("Updating DownloadStation status");
|
||||
|
||||
foreach (var activeDownload in runnerState.ActiveDownloadClients)
|
||||
foreach (var activeDownload in ActiveDownloadClients)
|
||||
{
|
||||
if (activeDownload.Value.Downloader is DownloadStationDownloader downloadStationDownloader)
|
||||
{
|
||||
|
|
@ -184,7 +184,7 @@ public class TorrentRunner(
|
|||
}
|
||||
|
||||
// Check if any torrents are finished downloading to the host, remove them from the active download list.
|
||||
var completedActiveDownloads = runnerState.ActiveDownloadClients.Where(m => m.Value.Finished).ToList();
|
||||
var completedActiveDownloads = ActiveDownloadClients.Where(m => m.Value.Finished).ToList();
|
||||
|
||||
if (completedActiveDownloads.Count > 0)
|
||||
{
|
||||
|
|
@ -196,7 +196,7 @@ public class TorrentRunner(
|
|||
|
||||
if (download == null)
|
||||
{
|
||||
runnerState.ActiveDownloadClients.TryRemove(downloadId, out _);
|
||||
ActiveDownloadClients.TryRemove(downloadId, out _);
|
||||
|
||||
Log($"Download with ID {downloadId} not found! Removed from download queue");
|
||||
|
||||
|
|
@ -237,14 +237,14 @@ public class TorrentRunner(
|
|||
await downloads.UpdateUnpackingQueued(downloadId, DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
runnerState.ActiveDownloadClients.TryRemove(downloadId, out _);
|
||||
ActiveDownloadClients.TryRemove(downloadId, out _);
|
||||
|
||||
Log($"Removed from ActiveDownloadClients", download, download.Torrent);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any torrents are finished unpacking, remove them from the active unpack list.
|
||||
var completedUnpacks = runnerState.ActiveUnpackClients.Where(m => m.Value.Finished).ToList();
|
||||
var completedUnpacks = ActiveUnpackClients.Where(m => m.Value.Finished).ToList();
|
||||
|
||||
if (completedUnpacks.Count > 0)
|
||||
{
|
||||
|
|
@ -256,7 +256,7 @@ public class TorrentRunner(
|
|||
|
||||
if (download == null)
|
||||
{
|
||||
runnerState.ActiveUnpackClients.TryRemove(downloadId, out _);
|
||||
ActiveUnpackClients.TryRemove(downloadId, out _);
|
||||
|
||||
Log($"Download with ID {downloadId} not found! Removed from unpack queue");
|
||||
|
||||
|
|
@ -278,33 +278,36 @@ public class TorrentRunner(
|
|||
|
||||
await downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow);
|
||||
|
||||
runnerState.ActiveUnpackClients.TryRemove(downloadId, out _);
|
||||
ActiveUnpackClients.TryRemove(downloadId, out _);
|
||||
|
||||
Log($"Removed from ActiveUnpackClients", download, download.Torrent);
|
||||
}
|
||||
}
|
||||
|
||||
var allTorrents = await torrents.Get();
|
||||
var downloadsById = allTorrents.SelectMany(m => m.Downloads).ToDictionary(m => m.DownloadId, m => m);
|
||||
|
||||
// Check for deleted torrents that are stuck in the ActiveDownloads or ActiveUnpacks
|
||||
foreach (var activeDownload in runnerState.ActiveDownloadClients)
|
||||
foreach (var activeDownload in ActiveDownloadClients)
|
||||
{
|
||||
if (!downloadsById.ContainsKey(activeDownload.Key))
|
||||
var download = allTorrents.SelectMany(m => m.Downloads).FirstOrDefault(m => m.DownloadId == activeDownload.Key);
|
||||
|
||||
if (download == null)
|
||||
{
|
||||
await activeDownload.Value.Cancel();
|
||||
runnerState.ActiveDownloadClients.TryRemove(activeDownload.Key, out _);
|
||||
ActiveDownloadClients.TryRemove(activeDownload.Key, out _);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var activeUnpacks in runnerState.ActiveUnpackClients)
|
||||
foreach (var activeUnpacks in ActiveUnpackClients)
|
||||
{
|
||||
if (!downloadsById.ContainsKey(activeUnpacks.Key))
|
||||
var download = allTorrents.SelectMany(m => m.Downloads).FirstOrDefault(m => m.DownloadId == activeUnpacks.Key);
|
||||
|
||||
if (download == null)
|
||||
{
|
||||
activeUnpacks.Value.Cancel();
|
||||
runnerState.ActiveUnpackClients.TryRemove(activeUnpacks.Key, out _);
|
||||
ActiveUnpackClients.TryRemove(activeUnpacks.Key, out _);
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -367,10 +370,8 @@ public class TorrentRunner(
|
|||
}
|
||||
|
||||
// Process torrents in DebridQueue
|
||||
var torrentsToAddToProvider = allTorrents
|
||||
.Where(m => m.Completed == null && m.Error == null && m.RdId == null && m.RdAdded == null && m.FileOrMagnet != null &&
|
||||
m.RdStatus == TorrentStatus.Queued)
|
||||
.ToList();
|
||||
var torrentsToAddToProvider = allTorrents.Where(m => m.RdId == null && m.RdAdded == null && m.FileOrMagnet != null && m.RdStatus == TorrentStatus.Queued)
|
||||
.ToList();
|
||||
|
||||
if (torrentsToAddToProvider.Count != 0)
|
||||
{
|
||||
|
|
@ -384,7 +385,7 @@ public class TorrentRunner(
|
|||
{
|
||||
var downloadingTorrentsCount = allTorrents.Count(m => m.RdStatus is not (TorrentStatus.Queued or TorrentStatus.Finished or TorrentStatus.Error));
|
||||
|
||||
var maxParallelDownloads = settings.Current.Provider.MaxParallelDownloads;
|
||||
var maxParallelDownloads = Settings.Get.Provider.MaxParallelDownloads;
|
||||
|
||||
logger.LogDebug("Currently downloading {downloadingTorrentCount}/{maxParallelDownloads} torrents, {queuedCount} queued.",
|
||||
downloadingTorrentsCount,
|
||||
|
|
@ -479,35 +480,31 @@ public class TorrentRunner(
|
|||
{
|
||||
// Check if there are any downloads that are queued and can be started.
|
||||
var queuedDownloads = torrent.Downloads
|
||||
.Where(m => m.Completed == null
|
||||
&& m.DownloadQueued != null
|
||||
&& m.DownloadQueued <= DateTimeOffset.UtcNow
|
||||
&& m.DownloadStarted == null
|
||||
&& m.Error == null)
|
||||
.Where(m => m.Completed == null && m.DownloadQueued != null && m.DownloadStarted == null && m.Error == null)
|
||||
.OrderBy(m => m.DownloadQueued)
|
||||
.ToList();
|
||||
|
||||
Log($"Currently {queuedDownloads.Count} queued downloads and {runnerState.ActiveDownloadClients.Count} total active downloads", torrent);
|
||||
Log($"Currently {queuedDownloads.Count} queued downloads and {ActiveDownloadClients.Count} total active downloads", torrent);
|
||||
|
||||
foreach (var download in queuedDownloads)
|
||||
{
|
||||
Log($"Processing to download", download, torrent);
|
||||
|
||||
if (runnerState.ActiveDownloadClients.Count >= settingDownloadLimit && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink)
|
||||
if (ActiveDownloadClients.Count >= settingDownloadLimit && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink)
|
||||
{
|
||||
Log($"Not starting download because there are already the max number of downloads active", download, torrent);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (runnerState.IsPausedForLowDiskSpace && torrent.DownloadClient == Data.Enums.DownloadClient.Bezzad)
|
||||
if (IsPausedForLowDiskSpace && torrent.DownloadClient == Data.Enums.DownloadClient.Bezzad)
|
||||
{
|
||||
logger.LogInformation($"Not starting Bezzad download because of low disk space {download.ToLog()} {torrent.ToLog()}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (runnerState.ActiveDownloadClients.ContainsKey(download.DownloadId))
|
||||
if (ActiveDownloadClients.ContainsKey(download.DownloadId))
|
||||
{
|
||||
Log($"Not starting download because this download is already active", download, torrent);
|
||||
|
||||
|
|
@ -534,24 +531,10 @@ public class TorrentRunner(
|
|||
{
|
||||
logger.LogError(ex, "Cannot unrestrict link: {ex.Message}", ex.Message);
|
||||
|
||||
if (download.RetryCount < torrent.DownloadRetryAttempts)
|
||||
{
|
||||
var retryCount = download.RetryCount + 1;
|
||||
var retryDelay = GetDownloadLinkRetryDelay(retryCount);
|
||||
var retryAt = DateTimeOffset.UtcNow.Add(retryDelay);
|
||||
|
||||
Log($"Retrying download link generation {retryCount}/{torrent.DownloadRetryAttempts} at {retryAt:u}", download, torrent);
|
||||
|
||||
await downloads.Reset(download.DownloadId, retryAt);
|
||||
await downloads.UpdateRetryCount(download.DownloadId, retryCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
await downloads.UpdateError(download.DownloadId, ex.Message);
|
||||
await downloads.UpdateCompleted(download.DownloadId, DateTimeOffset.UtcNow);
|
||||
download.Error = ex.Message;
|
||||
download.Completed = DateTimeOffset.UtcNow;
|
||||
}
|
||||
await downloads.UpdateError(download.DownloadId, ex.Message);
|
||||
await downloads.UpdateCompleted(download.DownloadId, DateTimeOffset.UtcNow);
|
||||
download.Error = ex.Message;
|
||||
download.Completed = DateTimeOffset.UtcNow;
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -573,7 +556,7 @@ public class TorrentRunner(
|
|||
// Start the download process
|
||||
var downloadClient = new DownloadClient(download, torrent, downloadPath, torrent.Category);
|
||||
|
||||
if (runnerState.ActiveDownloadClients.TryAdd(download.DownloadId, downloadClient))
|
||||
if (ActiveDownloadClients.TryAdd(download.DownloadId, downloadClient))
|
||||
{
|
||||
Log($"Starting download", download, torrent);
|
||||
|
||||
|
|
@ -593,7 +576,7 @@ public class TorrentRunner(
|
|||
await downloads.UpdateRemoteId(download.DownloadId, remoteId);
|
||||
}
|
||||
|
||||
if (runnerState.IsPausedForLowDiskSpace && downloadClient.Type == Data.Enums.DownloadClient.Bezzad)
|
||||
if (IsPausedForLowDiskSpace && downloadClient.Type == Data.Enums.DownloadClient.Bezzad)
|
||||
{
|
||||
logger.LogInformation($"Pausing new Bezzad download due to low disk space {download.ToLog()} {torrent.ToLog()}");
|
||||
await downloadClient.Pause();
|
||||
|
|
@ -653,14 +636,14 @@ public class TorrentRunner(
|
|||
}
|
||||
|
||||
// Check if we have reached the download limit, if so queue the download, but don't start it.
|
||||
if (runnerState.ActiveUnpackClients.Count >= settingUnpackLimit)
|
||||
if (ActiveUnpackClients.Count >= settingUnpackLimit)
|
||||
{
|
||||
Log($"Not starting unpack because there are already the max number of unpacks active", download, torrent);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (runnerState.ActiveUnpackClients.ContainsKey(download.DownloadId))
|
||||
if (ActiveUnpackClients.ContainsKey(download.DownloadId))
|
||||
{
|
||||
Log($"Not starting unpack because this download is already active", download, torrent);
|
||||
|
||||
|
|
@ -682,7 +665,7 @@ public class TorrentRunner(
|
|||
// Start the unpacking process
|
||||
var unpackClient = new UnpackClient(download, downloadPath);
|
||||
|
||||
if (runnerState.ActiveUnpackClients.TryAdd(download.DownloadId, unpackClient))
|
||||
if (ActiveUnpackClients.TryAdd(download.DownloadId, unpackClient))
|
||||
{
|
||||
Log($"Starting unpack", download, torrent);
|
||||
|
||||
|
|
@ -740,8 +723,8 @@ public class TorrentRunner(
|
|||
|
||||
var completePerc = 0;
|
||||
|
||||
var totalDownloadBytes = torrent.Downloads.Sum(m => runnerState.GetStats(m.DownloadId).BytesTotal);
|
||||
var totalDoneBytes = torrent.Downloads.Sum(m => runnerState.GetStats(m.DownloadId).BytesDone);
|
||||
var totalDownloadBytes = torrent.Downloads.Sum(m => GetStats(m.DownloadId).BytesTotal);
|
||||
var totalDoneBytes = torrent.Downloads.Sum(m => GetStats(m.DownloadId).BytesDone);
|
||||
|
||||
if (totalDownloadBytes > 0)
|
||||
{
|
||||
|
|
@ -802,19 +785,6 @@ public class TorrentRunner(
|
|||
});
|
||||
}
|
||||
|
||||
private static TimeSpan GetDownloadLinkRetryDelay(Int32 retryCount)
|
||||
{
|
||||
var seconds = retryCount switch
|
||||
{
|
||||
<= 1 => 15,
|
||||
2 => 30,
|
||||
3 => 60,
|
||||
_ => 120
|
||||
};
|
||||
|
||||
return TimeSpan.FromSeconds(seconds);
|
||||
}
|
||||
|
||||
private void Log(String message, Download? download, Torrent? torrent)
|
||||
{
|
||||
if (download != null)
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
using System.Collections.Concurrent;
|
||||
|
||||
namespace RdtClient.Service.Services;
|
||||
|
||||
public interface ITorrentRunnerState
|
||||
{
|
||||
ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients { get; }
|
||||
|
||||
ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients { get; }
|
||||
|
||||
Boolean IsPausedForLowDiskSpace { get; set; }
|
||||
|
||||
(Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId);
|
||||
|
||||
void Clear();
|
||||
}
|
||||
|
||||
public class TorrentRunnerState : ITorrentRunnerState
|
||||
{
|
||||
public ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients { get; } = new();
|
||||
|
||||
public ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients { get; } = new();
|
||||
|
||||
public Boolean IsPausedForLowDiskSpace { get; set; }
|
||||
|
||||
public (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId)
|
||||
{
|
||||
if (ActiveDownloadClients.TryGetValue(downloadId, out var downloadClient))
|
||||
{
|
||||
return (downloadClient.Speed, downloadClient.BytesTotal, downloadClient.BytesDone);
|
||||
}
|
||||
|
||||
if (ActiveUnpackClients.TryGetValue(downloadId, out var unpackClient))
|
||||
{
|
||||
return (0, 100, unpackClient.Progess);
|
||||
}
|
||||
|
||||
return (0, 0, 0);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ActiveDownloadClients.Clear();
|
||||
ActiveUnpackClients.Clear();
|
||||
IsPausedForLowDiskSpace = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.IO.Abstractions;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
|
@ -32,24 +32,22 @@ public class Torrents(
|
|||
PremiumizeDebridClient premiumizeDebridClient,
|
||||
RealDebridDebridClient realDebridDebridClient,
|
||||
DebridLinkClient debridLinkClient,
|
||||
TorBoxDebridClient torBoxDebridClient,
|
||||
ISettings settings,
|
||||
ITorrentRunnerState runnerState)
|
||||
TorBoxDebridClient torBoxDebridClient)
|
||||
{
|
||||
private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1);
|
||||
|
||||
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
|
||||
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles
|
||||
};
|
||||
|
||||
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
|
||||
|
||||
private IDebridClient DebridClient
|
||||
{
|
||||
get
|
||||
{
|
||||
return settings.Current.Provider.Provider switch
|
||||
return Settings.Get.Provider.Provider switch
|
||||
{
|
||||
Provider.Premiumize => premiumizeDebridClient,
|
||||
Provider.RealDebrid => realDebridDebridClient,
|
||||
|
|
@ -63,7 +61,7 @@ public class Torrents(
|
|||
|
||||
public virtual (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetDownloadStats(Guid downloadId)
|
||||
{
|
||||
return runnerState.GetStats(downloadId);
|
||||
return TorrentRunner.GetStats(downloadId);
|
||||
}
|
||||
|
||||
public virtual async Task<IList<Torrent>> Get()
|
||||
|
|
@ -199,9 +197,9 @@ public class Torrents(
|
|||
throw new($"{ex.Message}, trying to parse {magnetLink}");
|
||||
}
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(settings.Current.General.BannedTrackers))
|
||||
if (!String.IsNullOrWhiteSpace(Settings.Get.General.BannedTrackers))
|
||||
{
|
||||
var bannedTrackers = settings.Current.General.BannedTrackers.Split(',');
|
||||
var bannedTrackers = Settings.Get.General.BannedTrackers.Split(',');
|
||||
|
||||
foreach (var bannedTracker in bannedTrackers)
|
||||
{
|
||||
|
|
@ -266,9 +264,9 @@ public class Torrents(
|
|||
throw new($"{ex.Message}, trying to parse {fileAsBase64}");
|
||||
}
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(settings.Current.General.BannedTrackers))
|
||||
if (!String.IsNullOrWhiteSpace(Settings.Get.General.BannedTrackers))
|
||||
{
|
||||
var bannedTrackers = settings.Current.General.BannedTrackers.Split(',');
|
||||
var bannedTrackers = Settings.Get.General.BannedTrackers.Split(',');
|
||||
|
||||
foreach (var bannedTracker in bannedTrackers)
|
||||
{
|
||||
|
|
@ -314,16 +312,16 @@ public class Torrents(
|
|||
|
||||
private async Task CopyAddedTorrent(Torrent torrent)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(settings.Current.General.CopyAddedTorrents) || String.IsNullOrWhiteSpace(torrent.FileOrMagnet) || String.IsNullOrWhiteSpace(torrent.RdName))
|
||||
if (String.IsNullOrWhiteSpace(Settings.Get.General.CopyAddedTorrents) || String.IsNullOrWhiteSpace(torrent.FileOrMagnet) || String.IsNullOrWhiteSpace(torrent.RdName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!fileSystem.Directory.Exists(settings.Current.General.CopyAddedTorrents))
|
||||
if (!fileSystem.Directory.Exists(Settings.Get.General.CopyAddedTorrents))
|
||||
{
|
||||
fileSystem.Directory.CreateDirectory(settings.Current.General.CopyAddedTorrents);
|
||||
fileSystem.Directory.CreateDirectory(Settings.Get.General.CopyAddedTorrents);
|
||||
}
|
||||
|
||||
var extension = torrent.Type switch
|
||||
|
|
@ -333,7 +331,7 @@ public class Torrents(
|
|||
_ => throw new ArgumentException("Unexpected DownloadType")
|
||||
};
|
||||
|
||||
var copyFileName = Path.Combine(settings.Current.General.CopyAddedTorrents, FileHelper.RemoveInvalidFileNameChars(torrent.RdName));
|
||||
var copyFileName = Path.Combine(Settings.Get.General.CopyAddedTorrents, FileHelper.RemoveInvalidFileNameChars(torrent.RdName));
|
||||
|
||||
if (!copyFileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
|
@ -357,7 +355,7 @@ public class Torrents(
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"Unable to create torrent blackhole directory: {settings.Current.General.CopyAddedTorrents}: {ex.Message}");
|
||||
logger.LogError(ex, $"Unable to create torrent blackhole directory: {Settings.Get.General.CopyAddedTorrents}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -459,23 +457,12 @@ public class Torrents(
|
|||
|
||||
foreach (var downloadInfo in downloadInfos)
|
||||
{
|
||||
var addResult = await downloads.TryAddForTorrent(torrent.TorrentId, downloadInfo);
|
||||
// Make sure downloads don't get added multiple times
|
||||
var downloadExists = await downloads.Get(torrent.TorrentId, downloadInfo.RestrictedLink);
|
||||
|
||||
switch (addResult)
|
||||
if (downloadExists == null && !String.IsNullOrWhiteSpace(downloadInfo.RestrictedLink))
|
||||
{
|
||||
case DownloadAddResult.Added:
|
||||
case DownloadAddResult.AlreadyExists:
|
||||
continue;
|
||||
case DownloadAddResult.TorrentMissing:
|
||||
logger.LogDebug("Stopping download creation because the torrent was deleted concurrently. TorrentId: {torrentId}", torrent.TorrentId);
|
||||
|
||||
return;
|
||||
case DownloadAddResult.InvalidInput:
|
||||
logger.LogDebug("Skipping download creation because the provider returned an invalid download link. TorrentId: {torrentId}", torrent.TorrentId);
|
||||
|
||||
continue;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(addResult), addResult, null);
|
||||
await downloads.Add(torrent.TorrentId, downloadInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -513,7 +500,7 @@ public class Torrents(
|
|||
{
|
||||
var retry = 10;
|
||||
|
||||
while (runnerState.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
|
||||
while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient))
|
||||
{
|
||||
Log($"Cancelling download", download, torrent);
|
||||
|
||||
|
|
@ -531,7 +518,7 @@ public class Torrents(
|
|||
|
||||
retry = 10;
|
||||
|
||||
while (runnerState.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
|
||||
while (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient))
|
||||
{
|
||||
Log($"Cancelling unpack", download, torrent);
|
||||
|
||||
|
|
@ -552,6 +539,7 @@ public class Torrents(
|
|||
{
|
||||
Log($"Deleting RdtClient data", torrent);
|
||||
|
||||
await downloads.DeleteForTorrent(torrent.TorrentId);
|
||||
await torrentData.Delete(torrentId);
|
||||
}
|
||||
|
||||
|
|
@ -571,7 +559,7 @@ public class Torrents(
|
|||
|
||||
if (deleteLocalFiles && !String.IsNullOrWhiteSpace(torrent.RdName))
|
||||
{
|
||||
var downloadPath = DownloadPath(torrent, settings.Current);
|
||||
var downloadPath = DownloadPath(torrent);
|
||||
downloadPath = Path.Combine(downloadPath, torrent.RdName);
|
||||
|
||||
Log($"Deleting local files in {downloadPath}", torrent);
|
||||
|
|
@ -617,6 +605,7 @@ public class Torrents(
|
|||
return unrestrictedLink;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<String> RetrieveFileName(Guid downloadId)
|
||||
{
|
||||
var download = await downloads.GetById(downloadId) ?? throw new($"Download with ID {downloadId} not found");
|
||||
|
|
@ -636,13 +625,13 @@ public class Torrents(
|
|||
|
||||
var profile = new Profile
|
||||
{
|
||||
Provider = Enum.GetName(settings.Current.Provider.Provider),
|
||||
Provider = Enum.GetName(Settings.Get.Provider.Provider),
|
||||
UserName = user.Username,
|
||||
Expiration = user.Expiration,
|
||||
CurrentVersion = UpdateChecker.CurrentVersion,
|
||||
LatestVersion = UpdateChecker.LatestVersion,
|
||||
IsInsecure = UpdateChecker.IsInsecure,
|
||||
DisableUpdateNotification = settings.Current.General.DisableUpdateNotifications
|
||||
DisableUpdateNotification = Settings.Get.General.DisableUpdateNotifications
|
||||
};
|
||||
|
||||
return profile;
|
||||
|
|
@ -657,65 +646,31 @@ public class Torrents(
|
|||
try
|
||||
{
|
||||
var rdTorrents = await DebridClient.GetDownloads();
|
||||
var torrentsByRdId = CreateTorrentLookupByRdId(torrents);
|
||||
var providerTorrentsById = CreateProviderTorrentLookupById(rdTorrents);
|
||||
|
||||
foreach (var rdTorrent in rdTorrents)
|
||||
{
|
||||
torrentsByRdId.TryGetValue(rdTorrent.Id, out var torrent);
|
||||
|
||||
// TorBox migration from storing torrent hash in RdId to torrent ids.
|
||||
if (torrent == null
|
||||
&& Settings.Get.Provider.Provider == Provider.TorBox
|
||||
&& rdTorrent.Type == DownloadType.Torrent
|
||||
&& !String.IsNullOrWhiteSpace(rdTorrent.Hash)
|
||||
&& !String.IsNullOrWhiteSpace(rdTorrent.Id))
|
||||
{
|
||||
torrent = torrents.FirstOrDefault(localTorrent => localTorrent is { Type: DownloadType.Torrent, ClientKind: null or Provider.TorBox }
|
||||
&& !String.IsNullOrWhiteSpace(localTorrent.Hash)
|
||||
&& !String.IsNullOrWhiteSpace(localTorrent.RdId)
|
||||
&& localTorrent.RdId.Equals(localTorrent.Hash, StringComparison.OrdinalIgnoreCase)
|
||||
&& localTorrent.Hash.Equals(rdTorrent.Hash, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (torrent != null)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(torrent.RdId))
|
||||
{
|
||||
torrentsByRdId.Remove(torrent.RdId);
|
||||
}
|
||||
|
||||
await torrentData.UpdateRdId(torrent, rdTorrent.Id);
|
||||
torrent.RdId = rdTorrent.Id;
|
||||
torrent.ClientKind = Provider.TorBox;
|
||||
torrentsByRdId[rdTorrent.Id] = torrent;
|
||||
|
||||
logger.LogInformation("Migrated TorBox torrent RdId from hash to torrent id for {TorrentName} ({Hash}) -> {RdId}",
|
||||
torrent.RdName ?? rdTorrent.Filename,
|
||||
rdTorrent.Hash,
|
||||
rdTorrent.Id);
|
||||
}
|
||||
}
|
||||
var torrent = torrents.FirstOrDefault(m => m.RdId == rdTorrent.Id);
|
||||
|
||||
// Auto import torrents only torrents that have their files selected
|
||||
if (torrent == null && settings.Current.Provider.AutoImport)
|
||||
if (torrent == null && Settings.Get.Provider.AutoImport)
|
||||
{
|
||||
var newTorrent = new Torrent
|
||||
{
|
||||
Category = settings.Current.Provider.Default.Category,
|
||||
DownloadClient = settings.Current.DownloadClient.Client,
|
||||
Category = Settings.Get.Provider.Default.Category,
|
||||
DownloadClient = Settings.Get.DownloadClient.Client,
|
||||
DownloadAction =
|
||||
settings.Current.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
HostDownloadAction = settings.Current.Provider.Default.HostDownloadAction,
|
||||
FinishedActionDelay = settings.Current.Provider.Default.FinishedActionDelay,
|
||||
FinishedAction = settings.Current.Provider.Default.FinishedAction,
|
||||
DownloadMinSize = settings.Current.Provider.Default.MinFileSize,
|
||||
IncludeRegex = settings.Current.Provider.Default.IncludeRegex,
|
||||
ExcludeRegex = settings.Current.Provider.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = settings.Current.Provider.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = settings.Current.Provider.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = settings.Current.Provider.Default.DeleteOnError,
|
||||
Lifetime = settings.Current.Provider.Default.TorrentLifetime,
|
||||
Priority = settings.Current.Provider.Default.Priority > 0 ? settings.Current.Provider.Default.Priority : null,
|
||||
Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
|
||||
HostDownloadAction = Settings.Get.Provider.Default.HostDownloadAction,
|
||||
FinishedActionDelay = Settings.Get.Provider.Default.FinishedActionDelay,
|
||||
FinishedAction = Settings.Get.Provider.Default.FinishedAction,
|
||||
DownloadMinSize = Settings.Get.Provider.Default.MinFileSize,
|
||||
IncludeRegex = Settings.Get.Provider.Default.IncludeRegex,
|
||||
ExcludeRegex = Settings.Get.Provider.Default.ExcludeRegex,
|
||||
TorrentRetryAttempts = Settings.Get.Provider.Default.TorrentRetryAttempts,
|
||||
DownloadRetryAttempts = Settings.Get.Provider.Default.DownloadRetryAttempts,
|
||||
DeleteOnError = Settings.Get.Provider.Default.DeleteOnError,
|
||||
Lifetime = Settings.Get.Provider.Default.TorrentLifetime,
|
||||
Priority = Settings.Get.Provider.Default.Priority > 0 ? Settings.Get.Provider.Default.Priority : null,
|
||||
RdId = rdTorrent.Id
|
||||
};
|
||||
|
||||
|
|
@ -724,9 +679,7 @@ public class Torrents(
|
|||
continue;
|
||||
}
|
||||
|
||||
torrent = await torrentData.Add(rdTorrent.Id, rdTorrent.Hash, null, false, DownloadType.Torrent, settings.Current.DownloadClient.Client, newTorrent);
|
||||
torrentsByRdId[rdTorrent.Id] = torrent;
|
||||
torrents.Add(torrent);
|
||||
torrent = await torrentData.Add(rdTorrent.Id, rdTorrent.Hash, null, false, DownloadType.Torrent, Settings.Get.DownloadClient.Client, newTorrent);
|
||||
|
||||
await UpdateTorrentClientData(torrent, rdTorrent);
|
||||
}
|
||||
|
|
@ -738,9 +691,9 @@ public class Torrents(
|
|||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
var rdTorrent = torrent.RdId != null && providerTorrentsById.TryGetValue(torrent.RdId, out var providerTorrent) ? providerTorrent : null;
|
||||
var rdTorrent = rdTorrents.FirstOrDefault(m => m.Id == torrent.RdId);
|
||||
|
||||
if (rdTorrent == null && settings.Current.Provider.AutoDelete && torrent.RdStatus != TorrentStatus.Queued)
|
||||
if (rdTorrent == null && Settings.Get.Provider.AutoDelete && torrent.RdStatus != TorrentStatus.Queued)
|
||||
{
|
||||
await Delete(torrent.TorrentId, true, false, true);
|
||||
}
|
||||
|
|
@ -778,14 +731,14 @@ public class Torrents(
|
|||
|
||||
foreach (var download in torrent.Downloads)
|
||||
{
|
||||
while (runnerState.ActiveDownloadClients.TryRemove(download.DownloadId, out var downloadClient))
|
||||
while (TorrentRunner.ActiveDownloadClients.TryRemove(download.DownloadId, out var downloadClient))
|
||||
{
|
||||
await downloadClient.Cancel();
|
||||
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
while (runnerState.ActiveUnpackClients.TryRemove(download.DownloadId, out var unpackClient))
|
||||
while (TorrentRunner.ActiveUnpackClients.TryRemove(download.DownloadId, out var unpackClient))
|
||||
{
|
||||
unpackClient.Cancel();
|
||||
|
||||
|
|
@ -848,21 +801,21 @@ public class Torrents(
|
|||
|
||||
Log($"Retrying Download", download, download.Torrent);
|
||||
|
||||
while (runnerState.ActiveDownloadClients.TryRemove(download.DownloadId, out var downloadClient))
|
||||
while (TorrentRunner.ActiveDownloadClients.TryRemove(download.DownloadId, out var downloadClient))
|
||||
{
|
||||
await downloadClient.Cancel();
|
||||
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
while (runnerState.ActiveUnpackClients.TryRemove(download.DownloadId, out var unpackClient))
|
||||
while (TorrentRunner.ActiveUnpackClients.TryRemove(download.DownloadId, out var unpackClient))
|
||||
{
|
||||
unpackClient.Cancel();
|
||||
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
var downloadPath = DownloadPath(download.Torrent!, settings.Current);
|
||||
var downloadPath = DownloadPath(download.Torrent!);
|
||||
|
||||
var filePath = DownloadHelper.GetDownloadPath(downloadPath, download.Torrent!, download);
|
||||
|
||||
|
|
@ -890,43 +843,6 @@ public class Torrents(
|
|||
await torrentData.UpdateFilesSelected(torrentId, datetime);
|
||||
}
|
||||
|
||||
public async Task<Boolean> UpdateFileSelection(String hash, IReadOnlyCollection<Int32> fileIds, Boolean selected)
|
||||
{
|
||||
if (fileIds.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var torrent = await torrentData.GetByHash(hash);
|
||||
|
||||
if (torrent == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var files = torrent.Files.ToList();
|
||||
|
||||
if (files.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var fileId in fileIds)
|
||||
{
|
||||
if (fileId < 0 || fileId >= files.Count)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
files[fileId].Selected = selected;
|
||||
}
|
||||
|
||||
torrent.RdFiles = JsonSerializer.Serialize(files, JsonSerializerOptions);
|
||||
await torrentData.UpdateRdData(torrent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task UpdatePriority(String hash, Int32 priority)
|
||||
{
|
||||
var torrent = await torrentData.GetByHash(hash);
|
||||
|
|
@ -963,9 +879,9 @@ public class Torrents(
|
|||
return torrent;
|
||||
}
|
||||
|
||||
private static String DownloadPath(Torrent torrent, DbSettings settings)
|
||||
private static String DownloadPath(Torrent torrent)
|
||||
{
|
||||
var settingDownloadPath = settings.DownloadClient.DownloadPath;
|
||||
var settingDownloadPath = Settings.Get.DownloadClient.DownloadPath;
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(torrent.Category))
|
||||
{
|
||||
|
|
@ -1004,11 +920,11 @@ public class Torrents(
|
|||
await torrentData.Update(torrent);
|
||||
}
|
||||
|
||||
public async Task RunTorrentComplete(Guid torrentId, DbSettings? runSettings = null)
|
||||
public async Task RunTorrentComplete(Guid torrentId, DbSettings? settings = null)
|
||||
{
|
||||
runSettings ??= settings.Current;
|
||||
settings ??= Settings.Get;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(runSettings.General.RunOnTorrentCompleteFileName))
|
||||
if (String.IsNullOrWhiteSpace(settings.General.RunOnTorrentCompleteFileName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -1017,12 +933,12 @@ public class Torrents(
|
|||
|
||||
var downloadsForTorrent = await downloads.GetForTorrent(torrentId);
|
||||
|
||||
var fileName = runSettings.General.RunOnTorrentCompleteFileName;
|
||||
var arguments = runSettings.General.RunOnTorrentCompleteArguments ?? "";
|
||||
var fileName = settings.General.RunOnTorrentCompleteFileName;
|
||||
var arguments = settings.General.RunOnTorrentCompleteArguments ?? "";
|
||||
|
||||
Log($"Parsing external program {fileName} with arguments {arguments}", torrent);
|
||||
|
||||
var downloadPath = DownloadPath(torrent, runSettings);
|
||||
var downloadPath = DownloadPath(torrent);
|
||||
var torrentPath = Path.Combine(downloadPath, torrent.RdName ?? "Unknown");
|
||||
|
||||
var filePath = torrentPath;
|
||||
|
|
@ -1106,83 +1022,23 @@ public class Torrents(
|
|||
{
|
||||
try
|
||||
{
|
||||
var originalTorrent = CaptureRdState(torrent);
|
||||
var originalTorrent = JsonSerializer.Serialize(torrent, JsonSerializerOptions);
|
||||
|
||||
await DebridClient.UpdateData(torrent, torrentClientTorrent);
|
||||
|
||||
var newTorrent = CaptureRdState(torrent);
|
||||
var newTorrent = JsonSerializer.Serialize(torrent, JsonSerializerOptions);
|
||||
|
||||
if (originalTorrent != newTorrent)
|
||||
{
|
||||
await torrentData.UpdateRdData(torrent);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<String, Torrent> CreateTorrentLookupByRdId(IEnumerable<Torrent> torrents)
|
||||
{
|
||||
var lookup = new Dictionary<String, Torrent>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(torrent.RdId))
|
||||
{
|
||||
lookup[torrent.RdId] = torrent;
|
||||
}
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
private static Dictionary<String, DebridClientTorrent> CreateProviderTorrentLookupById(IEnumerable<DebridClientTorrent> torrents)
|
||||
{
|
||||
var lookup = new Dictionary<String, DebridClientTorrent>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(torrent.Id))
|
||||
{
|
||||
lookup[torrent.Id] = torrent;
|
||||
}
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
private static TorrentRdState CaptureRdState(Torrent torrent)
|
||||
{
|
||||
return new(torrent.RdName,
|
||||
torrent.RdSize,
|
||||
torrent.RdHost,
|
||||
torrent.RdSplit,
|
||||
torrent.RdProgress,
|
||||
torrent.RdStatus,
|
||||
torrent.RdStatusRaw,
|
||||
torrent.RdAdded,
|
||||
torrent.RdEnded,
|
||||
torrent.RdSpeed,
|
||||
torrent.RdSeeders,
|
||||
torrent.RdFiles);
|
||||
}
|
||||
|
||||
private readonly record struct TorrentRdState(
|
||||
String? RdName,
|
||||
Int64? RdSize,
|
||||
String? RdHost,
|
||||
Int64? RdSplit,
|
||||
Int64? RdProgress,
|
||||
TorrentStatus? RdStatus,
|
||||
String? RdStatusRaw,
|
||||
DateTimeOffset? RdAdded,
|
||||
DateTimeOffset? RdEnded,
|
||||
Int64? RdSpeed,
|
||||
Int64? RdSeeders,
|
||||
String? RdFiles);
|
||||
|
||||
private void Log(String message, Download? download, Torrent? torrent)
|
||||
{
|
||||
if (download != null)
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using RdtClient.Data.Models.QBittorrent;
|
||||
using RdtClient.Service.Services;
|
||||
using RdtClient.Web.Controllers;
|
||||
|
||||
namespace RdtClient.Web.Test.Controllers;
|
||||
|
||||
public class QBittorrentControllerTest
|
||||
{
|
||||
private readonly QBittorrentController _controller;
|
||||
private readonly Mock<QBittorrent> _qBittorrentMock;
|
||||
private readonly Mock<Torrents> _torrentsMock;
|
||||
private readonly TestSettings _settings;
|
||||
|
||||
public QBittorrentControllerTest()
|
||||
{
|
||||
_settings = new();
|
||||
_qBittorrentMock = new(new Mock<ILogger<QBittorrent>>().Object, _settings, null!, null!, null!, new TorrentRunnerState());
|
||||
_torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState());
|
||||
|
||||
_controller = new(new Mock<ILogger<QBittorrentController>>().Object,
|
||||
_qBittorrentMock.Object,
|
||||
new Mock<IHttpClientFactory>().Object,
|
||||
_settings,
|
||||
_torrentsMock.Object);
|
||||
|
||||
_controller.ControllerContext = new()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext()
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TorrentsInfo_FilterAll_DoesNotFilterOutResults()
|
||||
{
|
||||
// Arrange
|
||||
_qBittorrentMock.Setup(q => q.TorrentInfo())
|
||||
.ReturnsAsync(new List<TorrentInfo>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Hash = "hash1",
|
||||
State = "pausedUP",
|
||||
Progress = 1f
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _controller.TorrentsInfo(new()
|
||||
{
|
||||
Filter = "all",
|
||||
Hashes = "hash1"
|
||||
});
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var payload = Assert.IsAssignableFrom<IList<TorrentInfo>>(okResult.Value);
|
||||
Assert.Single(payload);
|
||||
Assert.Equal("hash1", payload[0].Hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TorrentsInfo_FilterCompleted_MatchesPausedUploadTorrents()
|
||||
{
|
||||
// Arrange
|
||||
_qBittorrentMock.Setup(q => q.TorrentInfo())
|
||||
.ReturnsAsync(new List<TorrentInfo>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Hash = "hash1",
|
||||
State = "pausedUP",
|
||||
Progress = 1f
|
||||
},
|
||||
new()
|
||||
{
|
||||
Hash = "hash2",
|
||||
State = "downloading",
|
||||
Progress = 0.4f
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _controller.TorrentsInfo(new()
|
||||
{
|
||||
Filter = "completed"
|
||||
});
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var payload = Assert.IsAssignableFrom<IList<TorrentInfo>>(okResult.Value);
|
||||
Assert.Single(payload);
|
||||
Assert.Equal("hash1", payload[0].Hash);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Sabnzbd;
|
||||
using RdtClient.Service.Services;
|
||||
|
|
@ -16,17 +17,15 @@ public class SabnzbdControllerTest
|
|||
private readonly Mock<Authentication> _authenticationMock;
|
||||
private readonly SabnzbdController _controller;
|
||||
private readonly Mock<Sabnzbd> _sabnzbdMock;
|
||||
private readonly TestSettings _settings;
|
||||
|
||||
public SabnzbdControllerTest()
|
||||
{
|
||||
_settings = new();
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.None;
|
||||
_settings.Current.Provider.ApiKey = "test-api-key";
|
||||
SettingData.Get.General.AuthenticationType = AuthenticationType.None;
|
||||
SettingData.Get.Provider.ApiKey = "test-api-key";
|
||||
|
||||
var torrentsMock = new Mock<Torrents>(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState());
|
||||
var torrentsMock = new Mock<Torrents>(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
|
||||
var sabnzbdLoggerMock = new Mock<ILogger<Sabnzbd>>();
|
||||
_sabnzbdMock = new(sabnzbdLoggerMock.Object, torrentsMock.Object, null!, _settings);
|
||||
_sabnzbdMock = new(sabnzbdLoggerMock.Object, torrentsMock.Object, null!);
|
||||
var loggerMock = new Mock<ILogger<SabnzbdController>>();
|
||||
_authenticationMock = new(null!, null!, null!);
|
||||
|
||||
|
|
@ -65,7 +64,7 @@ public class SabnzbdControllerTest
|
|||
public async Task GetQueue_Unauthorized_ReturnsOk_BecauseFilterIsSkippedInUnitTests()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
|
||||
var queue = new SabnzbdQueue
|
||||
{
|
||||
|
|
@ -87,7 +86,7 @@ public class SabnzbdControllerTest
|
|||
public async Task GetQueue_WithMaAuth_ReturnsOk()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.QueryString = new("?ma_username=user&ma_password=pass");
|
||||
_controller.ControllerContext.HttpContext = httpContext;
|
||||
|
|
@ -108,30 +107,6 @@ public class SabnzbdControllerTest
|
|||
Assert.IsType<OkObjectResult>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("?name=delete&value=hash1&del_files=1", true)]
|
||||
[InlineData("?name=delete&value=hash1&del_files=true", true)]
|
||||
[InlineData("?name=delete&value=hash1&del_files=0", false)]
|
||||
[InlineData("?name=delete&value=hash1", false)]
|
||||
public async Task Queue_Delete_PassesDeleteFilesFlag(String queryString, Boolean expectedDeleteFiles)
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.QueryString = new(queryString);
|
||||
_controller.ControllerContext.HttpContext = httpContext;
|
||||
|
||||
_sabnzbdMock.Setup(s => s.Delete("hash1", expectedDeleteFiles)).Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
var result = await _controller.Queue();
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
||||
var response = Assert.IsType<SabnzbdResponse>(okResult.Value);
|
||||
Assert.True(response.Status);
|
||||
_sabnzbdMock.Verify(s => s.Delete("hash1", expectedDeleteFiles), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetHistory_ReturnsOk()
|
||||
{
|
||||
|
|
@ -153,26 +128,6 @@ public class SabnzbdControllerTest
|
|||
Assert.Equal(1, response.History.NoOfSlots);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task History_Delete_PassesDeleteFilesFlag()
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.QueryString = new("?name=delete&value=hash1&del_files=1");
|
||||
_controller.ControllerContext.HttpContext = httpContext;
|
||||
|
||||
_sabnzbdMock.Setup(s => s.Delete("hash1", true)).Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
var result = await _controller.History();
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
||||
var response = Assert.IsType<SabnzbdResponse>(okResult.Value);
|
||||
Assert.True(response.Status);
|
||||
_sabnzbdMock.Verify(s => s.Delete("hash1", true), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetVersion_HasAllowAnonymousAttribute()
|
||||
{
|
||||
|
|
@ -191,7 +146,7 @@ public class SabnzbdControllerTest
|
|||
public void GetVersion_ReturnsOk()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
|
||||
// Act
|
||||
var result = _controller.Version();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Moq;
|
||||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Service.Middleware;
|
||||
using RdtClient.Service.Services;
|
||||
|
|
@ -14,22 +15,20 @@ public class SabnzbdHandlerTest
|
|||
private readonly Mock<Authentication> _authenticationMock;
|
||||
private readonly SabnzbdHandler _handler;
|
||||
private readonly Mock<IHttpContextAccessor> _httpContextAccessorMock;
|
||||
private readonly TestSettings _settings;
|
||||
|
||||
public SabnzbdHandlerTest()
|
||||
{
|
||||
_authenticationMock = new(null!, null!, null!);
|
||||
_httpContextAccessorMock = new();
|
||||
_settings = new();
|
||||
_handler = new(_authenticationMock.Object, _httpContextAccessorMock.Object, _settings);
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
_handler = new(_authenticationMock.Object, _httpContextAccessorMock.Object);
|
||||
SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_AuthNone_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.None;
|
||||
SettingData.Get.General.AuthenticationType = AuthenticationType.None;
|
||||
var context = CreateContext();
|
||||
|
||||
// Act
|
||||
|
|
@ -43,16 +42,9 @@ public class SabnzbdHandlerTest
|
|||
public async Task HandleAsync_ValidCredentials_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
Request =
|
||||
{
|
||||
QueryString = new("?ma_username=user&ma_password=pass")
|
||||
}
|
||||
};
|
||||
|
||||
Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.QueryString = new("?ma_username=user&ma_password=pass");
|
||||
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
|
||||
var context = CreateContext(httpContext);
|
||||
|
|
@ -65,119 +57,11 @@ public class SabnzbdHandlerTest
|
|||
Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid credentials");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_ValidApiKeyCredentials_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
Request =
|
||||
{
|
||||
QueryString = new("?apikey=user:pass")
|
||||
}
|
||||
};
|
||||
|
||||
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
|
||||
var context = CreateContext(httpContext);
|
||||
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success);
|
||||
|
||||
// Act
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid api key credentials");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_ValidApiKeyCredentialsFromForm_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.ContentType = "application/x-www-form-urlencoded";
|
||||
|
||||
httpContext.Request.Form = new FormCollection(new()
|
||||
{
|
||||
{
|
||||
"apikey", "user:pass"
|
||||
}
|
||||
});
|
||||
|
||||
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
|
||||
var context = CreateContext(httpContext);
|
||||
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success);
|
||||
|
||||
// Act
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid form api key credentials");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_ApiKeyPasswordContainingColon_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
Request =
|
||||
{
|
||||
QueryString = new("?apikey=user:pass:with:colons")
|
||||
}
|
||||
};
|
||||
|
||||
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
|
||||
var context = CreateContext(httpContext);
|
||||
_authenticationMock.Setup(a => a.Login("user", "pass:with:colons")).ReturnsAsync(SignInResult.Success);
|
||||
|
||||
// Act
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded, "HasSucceeded should be true when the password contains colons");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_MaCredentialsTakePrecedenceOverApiKey()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
Request =
|
||||
{
|
||||
QueryString = new("?ma_username=user&ma_password=wrong&apikey=user:pass")
|
||||
}
|
||||
};
|
||||
|
||||
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
|
||||
var context = CreateContext(httpContext);
|
||||
_authenticationMock.Setup(a => a.Login("user", "wrong")).ReturnsAsync(SignInResult.Failed);
|
||||
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success);
|
||||
|
||||
// Act
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasSucceeded, "HasSucceeded should be false because ma_username/ma_password take precedence");
|
||||
_authenticationMock.Verify(a => a.Login("user", "wrong"), Times.Once);
|
||||
_authenticationMock.Verify(a => a.Login("user", "pass"), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_AlreadyAuthenticated_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity("TestAuth"));
|
||||
httpContext.User = claimsPrincipal;
|
||||
|
|
@ -196,16 +80,9 @@ public class SabnzbdHandlerTest
|
|||
public async Task HandleAsync_InvalidCredentials_DoesNotSucceed()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
Request =
|
||||
{
|
||||
QueryString = new("?ma_username=user&ma_password=wrong")
|
||||
}
|
||||
};
|
||||
|
||||
Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.QueryString = new("?ma_username=user&ma_password=wrong");
|
||||
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
|
||||
var context = CreateContext(httpContext);
|
||||
|
|
@ -218,66 +95,11 @@ public class SabnzbdHandlerTest
|
|||
Assert.False(context.HasSucceeded, "HasSucceeded should be false for invalid credentials");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_InvalidApiKeyCredentials_DoesNotSucceed()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
Request =
|
||||
{
|
||||
QueryString = new("?apikey=user:wrong")
|
||||
}
|
||||
};
|
||||
|
||||
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
|
||||
var context = CreateContext(httpContext);
|
||||
_authenticationMock.Setup(a => a.Login("user", "wrong")).ReturnsAsync(SignInResult.Failed);
|
||||
|
||||
// Act
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasSucceeded, "HasSucceeded should be false for invalid api key credentials");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("missingdelimiter")]
|
||||
[InlineData(":pass")]
|
||||
[InlineData("user:")]
|
||||
public async Task HandleAsync_MalformedApiKey_DoesNotSucceed(String apiKey)
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
Request =
|
||||
{
|
||||
QueryString = new($"?apikey={apiKey}")
|
||||
}
|
||||
};
|
||||
|
||||
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
|
||||
var context = CreateContext(httpContext);
|
||||
|
||||
// Act
|
||||
await _handler.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasSucceeded, "HasSucceeded should be false for malformed api keys");
|
||||
_authenticationMock.Verify(a => a.Login(It.IsAny<String>(), It.IsAny<String>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_MissingCredentials_DoesNotSucceed()
|
||||
{
|
||||
// Arrange
|
||||
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||
var httpContext = new DefaultHttpContext();
|
||||
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class TorrentsControllerNzbTest
|
|||
|
||||
public TorrentsControllerNzbTest()
|
||||
{
|
||||
_torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, new TestSettings(), new TorrentRunnerState());
|
||||
_torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
|
||||
_loggerMock = new();
|
||||
_coordinatorMock = new();
|
||||
_controller = new(_loggerMock.Object, _torrentsMock.Object, null!, _coordinatorMock.Object);
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
|
|
@ -5,13 +5,12 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<AssemblyName>RdtClient.Web.Test</AssemblyName>
|
||||
<RootNamespace>RdtClient.Web.Test</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="SharpCompress" Version="[0.42.1]" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.Services;
|
||||
|
||||
namespace RdtClient.Web.Test;
|
||||
|
||||
internal sealed class TestSettings(DbSettings? settings = null) : ISettings
|
||||
{
|
||||
public DbSettings Current { get; } = settings ?? new();
|
||||
|
||||
public String DefaultSavePath
|
||||
{
|
||||
get
|
||||
{
|
||||
var downloadPath = Current.DownloadClient.MappedPath.TrimEnd('\\').TrimEnd('/');
|
||||
|
||||
return downloadPath + Path.DirectorySeparatorChar;
|
||||
}
|
||||
}
|
||||
|
||||
public IList<SettingProperty> GetAll()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public Task Update(IList<SettingProperty> settings)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Update(String settingId, Object? value)
|
||||
{
|
||||
SetValue(Current, settingId.Split(':'), 0, value);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void SetValue(Object target, IReadOnlyList<String> path, Int32 index, Object? value)
|
||||
{
|
||||
var property = target.GetType().GetProperty(path[index]) ?? throw new($"Unknown setting {String.Join(":", path)}");
|
||||
|
||||
if (index < path.Count - 1)
|
||||
{
|
||||
SetValue(property.GetValue(target)!, path, index + 1, value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
property.SetValue(target, null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||
var convertedValue = propertyType.IsEnum ? Enum.Parse(propertyType, value.ToString()!) : Convert.ChangeType(value, propertyType);
|
||||
property.SetValue(target, convertedValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,14 +6,14 @@ using RdtClient.Service.Services;
|
|||
namespace RdtClient.Web.Controllers;
|
||||
|
||||
[Route("Api/Authentication")]
|
||||
public class AuthController(Authentication authentication, ISettings settings) : Controller
|
||||
public class AuthController(Authentication authentication, Settings settings) : Controller
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[Route("IsLoggedIn")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> IsLoggedIn()
|
||||
{
|
||||
if (settings.Current.General.AuthenticationType == AuthenticationType.None)
|
||||
if (Settings.Get.General.AuthenticationType == AuthenticationType.None)
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ public class AuthController(Authentication authentication, ISettings settings) :
|
|||
return BadRequest();
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(settings.Current.Provider.ApiKey))
|
||||
if (!String.IsNullOrEmpty(Settings.Get.Provider.ApiKey))
|
||||
{
|
||||
return StatusCode(401);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.QBittorrent;
|
||||
using RdtClient.Service.Services;
|
||||
using Torrent = RdtClient.Data.Models.Data.Torrent;
|
||||
using RealDebridException = RDNET.RealDebridException;
|
||||
|
||||
namespace RdtClient.Web.Controllers;
|
||||
|
||||
|
|
@ -14,8 +14,7 @@ namespace RdtClient.Web.Controllers;
|
|||
[ApiController]
|
||||
[Route("api/v2")]
|
||||
[Route("qbittorrent/api/v2")]
|
||||
public class QBittorrentController(ILogger<QBittorrentController> logger, QBittorrent qBittorrent, IHttpClientFactory httpClientFactory, ISettings settings, Torrents torrents)
|
||||
: Controller
|
||||
public class QBittorrentController(ILogger<QBittorrentController> logger, QBittorrent qBittorrent, IHttpClientFactory httpClientFactory) : Controller
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[Route("/version/api")]
|
||||
|
|
@ -34,7 +33,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
|||
{
|
||||
logger.LogDebug($"Auth login");
|
||||
|
||||
if (settings.Current.General.AuthenticationType == AuthenticationType.None)
|
||||
if (Settings.Get.General.AuthenticationType == AuthenticationType.None)
|
||||
{
|
||||
return Content("Ok.", "text/plain");
|
||||
}
|
||||
|
|
@ -147,7 +146,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
|||
[HttpPost]
|
||||
public ActionResult<AppPreferences> AppDefaultSavePath()
|
||||
{
|
||||
var result = settings.DefaultSavePath;
|
||||
var result = Settings.AppDefaultSavePath;
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
|
@ -159,7 +158,10 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
|||
{
|
||||
var results = await qBittorrent.TorrentInfo();
|
||||
|
||||
results = results.Where(m => MatchesFilter(m, request.Filter)).ToList();
|
||||
if(!String.IsNullOrWhiteSpace(request.Filter))
|
||||
{
|
||||
results = results.Where(m => m.State != null && m.State.Equals(request.Filter, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(request.Category))
|
||||
{
|
||||
|
|
@ -191,8 +193,11 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
|||
public async Task<ActionResult<Int32>> TorrentsCount([FromQuery] QBTorrentsCountRequest request)
|
||||
{
|
||||
var results = await qBittorrent.TorrentInfo();
|
||||
results = results.Where(m => MatchesFilter(m, request.Filter)).ToList();
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(request.Filter))
|
||||
{
|
||||
results = results.Where(m => m.State != null && m.State.Equals(request.Filter, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
return results.Count;
|
||||
}
|
||||
|
||||
|
|
@ -369,28 +374,30 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
|||
|
||||
foreach (var url in urls)
|
||||
{
|
||||
Torrent? torrent;
|
||||
|
||||
if (url.StartsWith("magnet"))
|
||||
try
|
||||
{
|
||||
torrent = await qBittorrent.TorrentsAddMagnet(url.Trim(), request.Category, null);
|
||||
if (url.StartsWith("magnet"))
|
||||
{
|
||||
await qBittorrent.TorrentsAddMagnet(url.Trim(), request.Category, null);
|
||||
}
|
||||
else if (url.StartsWith("http"))
|
||||
{
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
var result = await httpClient.GetByteArrayAsync(url);
|
||||
await qBittorrent.TorrentsAddFile(result, request.Category, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest($"Invalid torrent link format {url}");
|
||||
}
|
||||
}
|
||||
else if (url.StartsWith("http"))
|
||||
catch (RealDebridException ex)
|
||||
{
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
var result = await httpClient.GetByteArrayAsync(url);
|
||||
torrent = await qBittorrent.TorrentsAddFile(result, request.Category, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest($"Invalid torrent link format {url}");
|
||||
}
|
||||
|
||||
var addResult = await WaitForTorrent(torrent.TorrentId);
|
||||
|
||||
if (!addResult)
|
||||
{
|
||||
return Ok("Fails.");
|
||||
// Infringing file.
|
||||
if (ex.ErrorCode == 35)
|
||||
{
|
||||
return Ok("Fails.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -411,14 +418,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
|||
await file.CopyToAsync(target);
|
||||
var fileBytes = target.ToArray();
|
||||
|
||||
var torrent = await qBittorrent.TorrentsAddFile(fileBytes, request.Category, request.Priority);
|
||||
|
||||
var addResult = await WaitForTorrent(torrent.TorrentId);
|
||||
|
||||
if (!addResult)
|
||||
{
|
||||
return Ok("Fails.");
|
||||
}
|
||||
await qBittorrent.TorrentsAddFile(fileBytes, request.Category, request.Priority);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -433,36 +433,10 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
|||
[Authorize(Policy = "AuthSetting")]
|
||||
[Route("torrents/filePrio")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> TorrentsFilePrio([FromQuery] QBTorrentsFilePrioRequest request)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(request.Hash) || String.IsNullOrWhiteSpace(request.Id) || request.Priority == null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var fileIds = request.Id
|
||||
.Split('|', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(value => Int32.TryParse(value, out var parsedValue) ? parsedValue : (Int32?)null)
|
||||
.Where(value => value.HasValue)
|
||||
.Select(value => value!.Value)
|
||||
.ToList();
|
||||
|
||||
if (fileIds.Count == 0)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
await qBittorrent.TorrentsFilePrio(request.Hash, fileIds, request.Priority.Value);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "AuthSetting")]
|
||||
[Route("torrents/filePrio")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> TorrentsFilePrioPost([FromForm] QBTorrentsFilePrioRequest request)
|
||||
public ActionResult TorrentsFilePrio()
|
||||
{
|
||||
return await TorrentsFilePrio(request);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "AuthSetting")]
|
||||
|
|
@ -619,7 +593,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
|||
[HttpGet]
|
||||
public ActionResult TransferInfo()
|
||||
{
|
||||
return Ok(qBittorrent.TransferInfo());
|
||||
return Ok(QBittorrent.TransferInfo());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "AuthSetting")]
|
||||
|
|
@ -652,56 +626,6 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
|||
{
|
||||
return await TorrentsTrackers(request);
|
||||
}
|
||||
|
||||
private static Boolean MatchesFilter(TorrentInfo torrent, String? filter)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(filter) || filter.Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return filter.ToLowerInvariant() switch
|
||||
{
|
||||
"downloading" => torrent.Progress < 1f && !String.Equals(torrent.State, "error", StringComparison.OrdinalIgnoreCase),
|
||||
"completed" => torrent.Progress >= 1f || torrent.CompletionOn.HasValue,
|
||||
"paused" => torrent.State?.StartsWith("paused", StringComparison.OrdinalIgnoreCase) == true,
|
||||
"active" => (torrent.Dlspeed ?? 0) > 0 || (torrent.Upspeed ?? 0) > 0,
|
||||
"inactive" => (torrent.Dlspeed ?? 0) <= 0 && (torrent.Upspeed ?? 0) <= 0,
|
||||
"resumed" => torrent.State?.StartsWith("paused", StringComparison.OrdinalIgnoreCase) != true &&
|
||||
!String.Equals(torrent.State, "error", StringComparison.OrdinalIgnoreCase),
|
||||
"stalled" => String.Equals(torrent.State, "stalledDL", StringComparison.OrdinalIgnoreCase) ||
|
||||
String.Equals(torrent.State, "stalledUP", StringComparison.OrdinalIgnoreCase),
|
||||
"stalled_uploading" => String.Equals(torrent.State, "stalledUP", StringComparison.OrdinalIgnoreCase),
|
||||
"stalled_downloading" => String.Equals(torrent.State, "stalledDL", StringComparison.OrdinalIgnoreCase),
|
||||
"errored" => String.Equals(torrent.State, "error", StringComparison.OrdinalIgnoreCase),
|
||||
_ => String.Equals(torrent.State, filter, StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Boolean> WaitForTorrent(Guid torrentId)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var torrent = await torrents.GetById(torrentId);
|
||||
|
||||
if (torrent == null)
|
||||
{
|
||||
throw new($"Failed to add torrent: Not Found");
|
||||
}
|
||||
|
||||
if (torrent.RdStatus == TorrentStatus.Error || torrent.Error != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (torrent.RdStatus != TorrentStatus.Queued)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class QBAuthLoginRequest
|
||||
|
|
@ -717,6 +641,7 @@ public class QBTorrentsInfoRequest
|
|||
public String? Hashes { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class QBTorrentsCountRequest
|
||||
{
|
||||
public String? Filter { get; set; }
|
||||
|
|
@ -727,13 +652,6 @@ public class QBTorrentsHashRequest
|
|||
public String? Hash { get; set; }
|
||||
}
|
||||
|
||||
public class QBTorrentsFilePrioRequest
|
||||
{
|
||||
public String? Hash { get; set; }
|
||||
public String? Id { get; set; }
|
||||
public Int32? Priority { get; set; }
|
||||
}
|
||||
|
||||
public class QBTorrentsDeleteRequest
|
||||
{
|
||||
public String? Hashes { get; set; }
|
||||
|
|
|
|||
|
|
@ -56,7 +56,22 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
|
|||
|
||||
if (name == "delete")
|
||||
{
|
||||
return await DeleteDownloads();
|
||||
var value = GetParam("value");
|
||||
|
||||
if (String.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return BadRequest(new SabnzbdResponse
|
||||
{
|
||||
Error = "No value specified for delete operation"
|
||||
});
|
||||
}
|
||||
|
||||
await sabnzbd.Delete(value ?? "");
|
||||
|
||||
return Ok(new SabnzbdResponse
|
||||
{
|
||||
Status = true
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new SabnzbdResponse
|
||||
|
|
@ -70,12 +85,6 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
|
|||
public async Task<ActionResult> History()
|
||||
{
|
||||
logger.LogDebug("Sabnzbd mode: history");
|
||||
var name = GetParam("name");
|
||||
|
||||
if (name == "delete")
|
||||
{
|
||||
return await DeleteDownloads();
|
||||
}
|
||||
|
||||
return Ok(new SabnzbdResponse
|
||||
{
|
||||
|
|
@ -174,31 +183,6 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
|
|||
});
|
||||
}
|
||||
|
||||
private async Task<ActionResult> DeleteDownloads()
|
||||
{
|
||||
var value = GetParam("value");
|
||||
|
||||
if (String.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return BadRequest(new SabnzbdResponse
|
||||
{
|
||||
Error = "No value specified for delete operation"
|
||||
});
|
||||
}
|
||||
|
||||
var deleteFiles = IsTrue(GetParam("del_files"));
|
||||
|
||||
foreach (var hash in value.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
await sabnzbd.Delete(hash, deleteFiles);
|
||||
}
|
||||
|
||||
return Ok(new SabnzbdResponse
|
||||
{
|
||||
Status = true
|
||||
});
|
||||
}
|
||||
|
||||
private String? GetParam(String name)
|
||||
{
|
||||
var value = Request.Query[name].ToString();
|
||||
|
|
@ -210,9 +194,4 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
|
|||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Boolean IsTrue(String? value)
|
||||
{
|
||||
return value is "1" || Boolean.TryParse(value, out var result) && result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Reflection;
|
|||
using Aria2NET;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.Helpers;
|
||||
|
|
@ -14,13 +15,13 @@ namespace RdtClient.Web.Controllers;
|
|||
|
||||
[Authorize(Policy = "AuthSetting")]
|
||||
[Route("Api/Settings")]
|
||||
public class SettingsController(ISettings settings, Torrents torrents) : Controller
|
||||
public class SettingsController(Settings settings, Torrents torrents) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
public ActionResult Get()
|
||||
{
|
||||
var result = settings.GetAll();
|
||||
var result = SettingData.GetAll();
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
|
@ -94,7 +95,7 @@ public class SettingsController(ISettings settings, Torrents torrents) : Control
|
|||
[Route("TestDownloadSpeed")]
|
||||
public async Task<ActionResult> TestDownloadSpeed(CancellationToken cancellationToken)
|
||||
{
|
||||
var downloadPath = settings.Current.DownloadClient.DownloadPath;
|
||||
var downloadPath = Settings.Get.DownloadClient.DownloadPath;
|
||||
|
||||
var testFilePath = Path.Combine(downloadPath, "testDefault.rar");
|
||||
|
||||
|
|
@ -105,7 +106,7 @@ public class SettingsController(ISettings settings, Torrents torrents) : Control
|
|||
Link = "https://34.download.real-debrid.com/speedtest/testDefault.rar",
|
||||
Torrent = new()
|
||||
{
|
||||
DownloadClient = settings.Current.DownloadClient.Client == DownloadClient.Symlink ? DownloadClient.Bezzad : settings.Current.DownloadClient.Client,
|
||||
DownloadClient = Settings.Get.DownloadClient.Client == DownloadClient.Symlink ? DownloadClient.Bezzad : Settings.Get.DownloadClient.Client,
|
||||
RdName = "testDefault.rar"
|
||||
}
|
||||
};
|
||||
|
|
@ -130,7 +131,7 @@ public class SettingsController(ISettings settings, Torrents torrents) : Control
|
|||
|
||||
if (downloadClient.Downloader is Aria2cDownloader aria2Downloader)
|
||||
{
|
||||
var aria2NetClient = new Aria2NetClient(settings.Current.DownloadClient.Aria2cUrl, settings.Current.DownloadClient.Aria2cSecret, httpClient, 1);
|
||||
var aria2NetClient = new Aria2NetClient(Settings.Get.DownloadClient.Aria2cUrl, Settings.Get.DownloadClient.Aria2cSecret, httpClient, 1);
|
||||
|
||||
var allDownloads = await aria2NetClient.TellAllAsync(cancellationToken);
|
||||
|
||||
|
|
@ -154,7 +155,7 @@ public class SettingsController(ISettings settings, Torrents torrents) : Control
|
|||
[Route("TestWriteSpeed")]
|
||||
public async Task<ActionResult> TestWriteSpeed()
|
||||
{
|
||||
var downloadPath = settings.Current.DownloadClient.DownloadPath;
|
||||
var downloadPath = Settings.Get.DownloadClient.DownloadPath;
|
||||
|
||||
var testFilePath = Path.Combine(downloadPath, "test.tmp");
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,72 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
{
|
||||
var results = await torrents.Get();
|
||||
|
||||
var torrentDtos = results.Select(torrent => TorrentDtoMapper.ToListDto(torrent, torrents.GetDownloadStats))
|
||||
var torrentDtos = results.Select(torrent => new TorrentDto
|
||||
{
|
||||
TorrentId = torrent.TorrentId,
|
||||
Hash = torrent.Hash,
|
||||
Category = torrent.Category,
|
||||
DownloadAction = torrent.DownloadAction,
|
||||
FinishedAction = torrent.FinishedAction,
|
||||
FinishedActionDelay = torrent.FinishedActionDelay,
|
||||
HostDownloadAction = torrent.HostDownloadAction,
|
||||
DownloadMinSize = torrent.DownloadMinSize,
|
||||
IncludeRegex = torrent.IncludeRegex,
|
||||
ExcludeRegex = torrent.ExcludeRegex,
|
||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||
DownloadClient = torrent.DownloadClient,
|
||||
Added = torrent.Added,
|
||||
FilesSelected = torrent.FilesSelected,
|
||||
Completed = torrent.Completed,
|
||||
Type = torrent.Type,
|
||||
IsFile = torrent.IsFile,
|
||||
Priority = torrent.Priority,
|
||||
RetryCount = torrent.RetryCount,
|
||||
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
|
||||
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||
DeleteOnError = torrent.DeleteOnError,
|
||||
Lifetime = torrent.Lifetime,
|
||||
Error = torrent.Error,
|
||||
RdId = torrent.RdId,
|
||||
RdName = torrent.RdName,
|
||||
RdSize = torrent.RdSize,
|
||||
RdHost = torrent.RdHost,
|
||||
RdSplit = torrent.RdSplit,
|
||||
RdProgress = torrent.RdProgress,
|
||||
RdStatus = torrent.RdStatus,
|
||||
RdStatusRaw = torrent.RdStatusRaw,
|
||||
RdAdded = torrent.RdAdded,
|
||||
RdEnded = torrent.RdEnded,
|
||||
RdSpeed = torrent.RdSpeed,
|
||||
RdSeeders = torrent.RdSeeders,
|
||||
Files = torrent.Files,
|
||||
Downloads = torrent.Downloads.Select(download =>
|
||||
{
|
||||
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
|
||||
|
||||
return new DownloadDto
|
||||
{
|
||||
DownloadId = download.DownloadId,
|
||||
TorrentId = download.TorrentId,
|
||||
Path = download.Path,
|
||||
Link = download.Link,
|
||||
Added = download.Added,
|
||||
DownloadQueued = download.DownloadQueued,
|
||||
DownloadStarted = download.DownloadStarted,
|
||||
DownloadFinished = download.DownloadFinished,
|
||||
UnpackingQueued = download.UnpackingQueued,
|
||||
UnpackingStarted = download.UnpackingStarted,
|
||||
UnpackingFinished = download.UnpackingFinished,
|
||||
Completed = download.Completed,
|
||||
RetryCount = download.RetryCount,
|
||||
Error = download.Error,
|
||||
BytesTotal = bytesTotal,
|
||||
BytesDone = bytesDone,
|
||||
Speed = speed
|
||||
};
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Ok(torrentDtos);
|
||||
|
|
@ -43,7 +108,72 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
file.Torrent = null;
|
||||
}
|
||||
|
||||
var torrentDto = TorrentDtoMapper.ToDetailDto(torrent, torrents.GetDownloadStats);
|
||||
var torrentDto = new TorrentDto
|
||||
{
|
||||
TorrentId = torrent!.TorrentId,
|
||||
Hash = torrent.Hash,
|
||||
Category = torrent.Category,
|
||||
DownloadAction = torrent.DownloadAction,
|
||||
FinishedAction = torrent.FinishedAction,
|
||||
FinishedActionDelay = torrent.FinishedActionDelay,
|
||||
HostDownloadAction = torrent.HostDownloadAction,
|
||||
DownloadMinSize = torrent.DownloadMinSize,
|
||||
IncludeRegex = torrent.IncludeRegex,
|
||||
ExcludeRegex = torrent.ExcludeRegex,
|
||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||
DownloadClient = torrent.DownloadClient,
|
||||
Added = torrent.Added,
|
||||
FilesSelected = torrent.FilesSelected,
|
||||
Completed = torrent.Completed,
|
||||
Type = torrent.Type,
|
||||
IsFile = torrent.IsFile,
|
||||
Priority = torrent.Priority,
|
||||
RetryCount = torrent.RetryCount,
|
||||
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
|
||||
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||
DeleteOnError = torrent.DeleteOnError,
|
||||
Lifetime = torrent.Lifetime,
|
||||
Error = torrent.Error,
|
||||
RdId = torrent.RdId,
|
||||
RdName = torrent.RdName,
|
||||
RdSize = torrent.RdSize,
|
||||
RdHost = torrent.RdHost,
|
||||
RdSplit = torrent.RdSplit,
|
||||
RdProgress = torrent.RdProgress,
|
||||
RdStatus = torrent.RdStatus,
|
||||
RdStatusRaw = torrent.RdStatusRaw,
|
||||
RdAdded = torrent.RdAdded,
|
||||
RdEnded = torrent.RdEnded,
|
||||
RdSpeed = torrent.RdSpeed,
|
||||
RdSeeders = torrent.RdSeeders,
|
||||
Files = torrent.Files,
|
||||
Downloads = torrent.Downloads.Select(download =>
|
||||
{
|
||||
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
|
||||
|
||||
return new DownloadDto
|
||||
{
|
||||
DownloadId = download.DownloadId,
|
||||
TorrentId = download.TorrentId,
|
||||
Path = download.Path,
|
||||
Link = download.Link,
|
||||
Added = download.Added,
|
||||
DownloadQueued = download.DownloadQueued,
|
||||
DownloadStarted = download.DownloadStarted,
|
||||
DownloadFinished = download.DownloadFinished,
|
||||
UnpackingQueued = download.UnpackingQueued,
|
||||
UnpackingStarted = download.UnpackingStarted,
|
||||
UnpackingFinished = download.UnpackingFinished,
|
||||
Completed = download.Completed,
|
||||
RetryCount = download.RetryCount,
|
||||
Error = download.Error,
|
||||
BytesTotal = bytesTotal,
|
||||
BytesDone = bytesDone,
|
||||
Speed = speed
|
||||
};
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
return Ok(torrentDto);
|
||||
}
|
||||
|
|
@ -110,7 +240,13 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
|
||||
logger.LogDebug($"Add file");
|
||||
|
||||
var bytes = await ReadFormFileBytes(file);
|
||||
var fileStream = file.OpenReadStream();
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
var bytes = memoryStream.ToArray();
|
||||
|
||||
await torrents.AddFileToDebridQueue(bytes, formData.Torrent);
|
||||
|
||||
|
|
@ -166,7 +302,13 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
formData.Torrent.RdName = file.FileName;
|
||||
}
|
||||
|
||||
var bytes = await ReadFormFileBytes(file);
|
||||
var fileStream = file.OpenReadStream();
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
var bytes = memoryStream.ToArray();
|
||||
|
||||
await torrents.AddNzbFileToDebridQueue(bytes, file.FileName, formData.Torrent);
|
||||
|
||||
|
|
@ -208,7 +350,13 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
return BadRequest("Invalid torrent file");
|
||||
}
|
||||
|
||||
var bytes = await ReadFormFileBytes(file);
|
||||
var fileStream = file.OpenReadStream();
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
var bytes = memoryStream.ToArray();
|
||||
|
||||
var torrent = await MonoTorrent.Torrent.LoadAsync(bytes);
|
||||
|
||||
|
|
@ -313,7 +461,13 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
}
|
||||
else if (file != null)
|
||||
{
|
||||
var bytes = await ReadFormFileBytes(file);
|
||||
var fileStream = file.OpenReadStream();
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
var bytes = memoryStream.ToArray();
|
||||
|
||||
var torrent = await MonoTorrent.Torrent.LoadAsync(bytes);
|
||||
|
||||
|
|
@ -328,32 +482,36 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
|
||||
if (!String.IsNullOrWhiteSpace(request.IncludeRegex))
|
||||
{
|
||||
var includeRegex = BuildRegex(request.IncludeRegex, out includeError);
|
||||
|
||||
if (includeRegex != null)
|
||||
foreach (var availableFile in availableFiles)
|
||||
{
|
||||
foreach (var availableFile in availableFiles)
|
||||
try
|
||||
{
|
||||
if (includeRegex.IsMatch(availableFile.Filename))
|
||||
if (Regex.IsMatch(availableFile.Filename, request.IncludeRegex))
|
||||
{
|
||||
selectedFiles.Add(availableFile);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
includeError = ex.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!String.IsNullOrWhiteSpace(request.ExcludeRegex))
|
||||
{
|
||||
var excludeRegex = BuildRegex(request.ExcludeRegex, out excludeError);
|
||||
|
||||
if (excludeRegex != null)
|
||||
foreach (var availableFile in availableFiles)
|
||||
{
|
||||
foreach (var availableFile in availableFiles)
|
||||
try
|
||||
{
|
||||
if (!excludeRegex.IsMatch(availableFile.Filename))
|
||||
if (!Regex.IsMatch(availableFile.Filename, request.ExcludeRegex))
|
||||
{
|
||||
selectedFiles.Add(availableFile);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
excludeError = ex.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -368,32 +526,6 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
|
|||
selectedFiles
|
||||
});
|
||||
}
|
||||
|
||||
private static Regex? BuildRegex(String pattern, out String error)
|
||||
{
|
||||
try
|
||||
{
|
||||
error = "";
|
||||
|
||||
return new(pattern, RegexOptions.Compiled);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Byte[]> ReadFormFileBytes(IFormFile file)
|
||||
{
|
||||
await using var fileStream = file.OpenReadStream();
|
||||
await using var memoryStream = file.Length is > 0 and <= Int32.MaxValue ? new((Int32)file.Length) : new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(memoryStream);
|
||||
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public class TorrentControllerUploadFileRequest
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8">
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.5" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue