Merge pull request #2 from rogerfar/master

Merge from parent
This commit is contained in:
Shawn 2023-03-13 07:50:43 -04:00 committed by GitHub
commit 6d65de6f5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
195 changed files with 38825 additions and 16797 deletions

16
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,16 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**What version are you using?**
**Wat OS are you running?**
**Are you using Docker or as a service?**
**Please attach a log file here with the log setting set to debug**

39
.gitignore vendored
View file

@ -1,44 +1,7 @@
### Angular ###
# compiled output
client/dist/
client/tmp/
client/app/**/*.js
client/app/**/*.js.map
# dependencies
client/node_modules/
client/bower_components/
# IDEs and editors
client/.idea/
# misc
client/.sass-cache/
client/connect.lock/
client/coverage/
client/libpeerconnection.log/
client/npm-debug.log
client/testem.log
client/typings/
# e2e
client/e2e/*.js
client/e2e/*.map
#System Files
**/.DS_Store/ **/.DS_Store/
### DotnetCore ###
# .NET Core build folders
server/**/bin
server/**/obj
server/**/*.user
server/**/.vs
server/RdtClient.Web/wwwroot/
build/ build/
publish/ publish/
server/Downloader/
node_modules/ node_modules/
RealDebridClient.zip RealDebridClient.zip
server/RdtClient.Web/appsettings.Development.json

View file

@ -4,6 +4,260 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.22] - 2023-03-11
### Added
- Add support for Premiumize
## [2.0.21] - 2023-03-09
### Changed
- Fixed docker build by pinning the .NET version to LTS.
## [2.0.20] - 2023-03-08
### Added
- Add support for multi-level unpacking.
- Add settings to specify the Error and Processed paths for the watch folders.
- Option to disable authentication completely.
### Changed
- Removed the Simple Downloader and replaced it with https://github.com/bezzad/Downloader as the Internal Downloader.
- Fixed setting the Base Href for sub folder hosting.
## [2.0.19] - 2022-10-18
### Changed
- Changed the AllDebrid provider to use HTTPS instead of HTTP.
## [2.0.18] - 2022-10-18
### Added
- Added the option to bulk delete torrents, thanks kanazaca!
- Added option to remove the torrent only from the client after downloads are completed.
- Added option to change the category of an existing torrent.
- Added option to not download files to the host.
### Changed
- If a watched file gives an error when adding, move it to an error folder.
## [2.0.17] - 2022-05-24
### Changed
- Fixed issue with some settings not saving.
## [2.0.16] - 2022-05-24
### Changed
- Fixed MacOS pre-fill on the login screen.
## [2.0.15] - 2022-05-15
### Changed
- Remove settings for Finish Action and Category for the qBittorrent integration, these should always be set to None and the category comes from the integration.
## [2.0.14] - 2022-05-14
### Changed
- Fixed Windows Service issue
- For service users: the appsettings.json is slightly changed: HostUrl is now Port. Important if you used a non standard (6500) port.
## [2.0.13] - 2022-05-14
### Changed
- Rewrote the settings storage. Added more settings to set defaults for importing.
- Fixed filtering of torrents when a category is passed to the TorrentsInfo endpoint, fixing Radarr/Sonarr integrations.
### Added
- Added settings to control the timeout and polling interval to RealDebrid/AllDebrid.
## [2.0.12] - 2022-03-20
### Changed
- Fixed the AllDebrid client.
## [2.0.11] - 2022-03-19
### Changed
- Fixed the "Progress" for the AllDebrid client, thanks @23doors.
## [2.0.10] - 2022-03-19
### Added
- When changing the download speed setting for the simple downloader it will apply the setting to active downloads.
- Add running of external applications when the torrent is finished.
### Changed
- Fixed deserialization of the availability check for RealDebrid.
- Fixed the simple downloader download limiter.
## [2.0.9] - 2022-03-12
### Changed
- Updated packages, added logging to the RD and AD providers when serialization fails.
## [2.0.8] - 2022-02-28
### Changed
- Fixed issue with AllDebrid sometimes returning NULL links.
## [2.0.7] - 2022-02-06
### Changed
- Added setting to set the category when a torrent is imported from RealDebrid or other provider.
## [2.0.6] - 2022-02-06
### Added
- Added setting to automatically delete torrents in the state of error after a certain amount of time.
- Added lifetime setting to automatically expire torrents after a certain amount of time.
## [2.0.5] - 2022-01-11
### Changed
- Updated AllDebrid provider to fix issue with ID's not being a number.
## [2.0.4] - 2022-01-08
### Changed
- Fixed bug where the QBittorrent API didn't report the error state correctly when an error ocurred in the debrid provider.
## [2.0.3] - 2022-01-02
### Changed
- Fixed automatic adding of AllDebrid torrents.
### Added
- Added update notification.
## [2.0.2] - 2021-11-24
### Changed
- Fixed update timer for providers.
## [2.0.1] - 2021-11-24
### Changed
- Fixed potential issue with the Real-Debrid mapper.
- Fixed serialization issue for AllDebrid.
## [2.0.0] - 2021-11-21
### Changed
- Update projects to .NET6 and Angular 13.
### Added
- Added setting to automatically import torrents from RealDebrid / AllDebrid.
- Added setting to automatically delete torrents from RealDebridClient when they have been removed from RealDebrid / AllDebrid.
## [1.9.8] - 2021-10-30
### Added
- Add speed limit setting on the Simple downloader.
## [1.9.7] - 2021-10-30
### Added
- Add AllDebrid support.
## [1.9.6] - 2021-10-30
### Added
- Improved handling of errors on the torrent itself
- Added retry counts when adding torrents
- Added retry counts on the Sonarr/Radarr settings page
## [1.9.5] - 2021-10-28
### Changed
- Fixed issues with the simple downloader.
- Fixed issue with retrying downloads.
### Added
- Added torrent uploading status from Real Debrid.
- Restored removing of torrents when deleted from Real Debrid.
## [1.9.4] - 2021-10-28
### Changed
- Fixed issues retrying torrents after multiple failed downloads in large torrents.
## [1.9.3] - 2021-10-27
### Changed
- Fixed issue with the unpack queue.
## [1.9.2] - 2021-10-27
### Changed
- Fixed issue where not the correct torrent was used for a download.
## [1.9.1] - 2021-10-27
### Added
- Added automatic torrent retrying when RealDebrid reports an error on the torrent.
### Changed
- Changed how Aria2 is polled, this will result in less RPC calls for a large amount of torrents.
- Add more and better logging for the torrent runner in Debug.
- Add Aria2 checks to see if links get added properly.
- Update Aria2.NET and RD.NET, added automatic retrying in case of server failures.
## [1.9.0] - 2021-10-24
### Added
- Add priorty for torrents. You can set the priority when adding a new torrent. When added from Sonarr/Radarr it will assume no priority.
- Added support for the Sonarr/Radarr/qBittorrent setPrio command. It will set the priority of a torrent to 1.
- Added support for the Sonarr/Radarr/qBittorrent pause and resume commands. This only works with the Aria2 downloader.
### Changed
- Fixed issues when downloads get deleted when they are still being checked.
- Improved timings and retrying of adding of downloads to Aria2. Will now try 5 times before failing.
## [1.8.9] - 2021-10-23
### Changed
- Add delays between adding downloads to Aria2 to avoid Aria2 going down when adding a large amount of downloads.
## [1.8.8] - 2021-10-21
### Changed
- Fixed starting downloads when RealDebrid reports ghost links in torrents.
## [1.8.7] - 2021-10-11
### Added
- Add Aria2 test connection button.
- Add full torrent retry mechanism, by default it will now retry the torrent 2 times when a download fails for more than 3 times.
### Changed
- Improved Aria2 download handling.
## [1.8.6] - 2021-10-09
### Added
- Experimental support for a Aria2 download client. Check the readme for usage.
### Changed
- Fixed potential error when sonarr is querying and the torrent isn't added to RealDebrid yet.
- Fixed interface randomly stop updating.
- Upgrade dependencies.
## [1.8.5] - 2021-10-07
### Changed
- Fixed issue where deleting a torrent could error out.
## [1.8.4] - 2021-08-05
### Changed
- Changed the default timeout for Real-Debrid communication to 10 seconds instead of 100 seconds.
## [1.8.3] - 2021-08-05
### Changed
- Fixed potential issue with duplicates categories.
## [1.8.2] - 2021-08-02
### Changed
- Fixed issue with starting downloads.
## [1.8.1] - 2021-07-31
### Changed
- Fixed issue where downloads were hanging the full interface.
## [1.8.0] - 2021-07-18
### Added
- Fixed support for categories. They are now saved persistently in the database.
- Added new "settings" page.
- Added new "add new" page. Added more options when manually adding a torrent, including the ability to manually select files.
- Added dedicated torrent pages with more information when selecting a torrent on the main page.
- Add ability to retry individual downloads.
### Changed
- Fixed enter key on the login and setup screen.
- Fixed an issue with selecting files and getting the links. This process could take a long time and would hang the client while waiting for a response.
### Removed
- Removed the retry and delete button from the main page and moved them to the torrent page.
- Removed the ability to retry all downloads.
## [1.7.8] - 2021-06-17
### Changed
- Fixed issue for real this time with a broken response from RDT when the available files returns in a format different than normal.
## [1.7.7] - 2021-06-09
### Changed
- Fixed some issues with download error handling.
- Fixed issue where files aren't always properly selected.
## [1.7.6] - 2021-06-05
### Changed
- Fixed build for Raspberry PI.
## [1.7.5] - 2021-06-05
### Changed
- Reduced the frequency of database reads and writes by adding caches for settings and torrents.
- Updated dependency of RD.NET to improve serialization error reporting.
- Changed how the base href is determined to support path proxies.
- Fixed issue with handling of renamed torrents.
## [1.7.4] - 2021-04-22 ## [1.7.4] - 2021-04-22
### Changed ### Changed
- Changed how the docker is built. - Changed how the docker is built.

View file

@ -1,5 +1,5 @@
# Stage 1 - Build the frontend # Stage 1 - Build the frontend
FROM node:15.5-buster AS node-build-env FROM node:16-buster AS node-build-env
ARG TARGETPLATFORM ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
ARG BUILDPLATFORM ARG BUILDPLATFORM
@ -9,17 +9,17 @@ RUN mkdir /appclient
WORKDIR /appclient WORKDIR /appclient
RUN \ RUN \
echo "**** Cloning Source Code ****" && \ echo "**** Cloning Source Code ****" && \
git clone https://github.com/rogerfar/rdt-client.git . && \ git clone https://github.com/rogerfar/rdt-client.git . && \
cd client && \ cd client && \
echo "**** Building Code ****" && \ echo "**** Building Code ****" && \
npm ci && \ npm ci && \
npx ng build --prod --output-path=out npx ng build --output-path=out
RUN ls -FCla /appclient/root RUN ls -FCla /appclient/root
# Stage 2 - Build the backend # Stage 2 - Build the backend
FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim-amd64 AS dotnet-build-env FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim-amd64 AS dotnet-build-env
ARG TARGETPLATFORM ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
ARG BUILDPLATFORM ARG BUILDPLATFORM
@ -29,15 +29,18 @@ RUN mkdir /appserver
WORKDIR /appserver WORKDIR /appserver
RUN \ RUN \
echo "**** Cloning Source Code ****" && \ echo "**** Cloning Source Code ****" && \
git clone https://github.com/rogerfar/rdt-client.git . && \ git clone https://github.com/rogerfar/rdt-client.git . && \
echo "**** Building Source Code for $TARGETPLATFORM on $BUILDPLATFORM ****" && \ echo "**** Building Source Code for $TARGETPLATFORM on $BUILDPLATFORM ****" && \
cd server && \ cd server && \
if [ "$TARGETPLATFORM" = "linux/arm/v7" -o "$TARGETPLATFORM" = "linux/arm64" ] ; then \ if [ "$TARGETPLATFORM" = "linux/arm/v7" ] ; then \
echo "**** Building $TARGETPLATFORM arm version" && \ echo "**** Building $TARGETPLATFORM arm v7 version" && \
dotnet restore -r linux-arm RdtClient.sln && dotnet publish -r linux-arm -c Release -o out ; \ dotnet restore -r linux-arm RdtClient.sln && dotnet publish -r linux-arm -c Release -o out ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v8"] ; then \
echo "**** Building $TARGETPLATFORM arm v8 version" && \
dotnet restore -r linux-arm64 RdtClient.sln && dotnet publish -r linux-arm64 -c Release -o out ; \
else \ else \
echo "**** Building $TARGETPLATFORM x86 version" && \ echo "**** Building $TARGETPLATFORM x64 version" && \
dotnet restore RdtClient.sln && dotnet publish -c Release -o out ; \ dotnet restore RdtClient.sln && dotnet publish -c Release -o out ; \
fi fi
@ -61,16 +64,16 @@ ENV RDTCLIENT_BRANCH="main"
RUN \ RUN \
mkdir -p /data/downloads /data/db || true && \ mkdir -p /data/downloads /data/db || true && \
echo "**** Updating package information ****" && \ echo "**** Updating package information ****" && \
apt-get update -y -qq && \ apt-get update -y -qq && \
echo "**** Install pre-reqs ****" && \ echo "**** Install pre-reqs ****" && \
apt-get install -y -qq wget dos2unix && \ apt-get install -y -qq wget dos2unix && \
apt-get install -y libc6 libgcc1 libgssapi-krb5-2 libssl1.1 libstdc++6 zlib1g libicu66 && \ apt-get install -y libc6 libgcc1 libgssapi-krb5-2 libssl1.1 libstdc++6 zlib1g libicu66 && \
echo "**** Installing dotnet ****" && \ echo "**** Installing dotnet ****" && \
wget -q https://dot.net/v1/dotnet-install.sh && \ wget -q https://dot.net/v1/dotnet-install.sh && \
chmod +x ./dotnet-install.sh && \ chmod +x ./dotnet-install.sh && \
bash ./dotnet-install.sh -c Current --runtime dotnet --install-dir /usr/share/dotnet && \ bash ./dotnet-install.sh -c LTS -v latest --runtime dotnet --install-dir /usr/share/dotnet && \
bash ./dotnet-install.sh -c Current --runtime aspnetcore --install-dir /usr/share/dotnet && \ bash ./dotnet-install.sh -c LTS -v latest --runtime aspnetcore --install-dir /usr/share/dotnet && \
echo "**** Cleaning image ****" && \ echo "**** Cleaning image ****" && \
apt-get -y -qq -o Dpkg::Use-Pty=0 clean && apt-get -y -qq -o Dpkg::Use-Pty=0 purge && \ apt-get -y -qq -o Dpkg::Use-Pty=0 clean && apt-get -y -qq -o Dpkg::Use-Pty=0 purge && \
echo "**** Setting permissions ****" && \ echo "**** Setting permissions ****" && \

View file

@ -1,16 +1,22 @@
# Real-Debrid Torrent Client # Real-Debrid Torrent Client
This is a web interface to manage your torrents on Real-Debrid. It supports the following features: This is a web interface to manage your torrents on Real-Debrid, AllDebrid or Premiumize. It supports the following features:
- Add new torrents through magnets or files - Add new torrents through magnets or files
- Download all files from Real Debrid to your local machine automatically - Download all files from Real-Debrid, AllDebrid or Premiumize to your local machine automatically
- Unpack all files when finished downloading - Unpack all files when finished downloading
- Implements a fake qBittorrent API so you can hook up other applications like Sonarr or Couchpotato. - Implements a fake qBittorrent API so you can hook up other applications like Sonarr, Radarr or Couchpotato.
- Built with Angular 11 and .NET 5 - Built with Angular 15 and .NET 6
**You will need a Premium service at Real-Debrid!** **You will need a Premium service at Real-Debrid, AllDebrid or Premiumize!**
[Click here to sign up (referal link so I can get a few free premium days).](https://real-debrid.com/?id=1348683) [Click here to sign up for Real-Debrid.](https://real-debrid.com/?id=1348683)
[Click here to sign up for AllDebrid.](https://alldebrid.com/?uid=2v91l)
[Click here to sign up for Premiumize.](https://www.premiumize.me/)
<sub>(referal links so I can get a few free premium days)</sub>
## Docker Setup ## Docker Setup
@ -62,15 +68,65 @@ docker-compose up -d
Replace the paths in `volumes` as in the above step. Replace the paths in `volumes` as in the above step.
## Run as a Service
Instead of running in Docker you can install it as a service in Windows or Linux.
## Windows Service ## Windows Service
Instead of running in Docker you can install it as a service in Windows or Linux (not tested). 1. Make sure you have the ASP.NET Core Runtime 6 installed: [https://dotnet.microsoft.com/download/dotnet/6.0](https://dotnet.microsoft.com/download/dotnet/6.0)
1. Get the latest zip file from the Releases page and extract it to your host. 1. Get the latest zip file from the Releases page and extract it to your host.
1. Open the `appsettings.json` file and replace the `LogLevel` `Path` to a path on your host. 1. Open the `appsettings.json` file and replace the `LogLevel` `Path` to a path on your host.
1. In `appsettings.json` replace the `Database` `Path` to a path on your host. 1. In `appsettings.json` replace the `Database` `Path` to a path on your host.
1. When using Windows paths, make sure to escape the slashes. For example: `D:\\RdtClient\\db\\rdtclient.db` 1. When using Windows paths, make sure to escape the slashes. For example: `D:\\RdtClient\\db\\rdtclient.db`
## Linux Service
Instead of running in Docker you can install it as a service in Linux.
1. Install .NET: [https://docs.microsoft.com/en-us/dotnet/core/install/linux](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
Ubuntu 20.04 example:
```wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb```
```sudo dpkg -i packages-microsoft-prod.deb```
```rm packages-microsoft-prod.deb```
```sudo apt-get update && sudo apt-get install -y dotnet-sdk-6.0```
2. Get latest archive from [releases](https://github.com/rogerfar/rdt-client/releases):
```wget <zip_url>```
3. Extract to path of your choice (~/rtdc in this example):
```unzip RealDebridClient.zip -d ~/rdtc && cd ~/rdtc```
4. In appsettings.json replace the Database Path to a path on your host. Any directories in path must already exist. Or just remove "/data/db/" for ease.
5. Test rdt client runs ok:
```dotnet RdtClient.Web.dll```
navigate to http://<ipaddress>:6500, if all is good then we'll create a service
6. Create a service (systemd in this example):
```sudo nano /etc/systemd/system/rdtc.service```
paste in this service file content and edit path of your directory:
```
[Unit]
Description=RdtClient Service
[Service]
WorkingDirectory=/home/<username>/rdtc
ExecStart=/usr/bin/dotnet RdtClient.Web.dll
SyslogIdentifier=RdtClient
User=<username>
[Install]
WantedBy=multi-user.target
```
7. enable and start the service:
```sudo systemctl daemon-reload```
```sudo systemctl enable rdtc```
```sudo systemctl start rdtc```
## Setup ## Setup
### First Login ### First Login
@ -86,24 +142,27 @@ Instead of running in Docker you can install it as a service in Windows or Linux
Currently there 2 available download clients: Currently there 2 available download clients:
#### Simple Downloader #### Internal Downloader
This is a simple 1 connection only download manager. It uses less resources than the multi-part downloader. It downloads straight to the download path. This [downloader](https://github.com/bezzad/Downloader) can be used to download files in parallel and with multiple chunks.
It has the following options: It has the following options:
- Maximum parallel downloads: This number indicates how many completed torrents from Real Debrid can be downloaded at the same time. On low powered systems it is recommended to keep this number low. - Download speed (in MB/s): This number indicates the speed in MB/s per download over all parallel downloads and chunks.
- Parallel connections per download: This number indicates how many parallel it will use per download. This can increase speed, recommended is no more than 8.
- Parallel chunks per download: This number indicates in how many chunks each download is split, recommended is no more than 8.
- Connection Timeout: This number indicates the timeout in milliseconds before a download chunk times out. It will retry each chunk 5 times before completely failing.
#### Multi Part Downloader #### Aria2c downloader
This [downloader](https://github.com/bezzad/Downloader) as more options and such uses more resources (memory, CPU) to download files. Recommended more powerful systems. This will use an external Aria2c downloader client. You will need to install this client yourself on your host, it is not included in the docker image.
It has the following options: It has the following options:
- Temp Download path: Set this path to where the downloader temporarily stores chunks. This path can be an internal path in Docker (i.e. `/data/temp`) but make sure you have enough disk space to complete the whole download. When all chunks are completed the completed file is copied to your download folder. - Url: The full URL to your Aria2c service. This must end in /jsonrpc. A standard path is `http://192.168.10.2:6800/jsonrpc`.
- Maximum parallel downloads: This number indicates how many completed torrents from Real Debrid can be downloaded at the same time. - Secret: Optional secret to connecto to your Aria2c service.
- Parallel connections per download: This number indicates how many threads/connections/parts/chunks it will use per download. This can increase speed, recommended is no more than 8.
- Download speed (in MB/s): This number indicates the speed in MB/s per download. If you set this to 10 and `Maximum parallel downloads` to 2, you can download with a maximum of 20MB/s. If Aria2c is selected, none of the above options for `Internal Downloader` are used, you have to configure Aria2c manually.
### Troubleshooting ### Troubleshooting
@ -136,8 +195,8 @@ Notice: the progress and ETA reported in Sonarr's Activity tab will not be accur
- NodeJS - NodeJS
- NPM - NPM
- Angular CLI - Angular CLI
- .NET 5 - .NET 6
- Visual Studio 2019 - Visual Studio 2022
- (optional) Resharper - (optional) Resharper
1. Open the client folder project in VS Code and run `npm install`. 1. Open the client folder project in VS Code and run `npm install`.

View file

@ -1,12 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.

37
client/.eslintrc.json Normal file
View file

@ -0,0 +1,37 @@
{
"root": true,
"ignorePatterns": ["projects/**/*"],
"overrides": [
{
"files": ["*.ts"],
"parserOptions": {
"project": ["tsconfig.json"],
"createDefaultProgram": true
},
"extends": ["plugin:@angular-eslint/recommended", "plugin:@angular-eslint/template/process-inline-templates"],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
]
}
},
{
"files": ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended"],
"rules": {}
}
]
}

46
client/.gitignore vendored
View file

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

2
client/.prettierignore Normal file
View file

@ -0,0 +1,2 @@
.angular
.vscode

View file

@ -3,11 +3,33 @@
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"rdt-client": { "client": {
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
"style": "scss" "style": "scss",
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:interceptor": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
},
"@schematics/angular:application": {
"strict": true
} }
}, },
"root": "", "root": "",
@ -22,96 +44,76 @@
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"aot": true, "inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"], "assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"], "styles": ["src/styles.scss"],
"scripts": [] "scripts": []
}, },
"configurations": { "configurations": {
"production": { "production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [ "fileReplacements": [
{ {
"replace": "src/environments/environment.ts", "replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts" "with": "src/environments/environment.prod.ts"
} }
], ],
"optimization": true, "outputHashing": "all"
"outputHashing": "all", },
"sourceMap": false, "development": {
"namedChunks": false, "buildOptimizer": false,
"extractLicenses": true, "optimization": false,
"vendorChunk": false, "vendorChunk": true,
"buildOptimizer": true, "extractLicenses": false,
"budgets": [ "sourceMap": true,
{ "namedChunks": true
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
} }
} },
"defaultConfiguration": "production"
}, },
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"options": { "options": {
"browserTarget": "rdt-client:build",
"proxyConfig": "angular-proxy.json" "proxyConfig": "angular-proxy.json"
}, },
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "rdt-client:build:production" "browserTarget": "client:build:production"
},
"development": {
"browserTarget": "client:build:development"
} }
} },
"defaultConfiguration": "development"
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "rdt-client:build" "browserTarget": "client:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
} }
}, },
"lint": { "lint": {
"builder": "@angular-devkit/build-angular:tslint", "builder": "@angular-eslint/builder:lint",
"options": { "options": {
"tsConfig": [ "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": ["**/node_modules/**"]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "rdt-client:serve"
},
"configurations": {
"production": {
"devServerTarget": "rdt-client:serve:production"
}
} }
} }
} }
} }
}, },
"defaultProject": "rdt-client" "cli": {
"analytics": false,
"schematicCollections": ["@angular-eslint/schematics"]
}
} }

View file

@ -1,32 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/rdt-client'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

28364
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,53 +1,54 @@
{ {
"name": "rdt-client", "name": "client",
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"test": "ng test", "watch": "ng build --watch --configuration development",
"update": "ng update --force --allow-dirty @angular/cli @angular/core @angular/cdk @angular/flex-layout",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e", "prettier": "prettier --write \"./**/*.{ts,html,json}\""
"update": "ng update --force --allow-dirty @angular/cli @angular/core"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "~11.2.0", "@angular/animations": "^15.2.2",
"@angular/common": "~11.2.0", "@angular/cdk": "^15.2.2",
"@angular/compiler": "~11.2.0", "@angular/common": "^15.2.2",
"@angular/core": "~11.2.0", "@angular/compiler": "^15.2.2",
"@angular/forms": "~11.2.0", "@angular/core": "^15.2.2",
"@angular/platform-browser": "~11.2.0", "@angular/flex-layout": "^15.0.0-beta.42",
"@angular/platform-browser-dynamic": "~11.2.0", "@angular/forms": "^15.2.2",
"@angular/router": "~11.2.0", "@angular/platform-browser": "^15.2.2",
"@fortawesome/fontawesome-free": "^5.15.2", "@angular/platform-browser-dynamic": "^15.2.2",
"@microsoft/signalr": "^5.0.3", "@angular/router": "^15.2.2",
"bulma": "^0.9.2", "@fortawesome/fontawesome-free": "^6.3.0",
"curray": "^1.0.8", "@microsoft/signalr": "^7.0.3",
"ngx-filesize": "^2.0.16", "bulma": "^0.9.4",
"rxjs": "~6.6.3", "curray": "^1.0.11",
"tslib": "^2.1.0", "file-saver-es": "^2.0.5",
"zone.js": "~0.11.3" "ngx-filesize": "^3.0.1",
"rxjs": "~7.8.0",
"tslib": "^2.5.0",
"zone.js": "~0.13.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.1102.0", "@angular-devkit/build-angular": "^15.2.2",
"@angular/cli": "~11.2.0", "@angular-eslint/builder": "15.2.1",
"@angular/compiler-cli": "~11.2.0", "@angular-eslint/eslint-plugin": "15.2.1",
"@angular/language-service": "~11.2.0", "@angular-eslint/eslint-plugin-template": "15.2.1",
"@types/node": "^14.14.27", "@angular-eslint/schematics": "15.2.1",
"@types/jasmine": "~3.6.3", "@angular-eslint/template-parser": "15.2.1",
"@types/jasminewd2": "~2.0.8", "@angular/cli": "^15.2.2",
"codelyzer": "^6.0.1", "@angular/compiler-cli": "^15.2.2",
"jasmine-core": "~3.6.0", "@angular/language-service": "^15.2.2",
"jasmine-spec-reporter": "~6.0.0", "@types/file-saver": "^2.0.5",
"karma": "~6.1.1", "@types/file-saver-es": "^2.0.1",
"karma-chrome-launcher": "~3.1.0", "@types/node": "^18.14.6",
"karma-coverage-istanbul-reporter": "~3.0.3", "@typescript-eslint/eslint-plugin": "5.54.1",
"karma-jasmine": "~4.0.1", "@typescript-eslint/parser": "5.54.1",
"karma-jasmine-html-reporter": "^1.5.4", "eslint": "^8.35.0",
"protractor": "~7.0.0", "prettier": "^2.8.4",
"ts-node": "~9.1.1", "typescript": "~4.9.5"
"tslint": "~6.1.0",
"typescript": "~4.1.5"
} }
} }

View file

@ -0,0 +1,215 @@
<div fxLayout.lt-lg="column" fxLayout.gt-sm="row" fxLayoutGap="20px" class="field">
<div fxFlex>
<div class="field">
<label class="label">Torrent file</label>
<div class="file has-name">
<label class="file-label">
<input class="file-input" type="file" name="resume" (change)="pickFile($event)" [disabled]="saving" />
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label"> Pick a torrent file... </span>
</span>
<span class="file-name">
{{ fileName }}
</span>
</label>
</div>
</div>
<hr />
<div class="field">
<label class="label">Magnet Link</label>
<div class="control">
<textarea
class="textarea"
placeholder="Paste your magnet link here"
[(ngModel)]="magnetLink"
[disabled]="saving"
(blur)="checkFiles()"
(paste)="onPaste()"
></textarea>
</div>
</div>
<hr />
<div class="field">
<label class="label">Post Download Action</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="hostDownloadAction">
<option [ngValue]="0">Download all files to host</option>
<option [ngValue]="1">Don't download any files to host</option>
</select>
</div>
<p class="help">
When a torrent is finished downloading on the provider, perform this action. Use this setting if you only want
to add files to Real-Debrid but not download them to the host.
</p>
</div>
<div class="field">
<label class="label">Post Torrent Download Action</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="downloadAction" [disabled]="provider === 'AllDebrid' || provider === 'Premiumize'">
<option [ngValue]="0">Download all files</option>
<option [ngValue]="1">Download all available files</option>
<option [ngValue]="2">Manually pick files</option>
</select>
</div>
<p class="help">When a torrent is fully downloaded on the provider, perform this action.</p>
<p class="help" *ngIf="provider === 'AllDebrid'">
This option is only available for RealDebrid. AllDebrid and Premiumize will always download the full torrent.
</p>
</div>
<div
class="notification is-success is-light"
*ngIf="provider === 'AllDebrid' && availableFiles && availableFiles.length > 0"
>
This torrent is available for immediate download.
</div>
<div
class="notification is-warning is-light"
*ngIf="provider === 'AllDebrid' && availableFiles && availableFiles.length === 0"
>
This torrent is not available for immediate download.
</div>
<div class="field">
<label class="label">Minimum file size to download</label>
<div class="control">
<div class="field has-addons" style="margin-bottom: 0">
<div class="control is-expanded">
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="downloadMinSize" />
</div>
<div class="control">
<a class="button is-static">MB</a>
</div>
</div>
</div>
<p class="help" *ngIf="provider === 'AllDebrid'">
When downloading with AllDebrid it cannot be guaranteed that only files above this limit will be download as
some files are grouped together in 1 large archive.
</p>
<p class="help" *ngIf="downloadAction === 2">This setting does not apply to manually selected files.</p>
</div>
<div class="field">
<label class="label">Finished action</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="finishedAction">
<option [ngValue]="0">Do nothing</option>
<option [ngValue]="1">Remove torrent from provider and client</option>
<option [ngValue]="2">Remove torrent from provider</option>
<option [ngValue]="3">Remove torrent from client</option>
</select>
</div>
</div>
<div class="field">
<label class="label">Category</label>
<div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="category" />
</div>
<p class="help">The category becomes a sub-folder in your main download path.</p>
</div>
<div class="field">
<label class="label">Priority</label>
<div class="control">
<input class="input" type="number" step="1" [(ngModel)]="priority" />
</div>
<p class="help">
Set the priority for this torrent where 1 is the highest. When empty it will be assigned the lowest priority.
</p>
</div>
<div class="field">
<label class="label">Automatic retry downloads</label>
<div class="control">
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="downloadRetryAttempts" />
</div>
<p class="help">When a single download fails it will retry it this many times before marking it as failed.</p>
</div>
<div class="field">
<label class="label">Automatic retry torrent</label>
<div class="control">
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="torrentRetryAttempts" />
</div>
<p class="help">
When a single download has failed multiple times (see setting above) or when the torrent itself received an
error it will retry the full torrent this many times before marking it failed.
</p>
</div>
<div class="field">
<label class="label">Delete download when in error</label>
<div class="control">
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentDeleteOnError" />
</div>
<p class="help">
When a download has been in error for this many minutes, delete it from the provider and the client. 0 to
disable.
</p>
</div>
<div class="field">
<label class="label">Torrent maximum lifetime</label>
<div class="control">
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentLifetime" />
</div>
<p class="help">
The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the
torrent is completed and has downloads, the lifetime setting will not apply. 0 to disable.
</p>
</div>
</div>
<div fxFlex *ngIf="provider === 'RealDebrid'">
<div class="field">
<label class="label">Available files</label>
<p class="help">
These files are available for immediate download from Real-Debrid. <br />
It is possible that there are more files in the torrent, which are not shown here.<br />
</p>
<div class="scroll-container">
<div class="field" *ngIf="downloadAction === 2">
<label class="checkbox is-fullwidth-label">
<input type="checkbox" [checked]="allSelected" (change)="downloadFileCheckedAll()" />
Select all
</label>
</div>
<div class="field" *ngIf="downloadAction === 2 && availableFiles !== null">
<label class="checkbox is-fullwidth-label" *ngFor="let file of availableFiles">
<input
type="checkbox"
[checked]="downloadFiles[file.filename]"
(change)="downloadFileChecked(file.filename)"
/>
{{ file.filename }} ({{ file.filesize | filesize }})
</label>
</div>
<div class="field" *ngIf="downloadAction !== 2 && availableFiles !== null">
<label class="is-fullwidth-label is-block" *ngFor="let file of availableFiles">
{{ file.filename }}
<span *ngIf="file.filesize > 0">({{ file.filesize | filesize }})</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="field">
<div class="control">
<div class="notification is-danger is-light" *ngIf="error">{{ error }}</div>
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()">
<span>Add Torrent</span>
</button>
</div>
</div>

View file

@ -0,0 +1,19 @@
.file-name {
max-width: 24em;
width: 24em;
}
.scroll-container {
width: 100%;
overflow-x: auto;
overflow-y: auto;
max-height: 603px;
}
.is-fullwidth-label {
width: 100%;
}
.is-block {
display: block;
}

View file

@ -0,0 +1,212 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { TorrentService } from 'src/app/torrent.service';
import { Torrent, TorrentFileAvailability } from '../models/torrent.model';
import { SettingsService } from '../settings.service';
@Component({
selector: 'app-add-new-torrent',
templateUrl: './add-new-torrent.component.html',
styleUrls: ['./add-new-torrent.component.scss'],
})
export class AddNewTorrentComponent implements OnInit {
public fileName: string;
public magnetLink: string;
private currentTorrentFile: string;
public provider: string;
public category: string;
public hostDownloadAction: number = 0;
public downloadAction: number = 0;
public finishedAction: number = 0;
public downloadMinSize: number = 0;
public torrentRetryAttempts: number = 1;
public downloadRetryAttempts: number = 3;
public torrentDeleteOnError: number = 0;
public torrentLifetime: number = 0;
public priority: number;
public availableFiles: TorrentFileAvailability[];
public downloadFiles: { [key: string]: boolean } = {};
public allSelected: boolean;
public saving = false;
public error: string;
private selectedFile: File;
constructor(
private router: Router,
private torrentService: TorrentService,
private settingsService: SettingsService
) {}
ngOnInit(): void {
this.settingsService.get().subscribe((settings) => {
const providerSetting = settings.first((m) => m.key === 'Provider:Provider');
this.provider = providerSetting.enumValues[providerSetting.value as number];
this.category = settings.first((m) => m.key === 'Gui:Default:Category')?.value as string;
this.hostDownloadAction = this.downloadAction = settings.first((m) => m.key === 'Gui:Default:HostDownloadAction')
?.value as number;
this.downloadAction =
settings.first((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0;
this.finishedAction = settings.first((m) => m.key === 'Gui:Default:FinishedAction')?.value as number;
this.downloadMinSize = settings.first((m) => m.key === 'Gui:Default:MinFileSize')?.value as number;
this.torrentRetryAttempts = settings.first((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number;
this.downloadRetryAttempts = settings.first((m) => m.key === 'Gui:Default:DownloadRetryAttempts')
?.value as number;
this.torrentDeleteOnError = settings.first((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number;
this.torrentLifetime = settings.first((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number;
this.priority = settings.first((m) => m.key === 'Gui:Default:Priority')?.value as number;
});
}
public pickFile(evt: Event): void {
const files = (evt.target as HTMLInputElement).files;
if (files.length === 0) {
return;
}
const file = files[0];
this.fileName = file.name;
this.selectedFile = file;
this.checkFiles();
}
public downloadFileChecked(file: string): void {
this.downloadFiles[file] = !this.downloadFiles[file];
this.allSelected = true;
this.availableFiles.forEach((file) => {
if (!this.downloadFiles[file.filename]) {
this.allSelected = false;
}
});
}
public downloadFileCheckedAll(): void {
this.allSelected = !this.allSelected;
this.availableFiles.forEach((file) => {
this.downloadFiles[file.filename] = this.allSelected;
});
}
public ok(): void {
this.saving = true;
this.error = null;
let downloadManualFiles: string = null;
if (this.downloadAction === 2) {
const selectedFiles = [];
for (let filePath in this.downloadFiles) {
if (this.downloadFiles[filePath] === true) {
selectedFiles.push(filePath);
}
}
if (selectedFiles.length === 0) {
this.error = 'No files have been selected to download';
return;
}
downloadManualFiles = selectedFiles.join(',');
}
const torrent = new Torrent();
torrent.category = this.category;
torrent.hostDownloadAction = this.hostDownloadAction;
torrent.downloadAction = this.downloadAction;
torrent.finishedAction = this.finishedAction;
torrent.downloadMinSize = this.downloadMinSize;
torrent.downloadManualFiles = downloadManualFiles;
torrent.priority = this.priority;
torrent.torrentRetryAttempts = this.torrentRetryAttempts;
torrent.downloadRetryAttempts = this.downloadRetryAttempts;
torrent.deleteOnError = this.torrentDeleteOnError;
torrent.lifetime = this.torrentLifetime;
if (this.magnetLink) {
this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe(
() => {
this.router.navigate(['/']);
},
(err) => {
this.error = err.error;
this.saving = false;
}
);
} else if (this.selectedFile) {
this.torrentService.uploadFile(this.selectedFile, torrent).subscribe(
() => {
this.router.navigate(['/']);
},
(err) => {
this.error = err.error;
this.saving = false;
}
);
} else {
this.error = 'No magnet or file uploaded';
this.saving = false;
}
}
public onPaste(): void {
setTimeout(() => {
this.checkFiles();
}, 100);
}
public checkFiles(): void {
if (this.magnetLink && this.magnetLink === this.currentTorrentFile) {
return;
}
this.saving = true;
this.error = null;
this.availableFiles = null;
this.downloadFiles = {};
this.allSelected = true;
if (this.magnetLink) {
this.torrentService.checkFilesMagnet(this.magnetLink).subscribe(
(result) => {
this.saving = false;
this.availableFiles = result;
this.currentTorrentFile = this.magnetLink;
result.forEach((file) => {
this.downloadFiles[file.filename] = true;
});
},
(err) => {
this.error = err.error;
this.saving = false;
}
);
} else if (this.selectedFile) {
this.torrentService.checkFiles(this.selectedFile).subscribe(
(result) => {
this.saving = false;
this.availableFiles = result;
result.forEach((file) => {
this.downloadFiles[file.filename] = true;
});
},
(err) => {
this.error = err.error;
this.saving = false;
}
);
} else {
this.saving = false;
}
}
}

View file

@ -1,9 +1,14 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
import { AuthResolverService } from './auth-resolver.service'; import { AuthResolverService } from './auth-resolver.service';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './login/login.component';
import { MainLayoutComponent } from './main-layout/main-layout.component'; import { MainLayoutComponent } from './main-layout/main-layout.component';
import { ProfileComponent } from './profile/profile.component';
import { SettingsComponent } from './settings/settings.component';
import { SetupComponent } from './setup/setup.component'; import { SetupComponent } from './setup/setup.component';
import { TorrentTableComponent } from './torrent-table/torrent-table.component';
import { TorrentComponent } from './torrent/torrent.component';
const routes: Routes = [ const routes: Routes = [
{ {
@ -20,6 +25,28 @@ const routes: Routes = [
resolve: { resolve: {
isLoggedIn: AuthResolverService, isLoggedIn: AuthResolverService,
}, },
children: [
{
path: '',
component: TorrentTableComponent,
},
{
path: 'torrent/:id',
component: TorrentComponent,
},
{
path: 'add',
component: AddNewTorrentComponent,
},
{
path: 'settings',
component: SettingsComponent,
},
{
path: 'profile',
component: ProfileComponent,
},
],
}, },
]; ];

View file

@ -1,24 +1,28 @@
import { ClipboardModule } from '@angular/cdk/clipboard';
import { APP_BASE_HREF } from '@angular/common';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { curray } from 'curray'; import { curray } from 'curray';
import { FileSizePipe, NgxFilesizeModule } from 'ngx-filesize'; import { FileSizePipe, NgxFilesizeModule } from 'ngx-filesize';
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { AuthInterceptor } from './auth.interceptor'; import { AuthInterceptor } from './auth.interceptor';
import { DecodeURIPipe } from './decode-uri.pipe';
import { DownloadStatusPipe } from './download-status.pipe'; import { DownloadStatusPipe } from './download-status.pipe';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './login/login.component';
import { MainLayoutComponent } from './main-layout/main-layout.component'; import { MainLayoutComponent } from './main-layout/main-layout.component';
import { AddNewTorrentComponent } from './navbar/add-new-torrent/add-new-torrent.component';
import { NavbarComponent } from './navbar/navbar.component'; import { NavbarComponent } from './navbar/navbar.component';
import { SettingsComponent } from './navbar/settings/settings.component'; import { Nl2BrPipe } from './nl2br.pipe';
import { ProfileComponent } from './profile/profile.component';
import { SettingsComponent } from './settings/settings.component';
import { SetupComponent } from './setup/setup.component'; import { SetupComponent } from './setup/setup.component';
import { TorrentDownloadComponent } from './torrent-download/torrent-download.component';
import { TorrentFileComponent } from './torrent-file/torrent-file.component';
import { TorrentRowComponent } from './torrent-row/torrent-row.component';
import { TorrentStatusPipe } from './torrent-status.pipe'; import { TorrentStatusPipe } from './torrent-status.pipe';
import { TorrentTableComponent } from './torrent-table/torrent-table.component'; import { TorrentTableComponent } from './torrent-table/torrent-table.component';
import { TorrentComponent } from './torrent/torrent.component';
curray(); curray();
@ -29,17 +33,30 @@ curray();
NavbarComponent, NavbarComponent,
AddNewTorrentComponent, AddNewTorrentComponent,
TorrentTableComponent, TorrentTableComponent,
TorrentRowComponent,
TorrentFileComponent,
SettingsComponent, SettingsComponent,
TorrentStatusPipe, TorrentStatusPipe,
DownloadStatusPipe, DownloadStatusPipe,
LoginComponent, LoginComponent,
SetupComponent, SetupComponent,
TorrentDownloadComponent, TorrentComponent,
DecodeURIPipe,
ProfileComponent,
Nl2BrPipe,
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule,
NgxFilesizeModule,
FlexLayoutModule,
ClipboardModule,
],
providers: [
FileSizePipe,
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: APP_BASE_HREF, useValue: (window as any)['_app_base'] || '/' },
], ],
imports: [BrowserModule, AppRoutingModule, FormsModule, HttpClientModule, NgxFilesizeModule],
providers: [FileSizePipe, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule {}

View file

@ -8,8 +8,8 @@ import { Observable } from 'rxjs/internal/Observable';
export class AuthService { export class AuthService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
public isLoggedIn(): Observable<void> { public isLoggedIn(): Observable<boolean> {
return this.http.get<void>(`/Api/Authentication/IsLoggedIn`); return this.http.get<boolean>(`/Api/Authentication/IsLoggedIn`);
} }
public create(userName: string, password: string): Observable<void> { public create(userName: string, password: string): Observable<void> {
@ -19,6 +19,13 @@ export class AuthService {
}); });
} }
public setupProvider(provider: string, token: string): Observable<void> {
return this.http.post<void>(`/Api/Authentication/SetupProvider`, {
provider,
token,
});
}
public login(userName: string, password: string): Observable<void> { public login(userName: string, password: string): Observable<void> {
return this.http.post<void>(`/Api/Authentication/Login`, { return this.http.post<void>(`/Api/Authentication/Login`, {
userName, userName,
@ -29,4 +36,11 @@ export class AuthService {
public logout() { public logout() {
return this.http.post<void>(`/Api/Authentication/Logout`, {}); return this.http.post<void>(`/Api/Authentication/Logout`, {});
} }
public update(userName: string, password: string): Observable<void> {
return this.http.post<void>(`/Api/Authentication/Update`, {
userName,
password,
});
}
} }

View file

@ -0,0 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'decodeURI',
})
export class DecodeURIPipe implements PipeTransform {
transform(input: any) {
return decodeURI(input);
}
}

View file

@ -14,7 +14,7 @@ export class DownloadStatusPipe implements PipeTransform {
} }
if (value.error) { if (value.error) {
return `Error: ${value.error}`; return value.error;
} }
if (value.completed != null) { if (value.completed != null) {
@ -26,8 +26,13 @@ export class DownloadStatusPipe implements PipeTransform {
} }
if (value.unpackingStarted) { if (value.unpackingStarted) {
const progress = ((value.bytesDone / value.bytesTotal) * 100).toFixed(2); let progress = (value.bytesDone / value.bytesTotal) * 100;
return `Unpacking ${progress || 0}%`;
if (isNaN(progress)) {
progress = 0;
}
return `Unpacking ${progress.toFixed(2)}%`;
} }
if (value.unpackingQueued) { if (value.unpackingQueued) {
@ -39,10 +44,15 @@ export class DownloadStatusPipe implements PipeTransform {
} }
if (value.downloadStarted) { if (value.downloadStarted) {
const progress = ((value.bytesDone / value.bytesTotal) * 100).toFixed(2); let progress = (value.bytesDone / value.bytesTotal) * 100;
if (isNaN(progress)) {
progress = 0;
}
const speed = this.pipe.transform(value.speed, 'filesize'); const speed = this.pipe.transform(value.speed, 'filesize');
return `Downloading ${progress || 0}% (${speed}/s)`; return `Downloading ${progress.toFixed(2)}% (${speed}/s)`;
} }
if (value.downloadQueued) { if (value.downloadQueued) {

View file

@ -5,37 +5,46 @@
<div class="column is-5-tablet is-5-desktop is-5-widescreen"> <div class="column is-5-tablet is-5-desktop is-5-widescreen">
<img src="../../assets/logo.png" /> <img src="../../assets/logo.png" />
<div class="box"> <div class="box">
<div class="field"> <form (ngSubmit)="login()">
<label for="" class="label">Username</label> <div class="field">
<div class="control has-icons-left"> <label for="" class="label">Username</label>
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required /> <div class="control has-icons-left">
<span class="icon is-small is-left"> <input
<i class="fa fa-envelope"></i> type="text"
</span> placeholder=""
class="input"
name="userName"
(change)="setUserName($event)"
required
/>
<span class="icon is-small is-left">
<i class="fa fa-envelope"></i>
</span>
</div>
</div> </div>
</div> <div class="field">
<div class="field"> <label for="" class="label" name="password">Password</label>
<label for="" class="label" name="password">Password</label> <div class="control has-icons-left">
<div class="control has-icons-left"> <input type="password" class="input" required name="password" (change)="setPassword($event)" />
<input type="password" class="input" required name="password" [(ngModel)]="password" /> <span class="icon is-small is-left">
<span class="icon is-small is-left"> <i class="fa fa-lock"></i>
<i class="fa fa-lock"></i> </span>
</span> </div>
</div> </div>
</div> <div class="field">
<div class="field"> <button
<button class="button is-success"
class="button is-success" type="submit"
(click)="login()" [ngClass]="{ 'is-loading': loggingIn }"
[ngClass]="{ 'is-loading': loggingIn }" [disabled]="loggingIn"
[disabled]="loggingIn" >
> Login
Login </button>
</button> </div>
</div> <div class="notification is-danger is-light" *ngIf="error?.length > 0">
<div class="notification is-danger is-light" *ngIf="error?.length > 0"> {{ error }}
{{ error }} </div>
</div> </form>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,13 +1,13 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AuthService } from '../auth.service';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'], styleUrls: ['./login.component.scss'],
}) })
export class LoginComponent implements OnInit { export class LoginComponent {
public userName: string; public userName: string;
public password: string; public password: string;
public error: string; public error: string;
@ -15,7 +15,13 @@ export class LoginComponent implements OnInit {
constructor(private authService: AuthService, private router: Router) {} constructor(private authService: AuthService, private router: Router) {}
ngOnInit(): void {} public setUserName(event: Event): void {
this.userName = (event.target as any).value;
}
public setPassword(event: Event): void {
this.password = (event.target as any).value;
}
public login(): void { public login(): void {
this.error = null; this.error = null;

View file

@ -1,6 +1,6 @@
<app-navbar></app-navbar> <app-navbar></app-navbar>
<section class="section main-list"> <section class="section main-list">
<div class="container"> <div class="container">
<app-torrent-table></app-torrent-table> <router-outlet></router-outlet>
</div> </div>
</section> </section>

View file

@ -1,12 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
@Component({ @Component({
selector: 'app-main-layout', selector: 'app-main-layout',
templateUrl: './main-layout.component.html', templateUrl: './main-layout.component.html',
styleUrls: ['./main-layout.component.scss'], styleUrls: ['./main-layout.component.scss'],
}) })
export class MainLayoutComponent implements OnInit { export class MainLayoutComponent {
constructor() {} constructor() {}
ngOnInit(): void {}
} }

View file

@ -15,4 +15,5 @@ export class Download {
public bytesTotal: number; public bytesTotal: number;
public bytesDone: number; public bytesDone: number;
public speed: number; public speed: number;
public retryCount: number;
} }

View file

@ -1,4 +1,7 @@
export class Profile { export class Profile {
public provider: string;
public userName: string; public userName: string;
public expiration: Date; public expiration: Date;
public currentVersion: string;
public latestVersion: string;
} }

View file

@ -1,4 +1,9 @@
export class Setting { export class Setting {
public settingId: string; key: string;
public value: string; value: boolean | number | null | string;
displayName: null | string;
description: null | string;
type: string;
settings: Setting[];
enumValues: { [key: string]: string };
} }

View file

@ -4,9 +4,27 @@ export class Torrent {
public torrentId: string; public torrentId: string;
public hash: string; public hash: string;
public category: string; public category: string;
public hostDownloadAction: number;
public downloadAction: number;
public finishedAction: number;
public downloadMinSize: number;
public downloadManualFiles: string;
public added: Date; public added: Date;
public filesSelected: Date;
public completed: Date; public completed: Date;
public autoDelete: boolean;
public fileOrMagnet: string;
public isFile: boolean;
public retryCount: number;
public downloadRetryAttempts: number;
public torrentRetryAttempts: number;
public deleteOnError: number;
public lifetime: number;
public priority: number;
public error: string;
public rdId: string; public rdId: string;
public rdName: string; public rdName: string;
@ -35,11 +53,17 @@ export class TorrentFile {
public download: Download; public download: Download;
} }
export class TorrentFileAvailability {
public filename: string;
public filesize: number;
}
export enum RealDebridStatus { export enum RealDebridStatus {
Processing = 0, Processing = 0,
WaitingForFileSelection = 1, WaitingForFileSelection = 1,
Downloading = 2, Downloading = 2,
Finished = 3, Finished = 3,
Uploading = 4,
Error = 99, Error = 99,
} }

View file

@ -1,77 +0,0 @@
<div class="modal" [class.is-active]="isActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add New Torrent</p>
<button class="delete" aria-label="close" (click)="cancel()"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">Magnet Link</label>
<div class="control">
<textarea
class="textarea"
placeholder="Paste your magnet link here"
[(ngModel)]="magnetLink"
[disabled]="saving"
></textarea>
</div>
</div>
<hr />
<div class="field">
<label class="label">Torrent file</label>
<div class="file has-name">
<label class="file-label">
<input class="file-input" type="file" name="resume" (change)="pickFile($event)" [disabled]="saving" />
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label"> Pick a torrent file... </span>
</span>
<span class="file-name">
{{ fileName }}
</span>
</label>
</div>
</div>
<hr />
<div class="field">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="autoDelete" />
Remove torrent when finished downloading on host
</label>
</div>
<div class="notification is-danger is-light" *ngIf="error">Cannot add torrent: {{ error | json }}</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()">
<span>Add Torrent</span>
</button>
<button class="button is-info" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="checkFiles()">
<span>Check available files</span>
</button>
<button class="button" (click)="cancel()" [disabled]="saving">Cancel</button>
</footer>
</div>
</div>
<div class="modal" [class.is-active]="isFileModalActive">
<div class="modal-background"></div>
<div class="modal-card file-modal">
<header class="modal-card-head">
<p class="modal-card-title">Available Files</p>
<button class="delete" aria-label="close" (click)="isFileModalActive = false"></button>
</header>
<section class="modal-card-body">
<table class="table" style="width: 100%">
<tbody>
<tr *ngFor="let file of fileList">
<td>{{ file }}</td>
</tr>
</tbody>
</table>
</section>
<footer class="modal-card-foot"></footer>
</div>
</div>

View file

@ -1,8 +0,0 @@
.file-name {
max-width: 24em;
width: 24em;
}
.file-modal {
width: calc(75% - 40px);
}

View file

@ -1,131 +0,0 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { TorrentService } from 'src/app/torrent.service';
@Component({
selector: 'app-add-new-torrent',
templateUrl: './add-new-torrent.component.html',
styleUrls: ['./add-new-torrent.component.scss'],
})
export class AddNewTorrentComponent implements OnInit {
@Input()
public get open(): boolean {
return this.isActive;
}
public set open(val: boolean) {
this.reset();
this.isActive = val;
}
@Output()
public openChange = new EventEmitter<boolean>();
public isActive = false;
public isFileModalActive = false;
public fileName: string;
public magnetLink: string;
public autoDelete: boolean;
public fileList: string[];
public saving = false;
public error: string;
private selectedFile: File;
constructor(private torrentService: TorrentService) {}
ngOnInit(): void {}
public reset(): void {
this.fileName = '';
this.magnetLink = '';
this.autoDelete = false;
this.saving = false;
this.selectedFile = null;
this.error = null;
}
public pickFile(evt: Event): void {
const files = (evt.target as HTMLInputElement).files;
if (files.length === 0) {
return;
}
const file = files[0];
this.fileName = file.name;
this.selectedFile = file;
}
public ok(): void {
this.saving = true;
this.error = null;
if (this.magnetLink) {
this.torrentService.uploadMagnet(this.magnetLink, this.autoDelete).subscribe(
() => {
this.cancel();
},
(err) => {
this.error = err.error;
this.saving = false;
}
);
} else if (this.selectedFile) {
this.torrentService.uploadFile(this.selectedFile, this.autoDelete).subscribe(
() => {
this.cancel();
},
(err) => {
this.error = err.error;
this.saving = false;
}
);
} else {
this.cancel();
}
}
public checkFiles(): void {
this.saving = true;
this.error = null;
if (this.magnetLink) {
this.torrentService.checkFilesMagnet(this.magnetLink).subscribe(
(result) => {
this.saving = false;
this.isFileModalActive = true;
this.fileList = result;
},
(err) => {
this.error = err.error;
this.saving = false;
}
);
} else if (this.selectedFile) {
this.torrentService.checkFiles(this.selectedFile).subscribe(
(result) => {
this.saving = false;
this.isFileModalActive = true;
this.fileList = result;
},
(err) => {
this.error = err.error;
this.saving = false;
}
);
} else {
this.cancel();
}
}
public cancel(): void {
this.isActive = false;
this.openChange.emit(this.open);
}
}

View file

@ -1,6 +1,6 @@
<nav class="navbar is-dark is-fixed-top" role="navigation" aria-label="main navigation"> <nav class="navbar is-dark is-fixed-top" role="navigation" aria-label="main navigation">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item"> <a class="navbar-item" routerLink="/">
<img src="../../assets/logo.png" /> <img src="../../assets/logo.png" />
</a> </a>
@ -20,19 +20,25 @@
<div class="navbar-menu" [class.is-active]="showMobileMenu"> <div class="navbar-menu" [class.is-active]="showMobileMenu">
<div class="navbar-start"> <div class="navbar-start">
<a class="navbar-item" (click)="this.showNewTorrent = true"> <a class="navbar-item" routerLink="/">
<span class="icon">
<i class="fas fa-table" aria-hidden="true"></i>
</span>
<span>Torrents</span>
</a>
<a class="navbar-item" routerLink="/add">
<span class="icon"> <span class="icon">
<i class="fas fa-plus" aria-hidden="true"></i> <i class="fas fa-plus" aria-hidden="true"></i>
</span> </span>
<span>Add New Torrent</span> <span>Add New Torrent</span>
</a> </a>
<a class="navbar-item" (click)="this.showSettings = true"> <a class="navbar-item" routerLink="/settings">
<span class="icon"> <span class="icon">
<i class="fas fa-cog" aria-hidden="true"></i> <i class="fas fa-cog" aria-hidden="true"></i>
</span> </span>
<span>Settings</span> <span>Settings</span>
</a> </a>
<a class="navbar-item" *ngIf="profile" href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener"> <a class="navbar-item" *ngIf="profile" href="{{ providerLink }}" target="_blank" rel="noopener">
<span class="icon"> <span class="icon">
<i class="fas fa-euro-sign" aria-hidden="true"></i> <i class="fas fa-euro-sign" aria-hidden="true"></i>
</span> </span>
@ -46,15 +52,19 @@
<span class="navbar-link"> Account </span> <span class="navbar-link"> Account </span>
<div class="navbar-dropdown is-right"> <div class="navbar-dropdown is-right">
<a class="navbar-item"> Profile </a> <a class="navbar-item" routerLink="profile"> Profile </a>
<a class="navbar-item" (click)="logout()"> Logout </a> <a class="navbar-item" (click)="logout()"> Logout </a>
<hr class="navbar-divider" /> <hr class="navbar-divider" />
<div class="navbar-item">Version 1.7.4</div> <div class="navbar-item">Version 2.0.22</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</nav> </nav>
<div
<app-add-new-torrent [(open)]="showNewTorrent"></app-add-new-torrent> class="notification is-warning"
<app-settings [(open)]="showSettings"></app-settings> *ngIf="profile && profile.latestVersion && profile.currentVersion !== profile.latestVersion"
>
Version {{ profile.latestVersion }} of RealDebrid Client was found. You are currently on version
{{ profile.currentVersion }}.
</div>

View file

@ -1,3 +1,7 @@
.no-premium { .no-premium {
color: red; color: red;
} }
.notification {
margin-top: 52px;
}

View file

@ -1,9 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { TorrentService } from '../torrent.service';
import { SettingsService } from '../settings.service';
import { Profile } from '../models/profile.model';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AuthService } from '../auth.service';
import { Profile } from '../models/profile.model';
import { SettingsService } from '../settings.service';
@Component({ @Component({
selector: 'app-navbar', selector: 'app-navbar',
@ -13,16 +12,26 @@ import { Router } from '@angular/router';
export class NavbarComponent implements OnInit { export class NavbarComponent implements OnInit {
public showMobileMenu = false; public showMobileMenu = false;
public showNewTorrent = false;
public showSettings = false;
public profile: Profile; public profile: Profile;
public providerLink: string;
constructor(private settingsService: SettingsService, private authService: AuthService, private router: Router) {} constructor(private settingsService: SettingsService, private authService: AuthService, private router: Router) {}
ngOnInit(): void { ngOnInit(): void {
this.settingsService.getProfile().subscribe((result) => { this.settingsService.getProfile().subscribe((result) => {
this.profile = 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;
}
}); });
} }

View file

@ -1,232 +0,0 @@
<div class="modal" [class.is-active]="isActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Settings</p>
<button class="delete" aria-label="close" (click)="cancel()"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">Real-Debrid API Key</label>
<div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="settingRealDebridApiKey" />
</div>
<p class="help">
You can find your API key here:
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener">https://real-debrid.com/apitoken</a
>.
</p>
</div>
<div class="field">
<label class="label">Log level</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="settingLogLevel">
<option value="Verbose">Verbose</option>
<option value="Debug">Debug</option>
<option value="Information">Information</option>
<option value="Warning">Warning</option>
<option value="Error">Error</option>
<option value="Fatal">Fatal</option>
</select>
</div>
<p class="help">Recommended level is Warning, set to Debug to get the most info.</p>
</div>
<div class="field">
<label class="label">Download path</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingDownloadPath" />
</div>
<p class="help">Path in the docker container to download files to (i.e. /data/downloads).</p>
</div>
<div class="field">
<label class="label">Mapped path</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingMappedPath" />
</div>
<p class="help">
Path where files are downloaded to on your host (i.e. D:\Downloads). This path is used for Radarr and Sonarr
to find your downloads.
</p>
</div>
<div class="field">
<label class="label">Download client</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="settingDownloadClient">
<option value="Simple">Simple Downloader</option>
<option value="MultiPart">Multi Part Downloader</option>
</select>
</div>
<p class="help">
Select which download client to use, see the
<a href="https://github.com/rogerfar/rdt-client/" target="_blank">README</a> for the various options.
</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Temp Download path</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingTempPath" />
</div>
<p class="help">
Path in the docker container to temporarily download to (i.e. /data/temp). Make sure the docker container has
enough disk space if using a path inside the container.
</p>
</div>
<div class="field">
<label class="label">Maximum parallel downloads</label>
<div class="control">
<input class="input" type="number" max="100" min="1" [(ngModel)]="settingDownloadLimit" />
</div>
<p class="help">Maximum amount of torrents that get downloaded to your host at the same time.</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Parallel connections per download</label>
<div class="control">
<input class="input" type="number" max="100" min="0" [(ngModel)]="settingDownloadChunkCount" />
</div>
<p class="help">
Maximum amount of parallel threads that are used to download a single torrent to your host. If set to 1 no
parallel downloading will be done.
</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Download speed (in MB/s)</label>
<div class="control">
<input class="input" type="number" max="1000" min="0" [(ngModel)]="settingDownloadMaxSpeed" />
</div>
<p class="help">Maximum download speed in Megabytes per second. When set to 0 unlimited speed is used.</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Proxy Server</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingProxyServer" />
</div>
<p class="help">Address of a proxy server.</p>
</div>
<div class="field">
<label class="label">Maximum unpack processes</label>
<div class="control">
<input class="input" type="number" max="100" min="1" [(ngModel)]="settingUnpackLimit" />
</div>
<p class="help">Maximum amount of downloads that get unpacked on your host at the same time.</p>
</div>
<div class="field">
<label class="label">Minimum file size to download</label>
<div class="control">
<div class="field has-addons" style="margin-bottom: 0">
<div class="control is-expanded">
<input class="input" type="number" max="100" min="1" [(ngModel)]="settingMinFileSize" />
</div>
<div class="control">
<a class="button is-static">MB</a>
</div>
</div>
<div class="help">When selecting files in the torrent, skip files smaller than this setting.</div>
</div>
</div>
<div class="field">
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="settingOnlyDownloadAvailableFiles" />
Only download available files on RealDebrid
</label>
<div class="help">
When selected, it will only download files in the torrent that have been download by Real Debrid. You can
use this in combination with the Min File size setting above.
</div>
</div>
</div>
<div class="field">
<label class="label">Test download path permissions</label>
<div class="control">
<button
class="button is-warning"
(click)="testDownloadPath()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testPathError && !testPathSuccess"
>
Test permissions
</button>
<div class="notification is-danger is-light" *ngIf="testPathError">
Could not test your download path<br />
{{ testPathError }}
</div>
<div class="notification is-success is-light" *ngIf="testPathSuccess">Your download path looks good!</div>
</div>
<div class="help">This will check if the download folder has write permissions.</div>
</div>
<div class="field">
<label class="label">Test RealDebrid download speed</label>
<div class="control">
<button
class="button is-warning"
(click)="testDownloadSpeed()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testDownloadSpeedError && !testDownloadSpeedSuccess"
>
Test download speed
</button>
<div class="notification is-danger is-light" *ngIf="testDownloadSpeedError">
Could not test your download speed<br />
{{ testDownloadSpeedError }}
</div>
<div class="notification is-success is-light" *ngIf="testDownloadSpeedSuccess">
Download speed {{ testDownloadSpeedSuccess | filesize }}/s
</div>
</div>
<div class="help">
This will download a 10GB test file from RealDebrid. Hit cancel when you have seen enough.
</div>
</div>
<div class="field">
<label class="label">Test download folder write speed</label>
<div class="control">
<button
class="button is-warning"
(click)="testWriteSpeed()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testWriteSpeedError && !testWriteSpeedSuccess"
>
Test write speed
</button>
<div class="notification is-danger is-light" *ngIf="testWriteSpeedError">
Could not test your download speed<br />
{{ testWriteSpeedError }}
</div>
<div class="notification is-success is-light" *ngIf="testWriteSpeedSuccess">
Write speed {{ testWriteSpeedSuccess | filesize }}/s
</div>
</div>
<div class="help">This will write a small file to your download folder to see how fast it can write to it.</div>
</div>
<div class="notification is-danger is-light" *ngIf="error?.length > 0">Error saving settings: {{ error }}</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success" (click)="ok()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">
Save Settings
</button>
<button class="button" (click)="cancel()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">Cancel</button>
</footer>
</div>
</div>

View file

@ -1,216 +0,0 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Setting } from 'src/app/models/setting.model';
import { SettingsService } from 'src/app/settings.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
})
export class SettingsComponent implements OnInit {
@Input()
public get open(): boolean {
return this.isActive;
}
public set open(val: boolean) {
this.reset();
this.isActive = val;
}
@Output()
public openChange = new EventEmitter<boolean>();
public isActive = false;
public saving = false;
public error: string;
public testPathError: string;
public testPathSuccess: boolean;
public testDownloadSpeedError: string;
public testDownloadSpeedSuccess: number;
public testWriteSpeedError: string;
public testWriteSpeedSuccess: number;
public settingLogLevel: string;
public settingRealDebridApiKey: string;
public settingDownloadPath: string;
public settingMappedPath: string;
public settingTempPath: string;
public settingDownloadClient: string;
public settingDownloadLimit: number;
public settingDownloadChunkCount: number;
public settingDownloadMaxSpeed: number;
public settingUnpackLimit: number;
public settingMinFileSize: number;
public settingOnlyDownloadAvailableFiles: boolean;
public settingProxyServer: string;
constructor(private settingsService: SettingsService) {}
ngOnInit(): void {}
public reset(): void {
this.saving = false;
this.error = null;
this.settingsService.get().subscribe(
(results) => {
this.settingRealDebridApiKey = this.getSetting(results, 'RealDebridApiKey');
this.settingLogLevel = this.getSetting(results, 'LogLevel');
this.settingDownloadPath = this.getSetting(results, 'DownloadPath');
this.settingMappedPath = this.getSetting(results, 'MappedPath');
this.settingTempPath = this.getSetting(results, 'TempPath');
this.settingDownloadClient = this.getSetting(results, 'DownloadClient');
this.settingDownloadLimit = parseInt(this.getSetting(results, 'DownloadLimit'), 10);
this.settingDownloadChunkCount = parseInt(this.getSetting(results, 'DownloadChunkCount'), 10);
this.settingDownloadMaxSpeed = parseInt(this.getSetting(results, 'DownloadMaxSpeed'), 10);
this.settingUnpackLimit = parseInt(this.getSetting(results, 'UnpackLimit'), 10);
this.settingMinFileSize = parseInt(this.getSetting(results, 'MinFileSize'), 10);
this.settingOnlyDownloadAvailableFiles = this.getSetting(results, 'OnlyDownloadAvailableFiles') === '1';
this.settingProxyServer = this.getSetting(results, 'ProxyServer');
},
(err) => {
this.error = err.error;
this.saving = true;
}
);
}
public ok(): void {
this.saving = true;
const settings: Setting[] = [
{
settingId: 'RealDebridApiKey',
value: this.settingRealDebridApiKey,
},
{
settingId: 'LogLevel',
value: this.settingLogLevel,
},
{
settingId: 'DownloadPath',
value: this.settingDownloadPath,
},
{
settingId: 'MappedPath',
value: this.settingMappedPath,
},
{
settingId: 'TempPath',
value: this.settingTempPath,
},
{
settingId: 'DownloadClient',
value: this.settingDownloadClient,
},
{
settingId: 'DownloadLimit',
value: (this.settingDownloadLimit ?? 10).toString(),
},
{
settingId: 'DownloadChunkCount',
value: (this.settingDownloadChunkCount ?? 8).toString(),
},
{
settingId: 'DownloadMaxSpeed',
value: (this.settingDownloadMaxSpeed ?? 0).toString(),
},
{
settingId: 'UnpackLimit',
value: (this.settingUnpackLimit ?? 1).toString(),
},
{
settingId: 'MinFileSize',
value: (this.settingMinFileSize ?? 0).toString(),
},
{
settingId: 'OnlyDownloadAvailableFiles',
value: (this.settingOnlyDownloadAvailableFiles ? '1' : '0').toString(),
},
{
settingId: 'ProxyServer',
value: this.settingProxyServer,
},
];
this.settingsService.update(settings).subscribe(
() => {
this.isActive = false;
this.openChange.emit(this.open);
},
(err) => {
this.error = err;
}
);
}
public testDownloadPath(): void {
this.saving = true;
this.testPathError = null;
this.testPathSuccess = false;
this.settingsService.testPath(this.settingDownloadPath).subscribe(
() => {
this.saving = false;
this.testPathSuccess = true;
},
(err) => {
this.testPathError = err.error;
this.saving = false;
}
);
}
public testDownloadSpeed(): void {
this.saving = true;
this.testDownloadSpeedError = null;
this.testDownloadSpeedSuccess = 0;
this.settingsService.testDownloadSpeed().subscribe(
(result) => {
this.saving = false;
this.testDownloadSpeedSuccess = result;
},
(err) => {
this.testDownloadSpeedError = err.error;
this.saving = false;
}
);
}
public testWriteSpeed(): void {
this.saving = true;
this.testWriteSpeedError = null;
this.testWriteSpeedSuccess = 0;
this.settingsService.testWriteSpeed().subscribe(
(result) => {
this.saving = false;
this.testWriteSpeedSuccess = result;
},
(err) => {
this.testWriteSpeedError = err.error;
this.saving = false;
}
);
}
public cancel(): void {
this.isActive = false;
this.openChange.emit(this.open);
}
private getSetting(settings: Setting[], key: string): string {
const setting = settings.filter((m) => m.settingId === key);
if (setting.length !== 1) {
throw new Error(`Unable to find setting with key ${key}`);
}
return setting[0].value;
}
}

View file

@ -0,0 +1,27 @@
import { Pipe, PipeTransform, SecurityContext, VERSION } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Pipe({
name: 'nl2br',
})
export class Nl2BrPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(value: string, sanitizeBeforehand?: boolean): string {
if (typeof value !== 'string') {
return value;
}
let result: any;
const textParsed = value.replace(/(?:\r\n|\r|\n)/g, '<br />');
if (!VERSION || VERSION.major === '2') {
result = this.sanitizer.bypassSecurityTrustHtml(textParsed);
} else if (sanitizeBeforehand) {
result = this.sanitizer.sanitize(SecurityContext.HTML, textParsed);
} else {
result = textParsed;
}
return result;
}
}

View file

@ -0,0 +1,38 @@
<div class="field">
<label class="label">Login username</label>
<div class="control">
<input class="input" type="text" minlength="1" maxlength="100" [(ngModel)]="username" />
</div>
<p class="help">
This is the username you use to login Real-Debrid Client. Only change this if you want to change the username.
</p>
</div>
<div class="field">
<label class="label">Login password</label>
<div class="control">
<input class="input" type="password" minlength="1" maxlength="100" [(ngModel)]="password" />
</div>
<p class="help">
This is the password you use to login Real-Debrid Client. Only change this if you want to change the password.
</p>
</div>
<div class="field">
<div class="control">
<div class="notification is-danger is-light" *ngIf="error !== null">Error saving: {{ error }}</div>
</div>
</div>
<div class="field">
<div class="control">
<div class="notification is-success is-light" *ngIf="success">Your profile has been updated</div>
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-success" (click)="save()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">
Save Profile
</button>
</div>
</div>

View file

@ -0,0 +1,36 @@
import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.scss'],
})
export class ProfileComponent {
constructor(private authService: AuthService) {}
public username: string;
public password: string;
public saving = false;
public success: boolean;
public error: string;
public save(): void {
this.success = false;
this.error = null;
this.saving = true;
this.authService.update(this.username, this.password).subscribe(
() => {
this.success = true;
this.saving = false;
},
(err) => {
this.error = err.error;
this.success = false;
this.saving = false;
}
);
}
}

View file

@ -15,7 +15,7 @@ export class SettingsService {
} }
public update(settings: Setting[]): Observable<void> { public update(settings: Setting[]): Observable<void> {
return this.http.put<void>(`/Api/Settings`, { settings }); return this.http.put<void>(`/Api/Settings`, settings);
} }
public getProfile(): Observable<Profile> { public getProfile(): Observable<Profile> {
@ -33,4 +33,11 @@ export class SettingsService {
public testWriteSpeed(): Observable<number> { public testWriteSpeed(): Observable<number> {
return this.http.get<number>(`/Api/Settings/TestWriteSpeed`); return this.http.get<number>(`/Api/Settings/TestWriteSpeed`);
} }
public testAria2cConnection(url: string, secret: string): Observable<{ version: string }> {
return this.http.post<{ version: string }>(`/Api/Settings/TestAria2cConnection`, {
url,
secret,
});
}
} }

View file

@ -0,0 +1,150 @@
<div class="tabs">
<ul>
<li [ngClass]="{ 'is-active': activeTab === i }" (click)="activeTab = i" *ngFor="let tab of tabs; let i = index">
<a>{{ tab.displayName }}</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 99 }" (click)="activeTab = 99">
<a>Speed Tests</a>
</li>
</ul>
</div>
<div *ngFor="let tab of tabs; let i = index" [hidden]="activeTab !== i">
<h3>{{ tab.description }}</h3>
<ng-container *ngFor="let setting of tab.settings">
<h3 *ngIf="setting.type === 'Object'" class="title is-3" style="margin-top: 1.2rem">{{ setting.displayName }}</h3>
<div class="field">
<label class="label" *ngIf="setting.type !== 'Boolean' && setting.type !== 'Object'">{{
setting.displayName
}}</label>
<ng-container [ngSwitch]="setting.type">
<div class="control" *ngSwitchCase="'String'">
<input class="input" type="text" maxlength="100" [(ngModel)]="setting.value" />
</div>
<div class="control" *ngSwitchCase="'Int32'">
<input class="input" type="number" [(ngModel)]="setting.value" />
</div>
<label class="checkbox" *ngSwitchCase="'Boolean'">
<input type="checkbox" [(ngModel)]="setting.value" />
{{ setting.displayName }}
</label>
<div class="control select is-fullwidth" *ngSwitchCase="'Enum'">
<select [(ngModel)]="setting.value">
<option [value]="kvp.key" *ngFor="let kvp of setting.enumValues | keyvalue">{{ kvp.value }}</option>
</select>
</div>
<ng-container *ngSwitchCase="'Object'"></ng-container>
<div class="control" *ngSwitchDefault>Invalid setting type {{ setting.type }}</div>
</ng-container>
<p class="help" [innerHtml]="setting.description | nl2br"></p>
<ng-container *ngIf="setting.key === 'DownloadClient:Aria2cSecret'">
<button
class="button is-warning"
(click)="testAria2cConnection()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
>
Test aria2 connection
</button>
<div class="notification is-danger is-light" style="margin-top: 1rem" *ngIf="testAria2cConnectionError">
Could connect to Aria2 client<br />
{{ testAria2cConnectionError }}
</div>
<div class="notification is-success is-light" style="margin-top: 1rem" *ngIf="testAria2cConnectionSuccess">
Found Aria2 client version {{ testAria2cConnectionSuccess }}
</div>
</ng-container>
</div>
</ng-container>
</div>
<div *ngIf="activeTab === 99">
<div class="field">
<label class="label">Test download path permissions</label>
<div class="control">
<button
class="button is-warning"
(click)="testDownloadPath()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testPathError && !testPathSuccess"
>
Test permissions
</button>
<div class="notification is-danger is-light" *ngIf="testPathError">
Could not test your download path<br />
{{ testPathError }}
</div>
<div class="notification is-success is-light" *ngIf="testPathSuccess">Your download path looks good!</div>
</div>
<div class="help">This will check if the download folder has write permissions.</div>
</div>
<div class="field">
<label class="label">Test Real-Debrid download speed</label>
<div class="control">
<button
class="button is-warning"
(click)="testDownloadSpeed()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testDownloadSpeedError && !testDownloadSpeedSuccess"
>
Test download speed
</button>
<div class="notification is-danger is-light" *ngIf="testDownloadSpeedError">
Could not test your download speed<br />
{{ testDownloadSpeedError }}
</div>
<div class="notification is-success is-light" *ngIf="testDownloadSpeedSuccess">
Download speed {{ testDownloadSpeedSuccess | filesize }}/s
</div>
</div>
<div class="help">
This will attempt to download a 10GB file from Real-Debrid. When 50MB has been downloaded the test will stop.
</div>
</div>
<div class="field">
<label class="label">Test download folder write speed</label>
<div class="control">
<button
class="button is-warning"
(click)="testWriteSpeed()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testWriteSpeedError && !testWriteSpeedSuccess"
>
Test write speed
</button>
<div class="notification is-danger is-light" *ngIf="testWriteSpeedError">
Could not test your download speed<br />
{{ testWriteSpeedError }}
</div>
<div class="notification is-success is-light" *ngIf="testWriteSpeedSuccess">
Write speed {{ testWriteSpeedSuccess | filesize }}/s
</div>
</div>
<div class="help">This will write a small file to your download folder to see how fast it can write to it.</div>
</div>
</div>
<div class="field">
<div class="control">
<div class="notification is-danger is-light" *ngIf="error?.length > 0">Error saving settings: {{ error }}</div>
</div>
</div>
<div class="field" *ngIf="activeTab < 99">
<div class="control">
<button class="button is-success" (click)="ok()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">
Save Settings
</button>
</div>
</div>

View file

@ -0,0 +1,141 @@
import { Component, OnInit } from '@angular/core';
import { SettingsService } from 'src/app/settings.service';
import { Setting } from '../models/setting.model';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
})
export class SettingsComponent implements OnInit {
public activeTab = 0;
public tabs: Setting[] = [];
public saving = false;
public error: string;
public testPathError: string;
public testPathSuccess: boolean;
public testDownloadSpeedError: string;
public testDownloadSpeedSuccess: number;
public testWriteSpeedError: string;
public testWriteSpeedSuccess: number;
public testAria2cConnectionError: string = null;
public testAria2cConnectionSuccess: string = null;
constructor(private settingsService: SettingsService) {}
ngOnInit(): void {
this.reset();
}
public reset(): void {
this.settingsService.get().subscribe((settings) => {
this.tabs = settings.where((m) => m.key.indexOf(':') === -1);
for (let tab of this.tabs) {
tab.settings = settings.where((m) => m.key.indexOf(`${tab.key}:`) > -1);
}
});
}
public ok(): void {
this.saving = true;
const settingsToSave = this.tabs.selectMany((m) => m.settings).where((m) => m.type !== 'Object');
this.settingsService.update(settingsToSave).subscribe(
() => {
setTimeout(() => {
this.saving = false;
}, 1000);
},
(err) => {
this.saving = false;
this.error = err;
}
);
}
public testDownloadPath(): void {
const settingDownloadPath = this.tabs
.first((m) => m.key === 'DownloadClient')
.settings.first((m) => m.key === 'DownloadClient:DownloadPath').value as string;
this.saving = true;
this.testPathError = null;
this.testPathSuccess = false;
this.settingsService.testPath(settingDownloadPath).subscribe(
() => {
this.saving = false;
this.testPathSuccess = true;
},
(err) => {
this.testPathError = err.error;
this.saving = false;
}
);
}
public testDownloadSpeed(): void {
this.saving = true;
this.testDownloadSpeedError = null;
this.testDownloadSpeedSuccess = 0;
this.settingsService.testDownloadSpeed().subscribe(
(result) => {
this.saving = false;
this.testDownloadSpeedSuccess = result;
},
(err) => {
this.testDownloadSpeedError = err.error;
this.saving = false;
}
);
}
public testWriteSpeed(): void {
this.saving = true;
this.testWriteSpeedError = null;
this.testWriteSpeedSuccess = 0;
this.settingsService.testWriteSpeed().subscribe(
(result) => {
this.saving = false;
this.testWriteSpeedSuccess = result;
},
(err) => {
this.testWriteSpeedError = err.error;
this.saving = false;
}
);
}
public testAria2cConnection(): void {
const settingAria2cUrl = this.tabs
.first((m) => m.key === 'DownloadClient')
.settings.first((m) => m.key === 'DownloadClient:Aria2cUrl').value as string;
const settingAria2cSecret = this.tabs
.first((m) => m.key === 'DownloadClient')
.settings.first((m) => m.key === 'DownloadClient:Aria2cSecret').value as string;
this.saving = true;
this.testAria2cConnectionError = null;
this.testAria2cConnectionSuccess = null;
this.settingsService.testAria2cConnection(settingAria2cUrl, settingAria2cSecret).subscribe(
(result) => {
this.saving = false;
this.testAria2cConnectionSuccess = result.version;
},
(err) => {
this.testAria2cConnectionError = err.error;
this.saving = false;
}
);
}
}

View file

@ -6,64 +6,93 @@
<img src="../../assets/logo.png" /> <img src="../../assets/logo.png" />
<div class="box" *ngIf="step === 1"> <div class="box" *ngIf="step === 1">
<div class="notification is-primary"> <div class="notification is-primary">
Welcome to RealDebrid Client. Please create your account by entering a username and password. Welcome to Real-Debrid Client. Please create your account by entering a username and password.
</div> </div>
<div class="field"> <form (ngSubmit)="setup()">
<label for="" class="label">Username</label> <div class="field">
<div class="control has-icons-left"> <label for="" class="label">Username</label>
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required /> <div class="control has-icons-left">
<span class="icon is-small is-left"> <input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required />
<i class="fa fa-envelope"></i> <span class="icon is-small is-left">
</span> <i class="fa fa-envelope"></i>
</span>
</div>
</div> </div>
</div> <div class="field">
<div class="field"> <label for="" class="label" name="password">Password</label>
<label for="" class="label" name="password">Password</label> <div class="control has-icons-left">
<div class="control has-icons-left"> <input type="password" class="input" required name="password" [(ngModel)]="password" />
<input type="password" class="input" required name="password" [(ngModel)]="password" /> <span class="icon is-small is-left">
<span class="icon is-small is-left"> <i class="fa fa-lock"></i>
<i class="fa fa-lock"></i> </span>
</span> </div>
</div> </div>
</div> <div class="field">
<div class="field"> <button class="button is-success" [ngClass]="{ 'is-loading': working }" [disabled]="working">
<button Setup
class="button is-success" </button>
(click)="setup()" </div>
[ngClass]="{ 'is-loading': working }" <div class="notification is-danger is-light" *ngIf="error?.length > 0">
[disabled]="working" {{ error }}
> </div>
Setup </form>
</button>
</div>
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
{{ error }}
</div>
</div> </div>
<div class="box" *ngIf="step === 2"> <div class="box" *ngIf="step === 2">
<div class="notification is-primary"> <div class="notification is-primary">
To be able to use the RealDebrid Client you need a premium subscription to download torrents. To be able to use the Real-Debrid Client you need a premium subscription to download torrents.
<br /><br /> <br /><br />
Not premium yet? Not premium yet? You have the choice of 2 providers:
<br />
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener" <a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener"
>Use this link to sign up to RealDebrid.</a >Use this link to sign up to Real-Debrid.</a
> >
<br /><br /> <br />
To connect to RealDebrid please enter your Personal Prive API Token found here: <a href="https://alldebrid.com/?uid=2v91l&lang=en" target="_blank" rel="noopener"
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener" >Use this link to sign up to AllDebrid.</a
>https://real-debrid.com/apitoken</a >
>. <br />
<a href="https://www.premiumize.me/" target="_blank" rel="noopener"
>Use this link to sign up to Premiumize.</a
>
<br />
<small>(Referal links)</small>
</div>
<div class="field">
<label class="label">Provider</label>
<div class="control select is-fullwidth">
<select [(ngModel)]="provider">
<option [value]="0">Real-Debrid</option>
<option [value]="1">AllDebrid</option>
<option [value]="2">Premiumize</option>
</select>
</div>
</div> </div>
<div class="field"> <div class="field">
<label for="" class="label">API Private Token</label> <label for="" class="label">API Private Token</label>
<div class="control has-icons-left"> <div class="control">
<input type="text" placeholder="" class="input" name="token" [(ngModel)]="token" required /> <input type="text" placeholder="" class="input" name="token" [(ngModel)]="token" required />
<span class="icon is-small is-left">
<i class="fa fa-envelope"></i>
</span>
</div> </div>
<p class="help" *ngIf="provider === 'RealDebrid'">
You can find your API key here:
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener"
>https://real-debrid.com/apitoken</a
>.
</p>
<p class="help" *ngIf="provider === 'AllDebrid'">
You can find your API key here:
<a href="https://alldebrid.com/apikeys/" target="_blank" rel="noopener"
>https://alldebrid.com/apikeys/</a
>.
</p>
<p class="help" *ngIf="provider === 'Premiumize'">
You can find your API key here:
<a href="https://www.premiumize.me/account" target="_blank" rel="noopener"
>https://www.premiumize.me/account</a
>.
</p>
</div> </div>
<div class="field"> <div class="field">
<button <button
class="button is-success" class="button is-success"
@ -81,7 +110,7 @@
<div class="box" *ngIf="step === 3"> <div class="box" *ngIf="step === 3">
<div class="notification is-primary"> <div class="notification is-primary">
You are now connected to RealDebrid! To setup Radarr or Sonarr, please following the instructions here: You are now connected to Real-Debrid! To setup Radarr or Sonarr, please following the instructions here:
<a href="https://github.com/rogerfar/rdt-client/" target="_blank" rel="noopener" <a href="https://github.com/rogerfar/rdt-client/" target="_blank" rel="noopener"
>https://github.com/rogerfar/rdt-client/</a >https://github.com/rogerfar/rdt-client/</a
>. >.

View file

@ -1,17 +1,16 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
import { Setting } from '../models/setting.model';
import { SettingsService } from '../settings.service';
@Component({ @Component({
selector: 'app-setup', selector: 'app-setup',
templateUrl: './setup.component.html', templateUrl: './setup.component.html',
styleUrls: ['./setup.component.scss'], styleUrls: ['./setup.component.scss'],
}) })
export class SetupComponent implements OnInit { export class SetupComponent {
public userName: string; public userName: string;
public password: string; public password: string;
public provider = 'RealDebrid';
public token: string; public token: string;
public error: string; public error: string;
@ -19,9 +18,7 @@ export class SetupComponent implements OnInit {
public step: number = 1; public step: number = 1;
constructor(private authService: AuthService, private settingsService: SettingsService, private router: Router) {} constructor(private authService: AuthService, private router: Router) {}
ngOnInit(): void {}
public setup(): void { public setup(): void {
this.error = null; this.error = null;
@ -40,16 +37,12 @@ export class SetupComponent implements OnInit {
} }
public setToken(): void { public setToken(): void {
const setting = new Setting(); this.authService.setupProvider(this.provider, this.token).subscribe(
setting.settingId = 'RealDebridApiKey';
setting.value = this.token;
this.settingsService.update([setting]).subscribe(
() => { () => {
this.step = 3; this.step = 3;
this.working = false; this.working = false;
}, },
(err) => { (err: any) => {
this.working = false; this.working = false;
this.error = err.error; this.error = err.error;
} }

View file

@ -1,12 +0,0 @@
<td colspan="4">
<i class="fas fa-download" aria-hidden="true"></i>
<span *ngIf="download.link">{{ download.link }}</span>
<span *ngIf="!download.link">{{ download.path }}</span>
</td>
<td>
{{ download.bytesTotal | filesize }}
</td>
<td>
{{ download | downloadStatus }}
</td>
<td></td>

View file

@ -1,3 +0,0 @@
td {
font-size: smaller;
}

View file

@ -1,16 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { Download } from '../models/download.model';
@Component({
selector: '[app-torrent-download]',
templateUrl: './torrent-download.component.html',
styleUrls: ['./torrent-download.component.scss'],
})
export class TorrentDownloadComponent implements OnInit {
@Input()
public download: Download;
constructor() {}
ngOnInit(): void {}
}

View file

@ -1,6 +0,0 @@
<td colspan="4">
<i class="fas fa-file" aria-hidden="true"></i>
{{ file.path }}
</td>
<td>{{ file.bytes | filesize }}</td>
<td colspan="10"></td>

View file

@ -1,3 +0,0 @@
td {
font-size: smaller;
}

View file

@ -1,16 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { TorrentFile } from '../models/torrent.model';
@Component({
selector: '[app-torrent-file]',
templateUrl: './torrent-file.component.html',
styleUrls: ['./torrent-file.component.scss'],
})
export class TorrentFileComponent implements OnInit {
@Input()
public file: TorrentFile;
constructor() {}
ngOnInit(): void {}
}

View file

@ -1,27 +0,0 @@
<td>{{ torrent.rdName }}</td>
<td>
{{ torrent.files.length | number }}
</td>
<td>
{{ torrent.downloads.length | number }}
</td>
<td class="auto">
<i class="fas fa-trash" *ngIf="torrent.autoDelete" title="Auto delete"></i>
</td>
<td>
{{ torrent.rdSize | filesize }}
</td>
<td>
{{ torrent | status }}
</td>
<td>
<span class="icon sync-icon" (click)="retryClick($event)" title="Retry" *ngIf="!loading">
<i class="fas fa-sync"></i>
</span>
<span class="icon delete-icon" (click)="deleteClick($event)" title="Delete torrent" *ngIf="!loading">
<i class="fas fa-times"></i>
</span>
<span class="icon loading-icon" *ngIf="loading">
<i class="fas fa-spinner fa-pulse"></i>
</span>
</td>

View file

@ -1,8 +0,0 @@
.delete-icon {
color: red;
cursor: pointer;
}
.auto .fas {
margin-right: 4px;
}

View file

@ -1,34 +0,0 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Torrent } from 'src/app/models/torrent.model';
@Component({
selector: '[app-torrent-row]',
templateUrl: './torrent-row.component.html',
styleUrls: ['./torrent-row.component.scss'],
})
export class TorrentRowComponent implements OnInit {
@Input()
public torrent: Torrent;
@Output('delete')
public delete = new EventEmitter();
@Output('retry')
public retry = new EventEmitter();
public loading = false;
constructor() {}
ngOnInit(): void {}
public deleteClick(event: Event): void {
event.stopPropagation();
this.delete.emit(this.torrent.torrentId);
}
public retryClick(event: Event): void {
event.stopPropagation();
this.retry.emit(this.torrent.torrentId);
}
}

View file

@ -9,6 +9,10 @@ export class TorrentStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {} constructor(private pipe: FileSizePipe) {}
transform(torrent: Torrent): string { transform(torrent: Torrent): string {
if (torrent.error) {
return torrent.error;
}
if (torrent.downloads.length > 0) { if (torrent.downloads.length > 0) {
const errors = torrent.downloads.where((m) => m.error != null); const errors = torrent.downloads.where((m) => m.error != null);
@ -22,33 +26,44 @@ export class TorrentStatusPipe implements PipeTransform {
return 'Finished'; return 'Finished';
} }
const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished); const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished && m.bytesDone > 0);
const downloaded = torrent.downloads.where((m) => m.downloadFinished != null);
if (downloading.length > 0) { if (downloading.length > 0) {
const bytesDone = downloading.sum((m) => m.bytesDone); const bytesDone = downloading.sum((m) => m.bytesDone);
const bytesTotal = downloading.sum((m) => m.bytesTotal); const bytesTotal = downloading.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100; let progress = (bytesDone / bytesTotal) * 100;
let allSpeeds = downloading.sum((m) => m.speed) / downloading.length;
if (isNaN(progress)) {
progress = 0;
}
let allSpeeds = downloading.sum((m) => m.speed);
let speed: string | string[] = '0'; let speed: string | string[] = '0';
if (allSpeeds > 0) {
speed = this.pipe.transform(allSpeeds, 'filesize');
return `Downloading (${progress.toFixed(2)}% - ${speed}/s)`; speed = this.pipe.transform(allSpeeds, 'filesize');
}
return `Downloading file ${downloading.length + downloaded.length}/${
torrent.downloads.length
} (${progress.toFixed(2)}% - ${speed}/s)`;
} }
const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished); const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished && m.bytesDone > 0);
const unpacked = torrent.downloads.where((m) => m.unpackingFinished != null);
if (unpacking.length > 0) { if (unpacking.length > 0) {
const bytesDone = unpacking.sum((m) => m.bytesDone); const bytesDone = unpacking.sum((m) => m.bytesDone);
const bytesTotal = unpacking.sum((m) => m.bytesTotal); const bytesTotal = unpacking.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100; let progress = (bytesDone / bytesTotal) * 100;
let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length;
if (allSpeeds > 0) { if (isNaN(progress)) {
return `Extracting (${progress.toFixed(2)}%)`; progress = 0;
} }
return `Extracting file ${unpacking.length + unpacked.length}/${torrent.downloads.length} (${progress.toFixed(
2
)}%)`;
} }
const queuedForUnpacking = torrent.downloads.where((m) => m.unpackingQueued && !m.unpackingStarted); const queuedForUnpacking = torrent.downloads.where((m) => m.unpackingQueued && !m.unpackingStarted);
@ -57,20 +72,16 @@ export class TorrentStatusPipe implements PipeTransform {
return `Queued for unpacking`; return `Queued for unpacking`;
} }
const queuedForDownload = torrent.downloads.where((m) => m.downloadQueued && !m.downloadStarted); const queuedForDownload = torrent.downloads.where((m) => !m.downloadStarted && !m.downloadFinished);
if (queuedForDownload.length > 0) { if (queuedForDownload.length > 0) {
return `Queued for downloading`; return `Queued for downloading`;
} }
const unpacked = torrent.downloads.where((m) => m.unpackingFinished != null);
if (unpacked.length > 0) { if (unpacked.length > 0) {
return `Files unpacked`; return `Files unpacked`;
} }
const downloaded = torrent.downloads.where((m) => m.downloadFinished != null);
if (downloaded.length > 0) { if (downloaded.length > 0) {
return `Files downloaded to host`; return `Files downloaded to host`;
} }
@ -91,7 +102,9 @@ export class TorrentStatusPipe implements PipeTransform {
case RealDebridStatus.Error: case RealDebridStatus.Error:
return `Torrent error: ${torrent.rdStatusRaw}`; return `Torrent error: ${torrent.rdStatusRaw}`;
case RealDebridStatus.Finished: case RealDebridStatus.Finished:
return `Torrent finished, waiting to download`; return `Torrent finished, waiting for download links`;
case RealDebridStatus.Uploading:
return `Torrent uploading`;
default: default:
return 'Unknown status'; return 'Unknown status';
} }

View file

@ -6,58 +6,86 @@
<table class="table is-fullwidth is-hoverable"> <table class="table is-fullwidth is-hoverable">
<thead> <thead>
<tr> <tr>
<th>
<input
type="checkbox"
(click)="toggleSelectAll($event)"
[checked]="selectedTorrents.length > 0 && selectedTorrents.length === torrents.length"
/>
</th>
<th>Name</th> <th>Name</th>
<th>Category</th>
<th>Priority</th>
<th>Files</th> <th>Files</th>
<th>Downloads</th> <th>Downloads</th>
<th>Auto</th>
<th>Size</th> <th>Size</th>
<th>Status</th> <th>Status</th>
<th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<ng-container *ngFor="let torrent of torrents; trackBy: trackByMethod"> <tr *ngFor="let torrent of torrents; trackBy: trackByMethod">
<tr <td>
app-torrent-row <input
[torrent]="torrent" type="checkbox"
(click)="selectTorrent(torrent)" (click)="toggleSelect(torrent.torrentId)"
(delete)="showDeleteModal($event)" [checked]="selectedTorrents.contains(torrent.torrentId)"
(retry)="showRetryModal($event)" />
></tr> </td>
<ng-container *ngIf="showFiles[torrent.torrentId]"> <td (click)="openTorrent(torrent.torrentId)">
<tr class="separator"> {{ torrent.rdName }}
<td colspan="10">Downloads</td> </td>
</tr> <td>
<tr app-torrent-download [download]="download" *ngFor="let download of torrent.downloads"></tr> {{ torrent.category }}
<tr class="separator"> </td>
<td colspan="10">Files in torrent</td> <td>
</tr> {{ torrent.priority }}
<tr app-torrent-file [file]="file" *ngFor="let file of torrent.files"></tr> </td>
</ng-container> <td>
</ng-container> {{ torrent.files.length | number }}
</td>
<td>
{{ torrent.downloads.length | number }}
</td>
<td>
{{ torrent.rdSize | filesize }}
</td>
<td>
{{ torrent | status }}
</td>
</tr>
</tbody> </tbody>
</table> </table>
<button
class="button is-danger"
(click)="showDeleteModal()"
[disabled]="selectedTorrents.length === 0"
*ngIf="torrents.length > 0"
>
Delete Selected
</button>
</div> </div>
<div class="modal" [class.is-active]="isDeleteModalActive"> <div class="modal" [class.is-active]="isDeleteModalActive">
<div class="modal-background"></div> <div class="modal-background"></div>
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head"> <header class="modal-card-head">
<p class="modal-card-title">Delete torrent</p> <p class="modal-card-title">Delete selected torrents</p>
<button class="delete" aria-label="close" (click)="deleteCancel()"></button> <button class="delete" aria-label="close" (click)="deleteCancel()"></button>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
<p>Are you sure you want to delete these torrent?</p>
<div class="field"> <div class="field">
<label class="label"></label> <label class="label"></label>
<div class="control"> <div class="control">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteData" /> <input type="checkbox" [(ngModel)]="deleteData" />
Delete Torrent from local RealDebrid Client Delete Torrents from client
</label> </label>
<br /> <br />
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteRdTorrent" /> <input type="checkbox" [(ngModel)]="deleteRdTorrent" />
Delete Torrent from RealDebrid Delete Torrents from provider
</label> </label>
<br /> <br />
<label class="checkbox"> <label class="checkbox">
@ -67,7 +95,7 @@
</div> </div>
</div> </div>
<div class="notification is-primary"> <div class="notification is-primary">
Deleting a torrent from RealDebrid will automatically delete it here too. Deleting a torrent from Real-Debrid will automatically delete it here too.
</div> </div>
<div class="notification is-danger is-light" *ngIf="deleteError?.length > 0"> <div class="notification is-danger is-light" *ngIf="deleteError?.length > 0">
Error deleting torrent: {{ deleteError }} Error deleting torrent: {{ deleteError }}
@ -80,7 +108,7 @@
[disabled]="deleting" [disabled]="deleting"
[ngClass]="{ 'is-loading': deleting }" [ngClass]="{ 'is-loading': deleting }"
> >
Delete Delete selected
</button> </button>
<button class="button" (click)="deleteCancel()" [disabled]="deleting" [ngClass]="{ 'is-loading': deleting }"> <button class="button" (click)="deleteCancel()" [disabled]="deleting" [ngClass]="{ 'is-loading': deleting }">
Cancel Cancel
@ -88,45 +116,3 @@
</footer> </footer>
</div> </div>
</div> </div>
<div class="modal" [class.is-active]="isRetryModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Retry torrent</p>
<button class="delete" aria-label="close" (click)="retryCancel()"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">Retry</label>
<div class="control">
<label class="radio">
<input type="radio" name="retry" [value]="0" [(ngModel)]="retry" />
Retry on Real-Debrid
</label>
<br />
<label class="radio">
<input type="radio" name="retry" [value]="1" [(ngModel)]="retry" />
Retry downloading locally
</label>
</div>
</div>
<div class="notification is-danger is-light" *ngIf="retryError?.length > 0">
Error retrying torrent: {{ retryError }}
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="retryOk()"
[disabled]="retrying"
[ngClass]="{ 'is-loading': retrying }"
>
Retry
</button>
<button class="button" (click)="retryCancel()" [disabled]="retrying" [ngClass]="{ 'is-loading': retrying }">
Cancel
</button>
</footer>
</div>
</div>

View file

@ -2,11 +2,4 @@ table {
tr { tr {
cursor: pointer; cursor: pointer;
} }
tr.separator {
td {
font-size: smaller;
font-weight: bold;
}
}
} }

View file

@ -1,40 +1,33 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Torrent } from '../models/torrent.model'; import { Torrent } from '../models/torrent.model';
import { TorrentService } from '../torrent.service'; import { TorrentService } from '../torrent.service';
import { forkJoin, Observable } from 'rxjs';
@Component({ @Component({
selector: 'app-torrent-table', selector: 'app-torrent-table',
templateUrl: './torrent-table.component.html', templateUrl: './torrent-table.component.html',
styleUrls: ['./torrent-table.component.scss'], styleUrls: ['./torrent-table.component.scss'],
}) })
export class TorrentTableComponent implements OnInit, OnDestroy { export class TorrentTableComponent implements OnInit {
public torrents: Torrent[] = []; public torrents: Torrent[] = [];
public selectedTorrents: string[] = [];
public error: string; public error: string;
public showFiles: { [key: string]: boolean } = {};
public isDeleteModalActive: boolean; public isDeleteModalActive: boolean;
public deleteError: string; public deleteError: string;
public deleting: boolean; public deleting: boolean;
public deleteTorrentId: string;
public deleteData: boolean; public deleteData: boolean;
public deleteRdTorrent: boolean; public deleteRdTorrent: boolean;
public deleteLocalFiles: boolean; public deleteLocalFiles: boolean;
public isRetryModalActive: boolean; constructor(private router: Router, private torrentService: TorrentService) {}
public retryError: string;
public retrying: boolean;
public retryTorrentId: string;
public retry: number;
constructor(private torrentService: TorrentService) {}
ngOnInit(): void { ngOnInit(): void {
this.torrentService.getList().subscribe( this.torrentService.getList().subscribe(
(result) => { (result) => {
this.torrents = result; this.torrents = result;
this.torrentService.connect();
this.torrentService.update$.subscribe((result2) => { this.torrentService.update$.subscribe((result2) => {
this.torrents = result2; this.torrents = result2;
}); });
@ -45,24 +38,40 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
); );
} }
ngOnDestroy(): void { public openTorrent(torrentId: string): void {
this.torrentService.disconnect(); this.router.navigate([`/torrent/${torrentId}`]);
}
public selectTorrent(torrent: Torrent): void {
this.showFiles[torrent.torrentId] = !this.showFiles[torrent.torrentId];
} }
public trackByMethod(index: number, el: Torrent): string { public trackByMethod(index: number, el: Torrent): string {
return el.torrentId; return el.torrentId;
} }
public showDeleteModal(torrentId: string): void { public toggleSelectAll(event: any) {
this.selectedTorrents = [];
if (event.target.checked) {
this.torrents.map((torrent) => {
this.selectedTorrents.push(torrent.torrentId);
});
}
}
public toggleSelect(torrentId: string) {
const index = this.selectedTorrents.indexOf(torrentId);
if (index > -1) {
this.selectedTorrents.splice(index, 1);
} else {
this.selectedTorrents.push(torrentId);
}
}
public showDeleteModal(): void {
this.deleteData = false; this.deleteData = false;
this.deleteRdTorrent = false; this.deleteRdTorrent = false;
this.deleteLocalFiles = false; this.deleteLocalFiles = false;
this.deleteError = null;
this.deleteTorrentId = torrentId;
this.isDeleteModalActive = true; this.isDeleteModalActive = true;
} }
@ -73,43 +82,23 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
public deleteOk(): void { public deleteOk(): void {
this.deleting = true; this.deleting = true;
this.torrentService let calls: Observable<void>[] = [];
.delete(this.deleteTorrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
.subscribe(
() => {
this.isDeleteModalActive = false;
this.deleting = false;
},
(err) => {
this.deleteError = err.error;
this.deleting = false;
}
);
}
public showRetryModal(torrentId: string): void { this.selectedTorrents.forEach((torrentId) => {
this.retry = 0; calls.push(this.torrentService.delete(torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles));
});
this.retryTorrentId = torrentId; forkJoin(calls).subscribe({
this.isRetryModalActive = true; complete: () => {
} this.isDeleteModalActive = false;
this.deleting = false;
public retryCancel(): void { this.selectedTorrents = [];
this.isRetryModalActive = false;
}
public retryOk(): void {
this.retrying = true;
this.torrentService.retry(this.retryTorrentId, this.retry).subscribe(
() => {
this.isRetryModalActive = false;
this.retrying = false;
}, },
(err) => { error: (err) => {
this.retryError = err.error; this.deleteError = err.error;
this.retrying = false; this.deleting = false;
} },
); });
} }
} }

View file

@ -2,19 +2,25 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import * as signalR from '@microsoft/signalr'; import * as signalR from '@microsoft/signalr';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { Torrent } from './models/torrent.model'; import { Torrent, TorrentFileAvailability } from './models/torrent.model';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class TorrentService { export class TorrentService {
constructor(private http: HttpClient) {}
public update$: Subject<Torrent[]> = new Subject(); public update$: Subject<Torrent[]> = new Subject();
private connection: signalR.HubConnection; private connection: signalR.HubConnection;
constructor(private http: HttpClient) {
this.connect();
}
public connect(): void { public connect(): void {
if (this.connection != null) {
return;
}
this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build(); this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build();
this.connection.start().catch((err) => console.error(err)); this.connection.start().catch((err) => console.error(err));
@ -23,38 +29,38 @@ export class TorrentService {
}); });
} }
public disconnect(): void {
this.connection?.stop();
}
public getList(): Observable<Torrent[]> { public getList(): Observable<Torrent[]> {
return this.http.get<Torrent[]>(`/Api/Torrents`); return this.http.get<Torrent[]>(`/Api/Torrents`);
} }
public uploadMagnet(magnetLink: string, autoDelete: boolean): Observable<void> { public get(torrentId: string): Observable<Torrent> {
return this.http.get<Torrent>(`/Api/Torrents/Get/${torrentId}`);
}
public uploadMagnet(magnetLink: string, torrent: Torrent): Observable<void> {
return this.http.post<void>(`/Api/Torrents/UploadMagnet`, { return this.http.post<void>(`/Api/Torrents/UploadMagnet`, {
magnetLink, magnetLink,
autoDelete, torrent,
}); });
} }
public uploadFile(file: File, autoDelete: boolean): Observable<void> { public uploadFile(file: File, torrent: Torrent): Observable<void> {
const formData: FormData = new FormData(); const formData: FormData = new FormData();
formData.append('file', file); formData.append('file', file);
formData.append('formData', JSON.stringify({ autoDelete })); formData.append('formData', JSON.stringify({ torrent }));
return this.http.post<void>(`/Api/Torrents/UploadFile`, formData); return this.http.post<void>(`/Api/Torrents/UploadFile`, formData);
} }
public checkFilesMagnet(magnetLink: string): Observable<string[]> { public checkFilesMagnet(magnetLink: string): Observable<TorrentFileAvailability[]> {
return this.http.post<string[]>(`/Api/Torrents/CheckFilesMagnet`, { return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFilesMagnet`, {
magnetLink, magnetLink,
}); });
} }
public checkFiles(file: File): Observable<string[]> { public checkFiles(file: File): Observable<TorrentFileAvailability[]> {
const formData: FormData = new FormData(); const formData: FormData = new FormData();
formData.append('file', file); formData.append('file', file);
return this.http.post<string[]>(`/Api/Torrents/CheckFiles`, formData); return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFiles`, formData);
} }
public delete( public delete(
@ -70,9 +76,15 @@ export class TorrentService {
}); });
} }
public retry(torrentId: string, retry: number): Observable<void> { public retry(torrentId: string): Observable<void> {
return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, { return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, {});
retry, }
});
public retryDownload(downloadId: string): Observable<void> {
return this.http.post<void>(`/Api/Torrents/RetryDownload/${downloadId}`, {});
}
public update(torrent: Torrent): Observable<void> {
return this.http.put<void>(`/Api/Torrents/Update`, torrent);
} }
} }

View file

@ -0,0 +1,566 @@
<div class="tabs">
<ul>
<li [ngClass]="{ 'is-active': activeTab === 0 }" (click)="activeTab = 0">
<a>General</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 1 }" (click)="activeTab = 1">
<a>Torrent Files</a>
</li>
<li [ngClass]="{ 'is-active': activeTab === 2 }" (click)="activeTab = 2">
<a>Downloads</a>
</li>
</ul>
</div>
<div *ngIf="torrent === null || torrent === undefined">
<div class="fa-3x">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div *ngIf="torrent !== null && torrent !== undefined">
<div fxLayout.lt-lg="column" fxLayout.gt-sm="row" fxLayoutGap="20px" *ngIf="activeTab === 0">
<div fxFlex>
<div class="field is-grouped">
<div class="control">
<button class="button is-danger" (click)="showDeleteModal()">Delete Torrent</button>
</div>
<div class="control">
<button class="button is-primary" (click)="showRetryModal()">Retry Torrent</button>
</div>
<div class="control">
<button class="button is-light" (click)="showUpdateSettingsModal()">Change Settings</button>
</div>
</div>
<div class="field">
<label class="label">Status</label>
{{ torrent | status }}
</div>
<div class="field">
<label class="label">Retry count</label>
{{ torrent.retryCount }} / {{ torrent.torrentRetryAttempts }}
</div>
<div class="field">
<label class="label">Hash</label>
{{ torrent.hash }}
</div>
<div class="field">
<label class="label">Priority</label>
{{ torrent.priority || "" }}
</div>
<div class="field">
<label class="label">Category</label>
{{ torrent.category || "(no category set)" }}
</div>
<div class="field">
<label class="label">Post Download Action</label>
<ng-container [ngSwitch]="torrent.hostDownloadAction">
<ng-container *ngSwitchCase="0">Download all files to host</ng-container>
<ng-container *ngSwitchCase="1">Don't download files to host</ng-container>
</ng-container>
</div>
<div class="field">
<label class="label">Post Torrent Download Action</label>
<ng-container [ngSwitch]="torrent.downloadAction">
<ng-container *ngSwitchCase="0">Download all files above a certain size</ng-container>
<ng-container *ngSwitchCase="1"
>Download all available files on Real-Debrid above a certain size</ng-container
>
<ng-container *ngSwitchCase="2">Pick files I want to download</ng-container>
</ng-container>
</div>
<div class="field">
<label class="label">Finished action</label>
<ng-container [ngSwitch]="torrent.finishedAction">
<ng-container *ngSwitchCase="0">Do nothing</ng-container>
<ng-container *ngSwitchCase="1">Remove torrent from Real-Debrid and Real-Debrid Client</ng-container>
<ng-container *ngSwitchCase="2">Remove torrent from Real-Debrid</ng-container>
<ng-container *ngSwitchCase="3">Remove torrent from client</ng-container>
</ng-container>
</div>
<div class="field">
<label class="label">Minimum file size to download</label>
{{ torrent.downloadMinSize }}MB
</div>
<div class="field" *ngIf="!torrent.isFile">
<label class="label">Magnet</label>
<span [cdkCopyToClipboard]="torrent.fileOrMagnet" (click)="copied = true" *ngIf="!copied"
>Click to copy magnet link to clipboard</span
>
<span *ngIf="copied">Link copied to clipboard!</span>
</div>
<div class="field" *ngIf="torrent.isFile">
<label class="label">Torrent file</label>
<span (click)="download()">Click to download torrent file</span>
</div>
<div class="field">
<label class="label">Added on</label>
{{ torrent.added | date: "fullDate" }} {{ torrent.added | date: "mediumTime" }}
</div>
<div class="field">
<label class="label">Files selected on</label>
<ng-container *ngIf="torrent.filesSelected === null">(no files selected yet) </ng-container>
<ng-container *ngIf="torrent.filesSelected !== null">
{{ torrent.filesSelected | date: "fullDate" }} {{ torrent.filesSelected | date: "mediumTime" }}
</ng-container>
</div>
<div class="field">
<label class="label">Completed on</label>
<ng-container *ngIf="torrent.completed === null">(not completed yet) </ng-container>
<ng-container *ngIf="torrent.completed !== null">
{{ torrent.completed | date: "fullDate" }} {{ torrent.completed | date: "mediumTime" }}
</ng-container>
</div>
</div>
<div fxFlex>
<div class="field">
<label class="label">Real-Debrid ID</label>
{{ torrent.rdId }}
</div>
<div class="field">
<label class="label">Real-Debrid Name</label>
{{ torrent.rdName }}
</div>
<div class="field">
<label class="label">Real-Debrid Size</label>
{{ torrent.rdSize | filesize }}
</div>
<div class="field">
<label class="label">Real-Debrid Host</label>
{{ torrent.rdHost }}
</div>
<div class="field">
<label class="label">Real-Debrid Split</label>
{{ torrent.rdSplit }}
</div>
<div class="field">
<label class="label">Real-Debrid Progress</label>
{{ torrent.rdProgress || 0 }}%
</div>
<div class="field">
<label class="label">Real-Debrid Status</label>
<ng-container [ngSwitch]="torrent.rdStatus">
<ng-container *ngSwitchCase="0">Processing</ng-container>
<ng-container *ngSwitchCase="1">Waiting For File Selection</ng-container>
<ng-container *ngSwitchCase="2">Downloading</ng-container>
<ng-container *ngSwitchCase="3">Finished</ng-container>
<ng-container *ngSwitchCase="99">Error</ng-container>
</ng-container>
({{ torrent.rdStatusRaw }})
</div>
<div class="field">
<label class="label">Real-Debrid Added</label>
{{ torrent.rdAdded | date: "fullDate" }} {{ torrent.rdAdded | date: "mediumTime" }}
</div>
<div class="field">
<label class="label">Real-Debrid Ended</label>
{{ torrent.rdEnded | date: "fullDate" }} {{ torrent.rdEnded | date: "mediumTime" }}
</div>
<div class="field">
<label class="label">Real-Debrid Speed</label>
{{ torrent.rdSpeed || 0 }}
</div>
<div class="field">
<label class="label">Real-Debrid Seeders</label>
{{ torrent.rdSeeders || "0" }}
</div>
</div>
</div>
<div *ngIf="activeTab === 1">
<div class="field">
<table class="table is-fullwidth">
<thead>
<tr>
<th>ID</th>
<th>Path</th>
<th>Size</th>
<th>Selected</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let file of torrent.files">
<td>
{{ file.id }}
</td>
<td>
{{ file.path }}
</td>
<td>
{{ file.bytes | filesize }}
</td>
<td>
<i class="fas fa-check" *ngIf="file.selected" style="color: green"></i>
<i class="fas fa-times" *ngIf="!file.selected" style="color: red"></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div *ngIf="activeTab === 2">
<div class="field">
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th style="width: 35px"></th>
<th>Link</th>
<th>Size</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let download of torrent.downloads">
<tr (click)="downloadExpanded[download.downloadId] = !downloadExpanded[download.downloadId]">
<td style="width: 35px">
<i class="fas fa-caret-right" *ngIf="!downloadExpanded[download.downloadId]"></i>
<i class="fas fa-caret-down" *ngIf="downloadExpanded[download.downloadId]"></i>
</td>
<td>
<ng-container *ngIf="download.link">
{{ download.link | decodeURI }}
</ng-container>
<ng-container *ngIf="!download.link">
{{ download.path }}
</ng-container>
</td>
<td>
{{ download.bytesTotal | filesize }}
</td>
<td>
{{ download | downloadStatus }}
</td>
</tr>
<tr *ngIf="downloadExpanded[download.downloadId]" class="separator">
<td style="width: 35px"></td>
<td colspan="5">
<div fxLayout.lt-lg="column" fxLayout.gt-sm="row" fxLayoutGap="20px">
<div fxFlex>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" (click)="showDownloadRetryModal(download.downloadId)">
Retry Download
</button>
</div>
</div>
<div class="field" *ngIf="download.error">
<label class="label">Error</label>
{{ download.error }}
</div>
<div class="field">
<label class="label">Real-Debrid Unrestricted Link</label>
<a href="{{ download.link }}" target="_blank" *ngIf="download.link">
{{ download.link | decodeURI }}</a
>
</div>
<div class="field">
<label class="label">Real-Debrid Link</label>
{{ download.path }}
</div>
<div class="field">
<label class="label">Download</label>
{{ download.bytesDone | filesize }} / {{ download.bytesTotal | filesize }} ({{
download.speed | filesize
}}/s)
</div>
<div class="field">
<label class="label">Retry Count</label>
{{ download.retryCount }} / {{ torrent.downloadRetryAttempts }}
</div>
</div>
<div fxFlex>
<div class="field">
<label class="label">Added</label>
<ng-container *ngIf="download.added">
{{ download.added | date: "fullDate" }} {{ download.added | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.added">(not added yet) </ng-container>
</div>
<div class="field">
<label class="label">Download Queued</label>
<ng-container *ngIf="download.downloadQueued">
{{ download.downloadQueued | date: "fullDate" }}
{{ download.downloadQueued | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.downloadQueued">(not queued for downloading yet) </ng-container>
</div>
<div class="field">
<label class="label">Download Started</label>
<ng-container *ngIf="download.downloadStarted">
{{ download.downloadStarted | date: "fullDate" }}
{{ download.downloadStarted | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.downloadStarted">(not started downloading yet) </ng-container>
</div>
<div class="field">
<label class="label">Download Finished</label>
<ng-container *ngIf="download.downloadFinished">
{{ download.downloadFinished | date: "fullDate" }}
{{ download.downloadFinished | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.downloadFinished">(not finished yet) </ng-container>
</div>
<div class="field">
<label class="label">Unpacking Queued</label>
<ng-container *ngIf="download.unpackingQueued">
{{ download.unpackingQueued | date: "fullDate" }}
{{ download.unpackingQueued | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.unpackingQueued">(not queued for unpacking yet) </ng-container>
</div>
<div class="field">
<label class="label">Unpacking Started</label>
<ng-container *ngIf="download.unpackingStarted">
{{ download.unpackingStarted | date: "fullDate" }}
{{ download.unpackingStarted | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.unpackingStarted">(not started unpacking yet) </ng-container>
</div>
<div class="field">
<label class="label">Unpacking Finished</label>
<ng-container *ngIf="download.unpackingFinished">
{{ download.unpackingFinished | date: "fullDate" }}
{{ download.unpackingFinished | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.unpackingFinished">(not finished unpacking yet) </ng-container>
</div>
<div class="field">
<label class="label">Completed</label>
<ng-container *ngIf="download.completed">
{{ download.completed | date: "fullDate" }}
{{ download.completed | date: "mediumTime" }}
</ng-container>
<ng-container *ngIf="!download.completed">(not completed yet) </ng-container>
</div>
</div>
</div>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal" [class.is-active]="isDeleteModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Delete torrent</p>
<button class="delete" aria-label="close" (click)="deleteCancel()"></button>
</header>
<section class="modal-card-body">
<p>Are you sure you want to delete this torrent?</p>
<div class="field">
<label class="label"></label>
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteData" />
Delete Torrent from client
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
Delete Torrent from provider
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteLocalFiles" />
Delete local files
</label>
</div>
</div>
<div class="notification is-primary">
Deleting a torrent from Real-Debrid will automatically delete it here too.
</div>
<div class="notification is-danger is-light" *ngIf="deleteError?.length > 0">
Error deleting torrent: {{ deleteError }}
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="deleteOk()"
[disabled]="deleting"
[ngClass]="{ 'is-loading': deleting }"
>
Delete
</button>
<button class="button" (click)="deleteCancel()" [disabled]="deleting" [ngClass]="{ 'is-loading': deleting }">
Cancel
</button>
</footer>
</div>
</div>
<div class="modal" [class.is-active]="isRetryModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Retry torrent</p>
<button class="delete" aria-label="close" (click)="retryCancel()"></button>
</header>
<section class="modal-card-body">
<p>Are you sure you want to retry this torrent?</p>
<p>
This action will delete all the torrent data + all local downloads. Then it will re-add the original magnet link
or torrent file to Real-Debrid.
</p>
<div class="notification is-danger is-light" *ngIf="retryError?.length > 0">
Error retrying torrent: {{ retryError }}
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="retryOk()"
[disabled]="retrying"
[ngClass]="{ 'is-loading': retrying }"
>
Retry
</button>
<button class="button" (click)="retryCancel()" [disabled]="retrying" [ngClass]="{ 'is-loading': retrying }">
Cancel
</button>
</footer>
</div>
</div>
<div class="modal" [class.is-active]="isDownloadRetryModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Retry download</p>
<button class="delete" aria-label="close" (click)="downloadRetryCancel()"></button>
</header>
<section class="modal-card-body">
<p>Are you sure you want to retry this download?</p>
<p>This action will remove the local download and re-download the file from Real-Debrid.</p>
<div class="notification is-danger is-light" *ngIf="downloadRetryError?.length > 0">
Error retrying download: {{ downloadRetryError }}
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="downloadRetryOk()"
[disabled]="downloadRetrying"
[ngClass]="{ 'is-loading': downloadRetrying }"
>
Retry
</button>
<button
class="button"
(click)="downloadRetryCancel()"
[disabled]="downloadRetrying"
[ngClass]="{ 'is-loading': downloadRetrying }"
>
Cancel
</button>
</footer>
</div>
</div>
<div class="modal" [class.is-active]="isUpdateSettingsModalActive">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Update torrent settings</p>
<button class="delete" aria-label="close" (click)="updateSettingsCancel()"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">Category</label>
<div class="control">
<input class="input" type="text" maxlength="100" [(ngModel)]="updateSettingsCategory" />
</div>
<p class="help">The category becomes a sub-folder in your main download path.</p>
</div>
<div class="field">
<label class="label">Priority</label>
<div class="control">
<input class="input" type="number" step="1" [(ngModel)]="updateSettingsPriority" />
</div>
<p class="help">
Set the priority for this torrent where 1 is the highest. When empty it will be assigned the lowest priority.
</p>
</div>
<div class="field">
<label class="label">Automatic retry downloads</label>
<div class="control">
<input
class="input"
type="number"
max="1000"
min="0"
step="1"
[(ngModel)]="updateSettingsDownloadRetryAttempts"
/>
</div>
<p class="help">When a single download fails it will retry it this many times before marking it as failed.</p>
</div>
<div class="field">
<label class="label">Automatic retry torrent</label>
<div class="control">
<input
class="input"
type="number"
max="1000"
min="0"
step="1"
[(ngModel)]="updateSettingsTorrentRetryAttempts"
/>
</div>
<p class="help">
When a single download has failed multiple times (see setting above) or when the torrent itself received an
error it will retry the full torrent this many times before marking it failed.
</p>
</div>
<div class="field">
<label class="label">Delete download when in error</label>
<div class="control">
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="updateSettingsDeleteOnError" />
</div>
<p class="help">
When a download has been in error for this many minutes, delete it from the provider and the client. 0 to
disable.
</p>
</div>
<div class="field">
<label class="label">Torrent maximum lifetime</label>
<div class="control">
<input
class="input"
type="number"
max="100000"
min="0"
step="1"
[(ngModel)]="updateSettingsTorrentLifetime"
/>
</div>
<p class="help">
The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the
torrent is completed and has downloads, the lifetime setting will not apply. 0 to disable.
</p>
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="updateSettingsOk()"
[disabled]="updating"
[ngClass]="{ 'is-loading': updating }"
>
Save
</button>
<button
class="button"
(click)="updateSettingsCancel()"
[disabled]="updating"
[ngClass]="{ 'is-loading': updating }"
>
Cancel
</button>
</footer>
</div>
</div>

View file

@ -0,0 +1,13 @@
.label {
margin-bottom: 0;
}
table {
tr {
cursor: pointer;
}
}
.fa-download {
margin-left :12px;
}

View file

@ -0,0 +1,214 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { saveAs } from 'file-saver-es';
import { Torrent } from '../models/torrent.model';
import { TorrentService } from '../torrent.service';
@Component({
selector: 'app-torrent',
templateUrl: './torrent.component.html',
styleUrls: ['./torrent.component.scss'],
})
export class TorrentComponent implements OnInit {
public torrent: Torrent;
public activeTab: number = 0;
public copied: boolean = false;
public downloadExpanded: { [downloadId: string]: boolean } = {};
public isDeleteModalActive: boolean;
public deleteError: string;
public deleting: boolean;
public deleteData: boolean;
public deleteRdTorrent: boolean;
public deleteLocalFiles: boolean;
public isRetryModalActive: boolean;
public retryError: string;
public retrying: boolean;
public isDownloadRetryModalActive: boolean;
public downloadRetryError: string;
public downloadRetrying: boolean;
public downloadRetryId: string;
public isUpdateSettingsModalActive: boolean;
public updateSettingsCategory: string;
public updateSettingsPriority: number;
public updateSettingsDownloadRetryAttempts: number;
public updateSettingsTorrentRetryAttempts: number;
public updateSettingsDeleteOnError: number;
public updateSettingsTorrentLifetime: number;
public updating: boolean;
constructor(private activatedRoute: ActivatedRoute, private router: Router, private torrentService: TorrentService) {}
ngOnInit(): void {
this.activatedRoute.params.subscribe((params) => {
const torrentId = params['id'];
this.torrentService.get(torrentId).subscribe(
(torrent) => {
this.torrent = torrent;
this.torrentService.update$.subscribe((result) => {
this.update(result);
});
},
() => {
this.router.navigate(['/']);
}
);
});
}
public update(torrents: Torrent[]): void {
const updatedTorrent = torrents.firstOrDefault((m) => m.torrentId === this.torrent.torrentId);
if (updatedTorrent == null) {
return;
}
this.torrent = updatedTorrent;
}
public download(): void {
const byteArray = new Uint8Array(
window
.atob(this.torrent.fileOrMagnet)
.split('')
.map(function (c) {
return c.charCodeAt(0);
})
);
var blob = new Blob([byteArray], { type: 'application/x-bittorrent' });
saveAs(blob, `${this.torrent.rdName}.torrent`);
}
public showDeleteModal(): void {
this.deleteData = false;
this.deleteRdTorrent = false;
this.deleteLocalFiles = false;
this.deleteError = null;
this.isDeleteModalActive = true;
}
public deleteCancel(): void {
this.isDeleteModalActive = false;
}
public deleteOk(): void {
this.deleting = true;
this.torrentService
.delete(this.torrent.torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
.subscribe(
() => {
this.isDeleteModalActive = false;
this.deleting = false;
this.router.navigate(['/']);
},
(err) => {
this.deleteError = err.error;
this.deleting = false;
}
);
}
public showRetryModal(): void {
this.retryError = null;
this.isRetryModalActive = true;
}
public retryCancel(): void {
this.isRetryModalActive = false;
}
public retryOk(): void {
this.retrying = true;
this.torrentService.retry(this.torrent.torrentId).subscribe(
() => {
this.isRetryModalActive = false;
this.retrying = false;
this.router.navigate(['/']);
},
(err) => {
this.retryError = err.error;
this.retrying = false;
}
);
}
public showDownloadRetryModal(downloadId: string): void {
this.downloadRetryId = downloadId;
this.downloadRetryError = null;
this.isDownloadRetryModalActive = true;
}
public downloadRetryCancel(): void {
this.isDownloadRetryModalActive = false;
}
public downloadRetryOk(): void {
this.downloadRetrying = true;
this.torrentService.retryDownload(this.downloadRetryId).subscribe(
() => {
this.isDownloadRetryModalActive = false;
this.downloadRetrying = false;
},
(err) => {
this.downloadRetryError = err.error;
this.downloadRetrying = false;
}
);
}
public showUpdateSettingsModal(): void {
this.updateSettingsCategory = this.torrent.category;
this.updateSettingsPriority = this.torrent.priority;
this.updateSettingsDownloadRetryAttempts = this.torrent.downloadRetryAttempts;
this.updateSettingsTorrentRetryAttempts = this.torrent.torrentRetryAttempts;
this.updateSettingsDeleteOnError = this.torrent.deleteOnError;
this.updateSettingsTorrentLifetime = this.torrent.lifetime;
this.isUpdateSettingsModalActive = true;
}
public updateSettingsCancel(): void {
this.isUpdateSettingsModalActive = false;
}
public updateSettingsOk(): void {
this.updating = true;
this.torrent.category = this.updateSettingsCategory;
this.torrent.priority = this.updateSettingsPriority;
this.torrent.downloadRetryAttempts = this.updateSettingsDownloadRetryAttempts;
this.torrent.torrentRetryAttempts = this.updateSettingsTorrentRetryAttempts;
this.torrent.deleteOnError = this.updateSettingsDeleteOnError;
this.torrent.lifetime = this.updateSettingsTorrentLifetime;
this.torrentService.update(this.torrent).subscribe(
() => {
this.isUpdateSettingsModalActive = false;
this.updating = false;
},
() => {
this.isUpdateSettingsModalActive = false;
this.updating = false;
}
);
}
}

View file

@ -1,10 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class UsersService {
constructor() {}
public getLoggedInUser() {}
}

View file

@ -1,5 +1,5 @@
// This file can be replaced during build by using the `fileReplacements` array. // This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. // `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`. // The list of file replacements can be found in `angular.json`.
export const environment = { export const environment = {
@ -13,4 +13,4 @@ export const environment = {
* This import should be commented out in production mode because it will have a negative impact * This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown. * on performance if an error is thrown.
*/ */
// import 'zone.js/dist/zone-error'; // Included with Angular CLI. // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

View file

@ -4,6 +4,12 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Real-Debrid Torrent Client</title> <title>Real-Debrid Torrent Client</title>
<base href="/" /> <base href="/" />
<script>
(function () {
window["_app_base"] = "/" + window.location.pathname.split("/")[1];
console.log("setting base href to " + window["_app_base"]);
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
</head> </head>

View file

@ -8,8 +8,8 @@
* file. * file.
* *
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), * automatically update themselves. This includes recent versions of Safari, Chrome (including
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. * Opera), Edge on the desktop, and iOS and Chrome on mobile.
* *
* Learn more in https://angular.io/guide/browser-support * Learn more in https://angular.io/guide/browser-support
*/ */
@ -18,16 +18,6 @@
* BROWSER POLYFILLS * BROWSER POLYFILLS
*/ */
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** /**
* By default, zone.js will patch all possible macroTask and DomEvents * By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags * user can disable parts of macroTask/DomEvents patch by setting following flags
@ -55,7 +45,7 @@
/*************************************************************************************************** /***************************************************************************************************
* Zone JS is required by default for Angular itself. * Zone JS is required by default for Angular itself.
*/ */
import 'zone.js/dist/zone'; // Included with Angular CLI. import 'zone.js'; // Included with Angular CLI.
/*************************************************************************************************** /***************************************************************************************************
* APPLICATION IMPORTS * APPLICATION IMPORTS

View file

@ -1,23 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(
path: string,
deep?: boolean,
filter?: RegExp
): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View file

@ -1,14 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/app", "outDir": "./out-tsc/app",
"types": [] "types": []
}, },
"files": [ "files": ["src/main.ts", "src/polyfills.ts"],
"src/main.ts", "include": ["src/**/*.d.ts"]
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
} }

View file

@ -1,23 +1,34 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"baseUrl": "./", "baseUrl": "./",
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true, "sourceMap": true,
"declaration": false, "declaration": false,
"downlevelIteration": true, "downlevelIteration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"module": "es2020",
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"target": "es2015", "strictNullChecks": false,
"target": "ES2022",
"module": "es2020",
"lib": [ "lib": [
"es2018", "es2020",
"dom" "dom"
] ],
"useDefineForClassFields": false
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"fullTemplateTypeCheck": true, "enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true "strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
} }
} }

View file

@ -1,18 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View file

@ -1,103 +0,0 @@
{
"extends": "tslint:recommended",
"rules": {
"align": {
"options": ["parameters", "statements"]
},
"array-type": false,
"arrow-return-shorthand": true,
"curly": true,
"deprecation": {
"severity": "warning"
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"directive-selector": [true, "attribute", "app", "camelCase"],
"component-selector": [true, "element", "app", "kebab-case"],
"eofline": true,
"import-blacklist": [true, "rxjs/Rx"],
"import-spacing": true,
"indent": {
"options": ["spaces"]
},
"max-classes-per-file": false,
"max-line-length": [true, 140],
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
"no-empty": false,
"no-inferrable-types": [true, "ignore-params"],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [true, "as-needed"],
"quotemark": [true, "single"],
"semicolon": {
"options": ["always"]
},
"space-before-function-paren": {
"options": {
"anonymous": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never",
"named": "never"
}
},
"typedef-whitespace": {
"options": [
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
]
},
"variable-name": {
"options": ["ban-keywords", "check-format", "allow-pascal-case"]
},
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
},
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
},
"rulesDirectory": ["codelyzer"]
}

View file

@ -39,12 +39,14 @@ param(
$imageName = "$($DockerAccount)/rdtclient" $imageName = "$($DockerAccount)/rdtclient"
$dockerArgs = @( "buildx", "build", "--push", "--platform", $Platforms, "--tag", "$($imageName):latest", "." ) $dockerArgs = @( "buildx", "build", "--no-cache", "--push", "--platform", $Platforms, "--tag", "$($imageName):latest", "." )
if (![string]::IsNullOrEmpty($Version)) { if ([string]::IsNullOrEmpty($Version)) {
$dockerArgs += @("--tag", "$($imageName):$($Version)" ) $Version = (Get-Content "package.json" | ConvertFrom-Json).version
$dockerApps += @("--build-arg", "VERSION=$($Version)" )
} }
$dockerArgs += @("--tag", "$($imageName):$($Version)" )
$dockerApps += @("--build-arg", "VERSION=$($Version)" )
Write-Host "Generating docker image $imageName for $Platforms" Write-Host "Generating docker image $imageName for $Platforms"
& docker $dockerArgs & docker $dockerArgs

View file

@ -13,7 +13,7 @@ $newNavbar = (Get-Content $navbar) -replace 'Version .*?<', "Version $version<"
cd client cd client
npm install npm install
ng build --prod --output-path=..\server\RdtClient.Web\wwwroot ng build --configuration production --output-path=..\server\RdtClient.Web\wwwroot
cd .. cd ..
cd server cd server

3265
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
{ {
"name": "rdt-client", "name": "rdt-client",
"version": "1.7.4", "version": "2.0.22",
"description": "This is a web interface to manage your torrents on Real-Debrid.", "description": "This is a web interface to manage your torrents on Real-Debrid.",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"gh-release": "^5.0.0" "gh-release": "^7.0.2"
}, },
"devDependencies": {}, "devDependencies": {},
"repository": { "repository": {

39
server/.gitignore vendored Normal file
View file

@ -0,0 +1,39 @@
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc
nupkg/
# Visual Studio Code
.vscode
# Rider
.idea
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
msbuild.log
msbuild.err
msbuild.wrn
# Visual Studio 2015
.vs/
wwwroot/

View file

@ -1,148 +1,32 @@
using System.Collections.Generic; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
namespace RdtClient.Data.Data namespace RdtClient.Data.Data;
#nullable disable
public class DataContext : IdentityDbContext
{ {
public class DataContext : IdentityDbContext public DataContext(DbContextOptions options) : base(options)
{ {
public DataContext(DbContextOptions options) : base(options) }
{
}
public DbSet<Download> Downloads { get; set; } public DbSet<Download> Downloads { get; set; }
public DbSet<Setting> Settings { get; set; } public DbSet<Setting> Settings { get; set; }
public DbSet<Torrent> Torrents { get; set; } public DbSet<Torrent> Torrents { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) protected override void OnModelCreating(ModelBuilder builder)
{ {
base.OnModelCreating(builder);
} var cascadeFKs = builder.Model.GetEntityTypes()
.SelectMany(t => t.GetForeignKeys())
.Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade);
protected override void OnModelCreating(ModelBuilder builder) foreach (var fk in cascadeFKs)
{ {
base.OnModelCreating(builder); fk.DeleteBehavior = DeleteBehavior.Restrict;
var cascadeFKs = builder.Model.GetEntityTypes()
.SelectMany(t => t.GetForeignKeys())
.Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade);
foreach (var fk in cascadeFKs)
{
fk.DeleteBehavior = DeleteBehavior.Restrict;
}
}
public async Task Seed()
{
var seedSettings = new List<Setting>
{
new Setting
{
SettingId = "RealDebridApiKey",
Type = "String",
Value = ""
},
new Setting
{
SettingId = "DownloadPath",
Type = "String",
#if DEBUG
Value = @"C:\Temp\rdtclient"
#else
Value = "/data/downloads"
#endif
},
new Setting
{
SettingId = "DownloadClient",
Type = "String",
Value = @"Simple"
},
new Setting
{
SettingId = "TempPath",
Type = "String",
#if DEBUG
Value = @"C:\Temp\rdtclient"
#else
Value = "/data/downloads"
#endif
},
new Setting
{
SettingId = "MappedPath",
Type = "String",
#if DEBUG
Value = @"C:\Temp\rdtclient"
#else
Value = @"C:\Downloads"
#endif
},
new Setting
{
SettingId = "DownloadLimit",
Type = "Int32",
Value = "2"
},
new Setting
{
SettingId = "UnpackLimit",
Type = "Int32",
Value = "1"
},
new Setting
{
SettingId = "MinFileSize",
Type = "Int32",
Value = "0"
},
new Setting
{
SettingId = "OnlyDownloadAvailableFiles",
Type = "Int32",
Value = "1"
},
new Setting
{
SettingId = "DownloadChunkCount",
Type = "Int32",
Value = "8"
},
new Setting
{
SettingId = "DownloadMaxSpeed",
Type = "Int32",
Value = "0"
},
new Setting
{
SettingId = "ProxyServer",
Type = "String",
Value = ""
},
new Setting
{
SettingId = "LogLevel",
Type = "String",
Value = "Warning"
}
};
var dbSettings = await Settings.ToListAsync();
foreach (var seedSetting in seedSettings)
{
var dbSetting = dbSettings.FirstOrDefault(m => m.SettingId == seedSetting.SettingId);
if (dbSetting == null)
{
await Settings.AddAsync(seedSetting);
await SaveChangesAsync();
}
}
} }
} }
} }

View file

@ -1,190 +1,267 @@
using System; using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Download = RdtClient.Data.Models.Data.Download; using Download = RdtClient.Data.Models.Data.Download;
namespace RdtClient.Data.Data namespace RdtClient.Data.Data;
public class DownloadData
{ {
public interface IDownloadData private readonly DataContext _dataContext;
public DownloadData(DataContext dataContext)
{ {
Task<Download> GetById(Guid downloadId); _dataContext = dataContext;
Task<Download> Get(Guid torrentId, String path);
Task<Download> Add(Guid torrentId, String path);
Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink);
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateError(Guid downloadId, String error);
Task UpdateRetryCount(Guid downloadId, Int32 retryCount);
Task DeleteForTorrent(Guid torrentId);
} }
public class DownloadData : IDownloadData public async Task<List<Download>> GetForTorrent(Guid torrentId)
{ {
private readonly DataContext _dataContext; return await _dataContext.Downloads
.AsNoTracking()
.Where(m => m.TorrentId == torrentId)
.ToListAsync();
}
public DownloadData(DataContext dataContext) public async Task<Download?> GetById(Guid downloadId)
{
return await _dataContext.Downloads
.Include(m => m.Torrent)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
}
public async Task<Download?> Get(Guid torrentId, String path)
{
return await _dataContext.Downloads
.Include(m => m.Torrent)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
}
public async Task<Download> Add(Guid torrentId, String path)
{
var download = new Download
{ {
_dataContext = dataContext; DownloadId = Guid.NewGuid(),
TorrentId = torrentId,
Path = path,
Added = DateTimeOffset.UtcNow,
DownloadQueued = DateTimeOffset.UtcNow,
RetryCount = 0
};
await _dataContext.Downloads.AddAsync(download);
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
return download;
}
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{
return;
} }
public async Task<IList<Download>> Get() dbDownload.Link = unrestrictedLink;
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
}
public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{ {
return await _dataContext.Downloads return;
.AsNoTracking()
.Include(m => m.Torrent)
.ToListAsync();
} }
public async Task<IList<Download>> GetForTorrent(Guid torrentId) dbDownload.DownloadStarted = dateTime;
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
}
public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{ {
return await _dataContext.Downloads return;
.AsNoTracking()
.Where(m => m.TorrentId == torrentId)
.ToListAsync();
} }
public async Task<Download> GetById(Guid downloadId)
{
return await _dataContext.Downloads
.Include(m => m.Torrent)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
}
public async Task<Download> Get(Guid torrentId, String path)
{
return await _dataContext.Downloads.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
}
public async Task<Download> Add(Guid torrentId, String path)
{
var download = new Download
{
DownloadId = Guid.NewGuid(),
TorrentId = torrentId,
Path = path,
Added = DateTimeOffset.UtcNow,
DownloadQueued = DateTimeOffset.UtcNow,
RetryCount = 0
};
await _dataContext.Downloads.AddAsync(download);
await _dataContext.SaveChangesAsync();
return download;
}
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.Link = unrestrictedLink;
await _dataContext.SaveChangesAsync();
}
public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.DownloadStarted = dateTime;
await _dataContext.SaveChangesAsync();
}
public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{
return;
}
dbDownload.DownloadFinished = dateTime; dbDownload.DownloadFinished = dateTime;
await _dataContext.SaveChangesAsync(); await _dataContext.SaveChangesAsync();
}
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime) await TorrentData.VoidCache();
}
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{ {
var dbDownload = await _dataContext.Downloads return;
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.UnpackingQueued = dateTime;
await _dataContext.SaveChangesAsync();
} }
public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime) dbDownload.UnpackingQueued = dateTime;
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
}
public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{ {
var dbDownload = await _dataContext.Downloads return;
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.UnpackingStarted = dateTime;
await _dataContext.SaveChangesAsync();
} }
public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime) dbDownload.UnpackingStarted = dateTime;
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
}
public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{ {
var dbDownload = await _dataContext.Downloads return;
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.UnpackingFinished = dateTime;
await _dataContext.SaveChangesAsync();
} }
dbDownload.UnpackingFinished = dateTime;
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
}
public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime) public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{ {
var dbDownload = await _dataContext.Downloads return;
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.Completed = dateTime;
await _dataContext.SaveChangesAsync();
} }
public async Task UpdateError(Guid downloadId, String error) dbDownload.Completed = dateTime;
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
}
public async Task UpdateError(Guid downloadId, String? error)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{ {
var dbDownload = await _dataContext.Downloads return;
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.Error = error;
await _dataContext.SaveChangesAsync();
} }
public async Task UpdateRetryCount(Guid downloadId, Int32 retryCount) dbDownload.Error = error;
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
}
public async Task UpdateRetryCount(Guid downloadId, Int32 retryCount)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{ {
var dbDownload = await _dataContext.Downloads return;
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.RetryCount = retryCount;
await _dataContext.SaveChangesAsync();
} }
public async Task DeleteForTorrent(Guid torrentId) dbDownload.RetryCount = retryCount;
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
}
public async Task UpdateRemoteId(Guid downloadId, String remoteId)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{ {
var downloads = await _dataContext.Downloads return;
.Where(m => m.TorrentId == torrentId)
.ToListAsync();
_dataContext.Downloads.RemoveRange(downloads);
await _dataContext.SaveChangesAsync();
} }
dbDownload.RemoteId = remoteId;
await _dataContext.SaveChangesAsync();
}
public async Task DeleteForTorrent(Guid torrentId)
{
var downloads = await _dataContext.Downloads
.Where(m => m.TorrentId == torrentId)
.ToListAsync();
_dataContext.Downloads.RemoveRange(downloads);
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
}
public async Task Reset(Guid downloadId)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null)
{
throw new Exception($"Cannot find download with ID {downloadId}");
}
dbDownload.RetryCount = 0;
dbDownload.Link = null;
dbDownload.Added = DateTimeOffset.UtcNow;
dbDownload.DownloadQueued = DateTimeOffset.UtcNow;
dbDownload.DownloadStarted = null;
dbDownload.DownloadFinished = null;
dbDownload.UnpackingQueued = null;
dbDownload.UnpackingStarted = null;
dbDownload.UnpackingFinished = null;
dbDownload.Completed = null;
dbDownload.Error = null;
await _dataContext.SaveChangesAsync();
await TorrentData.VoidCache();
} }
} }

View file

@ -1,53 +1,212 @@
using System; using System.ComponentModel;
using System.Collections.Generic; using System.Reflection;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal;
using Serilog;
namespace RdtClient.Data.Data namespace RdtClient.Data.Data;
public class SettingData
{ {
public interface ISettingData private readonly DataContext _dataContext;
public static DbSettings Get { get; } = new DbSettings();
public static IList<SettingProperty> GetAll()
{ {
Task<IList<Setting>> GetAll(); return GetSettings(Get, null).ToList();
Task Update(IList<Setting> settings);
Task<Setting> Get(String key);
} }
public class SettingData : ISettingData public SettingData(DataContext dataContext)
{ {
private readonly DataContext _dataContext; _dataContext = dataContext;
}
public SettingData(DataContext dataContext) public async Task Update(IList<SettingProperty> settings)
{
var dbSettings = await _dataContext.Settings.ToListAsync();
foreach (var dbSetting in dbSettings)
{ {
_dataContext = dataContext; var setting = settings.FirstOrDefault(m => m.Key == dbSetting.SettingId);
}
public async Task<IList<Setting>> GetAll() if (setting != null)
{
return await _dataContext.Settings.AsNoTracking().ToListAsync();
}
public async Task Update(IList<Setting> settings)
{
var dbSettings = await _dataContext.Settings.ToListAsync();
foreach (var dbSetting in dbSettings)
{ {
var setting = settings.FirstOrDefault(m => m.SettingId == dbSetting.SettingId); dbSetting.Value = setting.Value?.ToString();
if (setting != null)
{
dbSetting.Value = setting.Value;
}
} }
}
await _dataContext.SaveChangesAsync();
await ResetCache();
}
public async Task Update(String settingId, Object? value)
{
var dbSetting = await _dataContext.Settings.FirstOrDefaultAsync(m => m.SettingId == settingId);
if (dbSetting == null)
{
return;
}
dbSetting.Value = value?.ToString();
await _dataContext.SaveChangesAsync();
await ResetCache();
}
public async Task ResetCache()
{
var settings = await _dataContext.Settings.AsNoTracking().ToListAsync();
if (settings.Count == 0)
{
throw new Exception("No settings found, please restart");
}
SetSettings(settings, Get, null);
}
public async Task Seed()
{
var dbSettings = await _dataContext.Settings.AsNoTracking().ToListAsync();
var expectedSettings = GetSettings(Get, null).Where(m => m.Type != "Object").Select(m => new Setting
{
SettingId = m.Key,
Value = m.Value?.ToString()
}).ToList();
var newSettings = expectedSettings.Where(m => dbSettings.All(p => p.SettingId != m.SettingId)).ToList();
if (newSettings.Any())
{
await _dataContext.Settings.AddRangeAsync(newSettings);
await _dataContext.SaveChangesAsync(); await _dataContext.SaveChangesAsync();
} }
public async Task<Setting> Get(String key) var oldSettings = dbSettings.Where(m => expectedSettings.All(p => p.SettingId != m.SettingId)).ToList();
if (oldSettings.Any())
{ {
return await _dataContext.Settings.AsNoTracking().FirstOrDefaultAsync(m => m.SettingId == key); _dataContext.Settings.RemoveRange(oldSettings);
await _dataContext.SaveChangesAsync();
} }
} }
}
private static IEnumerable<SettingProperty> GetSettings(Object defaultSetting, String? parent)
{
var result = new List<SettingProperty>();
var properties = defaultSetting.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in properties)
{
var displayName = Attribute.GetCustomAttribute(property, typeof(DisplayNameAttribute)) as DisplayNameAttribute;
var description = Attribute.GetCustomAttribute(property, typeof(DescriptionAttribute)) as DescriptionAttribute;
var propertyName = property.Name;
if (parent != null)
{
propertyName = $"{parent}:{propertyName}";
}
var settingProperty = new SettingProperty
{
Key = propertyName,
DisplayName = displayName?.DisplayName,
Description = description?.Description,
Type = property.PropertyType.Name
};
if (property.PropertyType.IsEnum ||
property.PropertyType.IsValueType ||
property.PropertyType == typeof(String))
{
settingProperty.Value = property.GetValue(defaultSetting);
if (property.PropertyType.IsEnum)
{
settingProperty.Type = "Enum";
settingProperty.EnumValues = new Dictionary<Int32, String>();
foreach (var e in Enum.GetValues(property.PropertyType).Cast<Enum>())
{
var enumMember = property.PropertyType.GetMember(e.ToString()).First();
var enumDescriptionAttribute = enumMember.GetCustomAttribute<DescriptionAttribute>();
var enumName = enumDescriptionAttribute?.Description ?? Enum.GetName(property.PropertyType, e) ?? "Unknown value";
settingProperty.EnumValues.Add((Int32)(Object)e, enumName);
}
}
result.Add(settingProperty);
}
else
{
settingProperty.Type = "Object";
result.Add(settingProperty);
var childResults = GetSettings(property.GetValue(defaultSetting)!, propertyName);
result.AddRange(childResults);
}
}
return result;
}
private static void SetSettings(IList<Setting> settings, Object defaultSetting, String? parent)
{
var properties = defaultSetting.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in properties)
{
var propertyName = property.Name;
if (parent != null)
{
propertyName = $"{parent}:{propertyName}";
}
if (property.PropertyType.IsEnum ||
property.PropertyType.IsValueType ||
property.PropertyType == typeof(String))
{
var setting = settings.FirstOrDefault(m => m.SettingId == propertyName);
if (setting != null)
{
if (property.PropertyType.IsEnum)
{
var newValue = Enum.Parse(property.PropertyType, setting.Value ?? "0");
property.SetValue(defaultSetting, newValue);
}
else
{
var converter = TypeDescriptor.GetConverter(property.PropertyType);
if (setting.Value == null)
{
property.SetValue(defaultSetting, null);
}
else if (converter.IsValid(setting.Value))
{
var newValue = converter.ConvertFrom(setting.Value);
property.SetValue(defaultSetting, newValue);
}
else
{
Log.Warning($"Invalid value for setting {propertyName}: {setting.Value}");
}
}
}
}
else
{
SetSettings(settings, property.GetValue(defaultSetting)!, propertyName);
}
}
}
}

View file

@ -1,168 +1,309 @@
using System; using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
namespace RdtClient.Data.Data namespace RdtClient.Data.Data;
public class TorrentData
{ {
public interface ITorrentData private static IList<Torrent>? _torrentCache;
private static readonly SemaphoreSlim TorrentCacheLock = new(1, 1);
private readonly DataContext _dataContext;
public TorrentData(DataContext dataContext)
{ {
Task<IList<Torrent>> Get(); _dataContext = dataContext;
Task<Torrent> GetById(Guid torrentId);
Task<Torrent> GetByHash(String hash);
Task<Torrent> Add(String realDebridId, String hash, String category, Boolean autoDelete, String fileOrMagnetContents, Boolean isFile);
Task UpdateRdData(Torrent torrent);
Task UpdateCategory(Guid torrentId, String category);
Task UpdateComplete(Guid torrentId, DateTimeOffset? datetime);
Task Delete(Guid torrentId);
} }
public class TorrentData : ITorrentData public async Task<IList<Torrent>> Get()
{ {
private readonly DataContext _dataContext; await TorrentCacheLock.WaitAsync();
public TorrentData(DataContext dataContext) try
{ {
_dataContext = dataContext; _torrentCache ??= await _dataContext.Torrents
.AsNoTracking()
.Include(m => m.Downloads)
.ToListAsync();
return _torrentCache.OrderBy(m => m.Priority ?? 9999).ThenBy(m => m.Added).ToList();
}
finally
{
TorrentCacheLock.Release();
}
}
public async Task<Torrent?> GetById(Guid torrentId)
{
var dbTorrent = await _dataContext.Torrents
.AsNoTracking()
.Include(m => m.Downloads)
.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{
return null;
} }
public async Task<IList<Torrent>> Get() foreach (var file in dbTorrent.Downloads)
{ {
var results = await _dataContext.Torrents file.Torrent = null;
.AsNoTracking()
.Include(m => m.Downloads)
.ToListAsync();
return results.OrderByDescending(m => m.Added).ToList();
} }
public async Task<Torrent> GetById(Guid torrentId) return dbTorrent;
}
public async Task<Torrent?> GetByHash(String hash)
{
var dbTorrent = await _dataContext.Torrents
.AsNoTracking()
.Include(m => m.Downloads)
.FirstOrDefaultAsync(m => m.Hash.ToLower() == hash.ToLower());
if (dbTorrent == null)
{ {
var dbTorrent = await _dataContext.Torrents return null;
.AsNoTracking()
.Include(m => m.Downloads)
.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{
return null;
}
foreach (var file in dbTorrent.Downloads)
{
file.Torrent = null;
}
return dbTorrent;
} }
public async Task<Torrent> GetByHash(String hash) foreach (var file in dbTorrent.Downloads)
{ {
var dbTorrent = await _dataContext.Torrents file.Torrent = null;
.AsNoTracking()
.Include(m => m.Downloads)
.FirstOrDefaultAsync(m => m.Hash.ToLower() == hash.ToLower());
if (dbTorrent == null)
{
return null;
}
foreach (var file in dbTorrent.Downloads)
{
file.Torrent = null;
}
return dbTorrent;
} }
public async Task<Torrent> Add(String realDebridId, String hash, String category, Boolean autoDelete, String fileOrMagnetContents, Boolean isFile) return dbTorrent;
}
public async Task<Torrent> Add(String realDebridId,
String hash,
String? fileOrMagnetContents,
Boolean isFile,
Torrent torrent)
{
var newTorrent = new Torrent
{ {
var torrent = new Torrent TorrentId = Guid.NewGuid(),
{ Added = DateTimeOffset.UtcNow,
TorrentId = Guid.NewGuid(), RdId = realDebridId,
Added = DateTimeOffset.UtcNow, Hash = hash.ToLower(),
RdId = realDebridId, Category = torrent.Category,
Hash = hash.ToLower(), HostDownloadAction = torrent.HostDownloadAction,
Category = category, DownloadAction = torrent.DownloadAction,
AutoDelete = autoDelete, FinishedAction = torrent.FinishedAction,
FileOrMagnet = fileOrMagnetContents, DownloadMinSize = torrent.DownloadMinSize,
IsFile = isFile DownloadManualFiles = torrent.DownloadManualFiles,
}; FileOrMagnet = fileOrMagnetContents,
IsFile = isFile,
Priority = torrent.Priority,
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
DeleteOnError = torrent.DeleteOnError,
Lifetime = torrent.Lifetime
};
await _dataContext.Torrents.AddAsync(torrent); await _dataContext.Torrents.AddAsync(newTorrent);
await _dataContext.SaveChangesAsync(); await _dataContext.SaveChangesAsync();
return torrent; await VoidCache();
return newTorrent;
}
public async Task UpdateRdData(Torrent torrent)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
if (dbTorrent == null)
{
return;
} }
public async Task UpdateRdData(Torrent torrent) dbTorrent.RdName = torrent.RdName;
{ dbTorrent.RdSize = torrent.RdSize;
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId); dbTorrent.RdHost = torrent.RdHost;
dbTorrent.RdSplit = torrent.RdSplit;
if (dbTorrent == null) dbTorrent.RdProgress = torrent.RdProgress;
{ dbTorrent.RdStatus = torrent.RdStatus;
return; dbTorrent.RdStatusRaw = torrent.RdStatusRaw;
} dbTorrent.RdAdded = torrent.RdAdded;
dbTorrent.RdEnded = torrent.RdEnded;
dbTorrent.RdName = torrent.RdName; dbTorrent.RdSpeed = torrent.RdSpeed;
dbTorrent.RdSize = torrent.RdSize; dbTorrent.RdSeeders = torrent.RdSeeders;
dbTorrent.RdHost = torrent.RdHost; dbTorrent.RdFiles = torrent.RdFiles;
dbTorrent.RdSplit = torrent.RdSplit;
dbTorrent.RdProgress = torrent.RdProgress;
dbTorrent.RdStatus = torrent.RdStatus;
dbTorrent.RdStatusRaw = torrent.RdStatusRaw;
dbTorrent.RdAdded = torrent.RdAdded;
dbTorrent.RdEnded = torrent.RdEnded;
dbTorrent.RdSpeed = torrent.RdSpeed;
dbTorrent.RdSeeders = torrent.RdSeeders;
if (torrent.Files != null)
{
dbTorrent.RdFiles = torrent.RdFiles;
}
await _dataContext.SaveChangesAsync();
}
public async Task UpdateCategory(Guid torrentId, String category) await _dataContext.SaveChangesAsync();
await VoidCache();
}
public async Task Update(Torrent torrent)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
if (dbTorrent == null)
{ {
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); return;
if (dbTorrent == null)
{
return;
}
dbTorrent.Category = category;
await _dataContext.SaveChangesAsync();
} }
public async Task UpdateComplete(Guid torrentId, DateTimeOffset? datetime) dbTorrent.Category = torrent.Category;
{ dbTorrent.Priority = torrent.Priority;
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); dbTorrent.DownloadRetryAttempts = torrent.DownloadRetryAttempts;
dbTorrent.TorrentRetryAttempts = torrent.TorrentRetryAttempts;
dbTorrent.Completed = datetime; dbTorrent.DeleteOnError = torrent.DeleteOnError;
dbTorrent.Lifetime = torrent.Lifetime;
await _dataContext.SaveChangesAsync(); await _dataContext.SaveChangesAsync();
await VoidCache();
}
public async Task UpdateCategory(Guid torrentId, String? category)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{
return;
} }
public async Task Delete(Guid torrentId) dbTorrent.Category = category;
await _dataContext.SaveChangesAsync();
await VoidCache();
}
public async Task UpdateComplete(Guid torrentId, String? error, DateTimeOffset? datetime, Boolean retry)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{ {
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); return;
}
if (dbTorrent == null) if (String.IsNullOrWhiteSpace(error))
{
var downloads = await _dataContext.Downloads.AsNoTracking().Where(m => m.TorrentId == torrentId).ToListAsync();
var downloadWithErrors = downloads.Where(m => !String.IsNullOrWhiteSpace(m.Error)).ToList();
if (downloadWithErrors.Any())
{ {
return; error = $"{downloadWithErrors.Count}/{downloads.Count} downloads failed with errors";
} }
}
_dataContext.Torrents.Remove(dbTorrent); if (!String.IsNullOrWhiteSpace(error) && retry)
{
if (dbTorrent.RetryCount < dbTorrent.TorrentRetryAttempts)
{
dbTorrent.RetryCount += 1;
dbTorrent.Retry = DateTime.UtcNow;
}
}
await _dataContext.SaveChangesAsync(); dbTorrent.Completed = datetime;
dbTorrent.Error = error;
await _dataContext.SaveChangesAsync();
await VoidCache();
}
public async Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{
return;
}
dbTorrent.FilesSelected = datetime;
await _dataContext.SaveChangesAsync();
await VoidCache();
}
public async Task UpdatePriority(Guid torrentId, Int32? priority)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{
return;
}
dbTorrent.Priority = priority;
await _dataContext.SaveChangesAsync();
await VoidCache();
}
public async Task UpdateRetry(Guid torrentId, DateTimeOffset? dateTime, Int32 retryCount)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{
return;
}
dbTorrent.RetryCount = retryCount;
dbTorrent.Retry = dateTime;
await _dataContext.SaveChangesAsync();
await VoidCache();
}
public async Task UpdateError(Guid torrentId, String error)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{
return;
}
dbTorrent.Error = error;
await _dataContext.SaveChangesAsync();
await VoidCache();
}
public async Task Delete(Guid torrentId)
{
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null)
{
return;
}
_dataContext.Torrents.Remove(dbTorrent);
await _dataContext.SaveChangesAsync();
await VoidCache();
}
public static async Task VoidCache()
{
await TorrentCacheLock.WaitAsync();
try
{
_torrentCache = null;
}
finally
{
TorrentCacheLock.Release();
} }
} }
} }

View file

@ -1,26 +1,19 @@
using System.Threading.Tasks; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace RdtClient.Data.Data namespace RdtClient.Data.Data;
public class UserData
{ {
public interface IUserData private readonly DataContext _dataContext;
public UserData(DataContext dataContext)
{ {
Task<IdentityUser> GetUser(); _dataContext = dataContext;
} }
public class UserData : IUserData public async Task<IdentityUser?> GetUser()
{ {
private readonly DataContext _dataContext; return await _dataContext.Users.FirstOrDefaultAsync();
public UserData(DataContext dataContext)
{
_dataContext = dataContext;
}
public async Task<IdentityUser> GetUser()
{
return await _dataContext.Users.FirstOrDefaultAsync();
}
} }
} }

View file

@ -1,16 +1,25 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RdtClient.Data.Data; using RdtClient.Data.Data;
using RdtClient.Data.Models.Internal;
namespace RdtClient.Data namespace RdtClient.Data;
public static class DiConfig
{ {
public static class DiConfig public static void Config(IServiceCollection services, AppSettings appSettings)
{ {
public static void Config(IServiceCollection services) if (String.IsNullOrWhiteSpace(appSettings.Database?.Path))
{ {
services.AddScoped<IDownloadData, DownloadData>(); throw new Exception("Invalid database path found in appSettings");
services.AddScoped<ISettingData, SettingData>();
services.AddScoped<ITorrentData, TorrentData>();
services.AddScoped<IUserData, UserData>();
} }
var connectionString = $"Data Source={appSettings.Database.Path}";
services.AddDbContext<DataContext>(options => options.UseSqlite(connectionString));
services.AddScoped<DownloadData>();
services.AddScoped<SettingData>();
services.AddScoped<TorrentData>();
services.AddScoped<UserData>();
} }
} }

View file

@ -0,0 +1,12 @@
using System.ComponentModel;
namespace RdtClient.Data.Enums;
public enum AuthenticationType
{
[Description("Username + Password")]
UserNamePassword,
[Description("No Authentication")]
None
}

View file

@ -0,0 +1,12 @@
using System.ComponentModel;
namespace RdtClient.Data.Enums;
public enum DownloadClient
{
[Description("Internal Downloader")]
Internal,
[Description("Aria2c")]
Aria2c
}

View file

@ -0,0 +1,18 @@
using System.ComponentModel;
namespace RdtClient.Data.Enums;
public enum LogLevel
{
[Description("Debug")]
Debug,
[Description("Information")]
Information,
[Description("Warning")]
Warning,
[Description("Error")]
Error
}

View file

@ -0,0 +1,15 @@
using System.ComponentModel;
namespace RdtClient.Data.Enums;
public enum Provider
{
[Description("RealDebrid")]
RealDebrid,
[Description("AllDebrid")]
AllDebrid,
[Description("Premiumize")]
Premiumize
}

View file

@ -0,0 +1,15 @@
using System.ComponentModel;
namespace RdtClient.Data.Enums;
public enum TorrentDownloadAction
{
[Description("Download All Files")]
DownloadAll = 0,
[Description("Download All Available Files")]
DownloadAvailableFiles = 1,
[Description("Manually Select Files")]
DownloadManual = 2
}

View file

@ -0,0 +1,18 @@
using System.ComponentModel;
namespace RdtClient.Data.Enums;
public enum TorrentFinishedAction
{
[Description("No Action")]
None = 0,
[Description("Remove Torrent From Client And Provider")]
RemoveAllTorrents = 1,
[Description("Remove Torrent From Provider")]
RemoveRealDebrid = 2,
[Description("Remove Torrent From Client")]
RemoveClient = 3
}

View file

@ -0,0 +1,12 @@
using System.ComponentModel;
namespace RdtClient.Data.Enums;
public enum TorrentHostDownloadAction
{
[Description("Download all files to host")]
DownloadAll = 0,
[Description("Don't download any files to host")]
DownloadNone = 1,
}

View file

@ -1,12 +1,12 @@
namespace RdtClient.Data.Enums namespace RdtClient.Data.Enums;
{
public enum RealDebridStatus
{
Processing = 0,
WaitingForFileSelection = 1,
Downloading = 2,
Finished = 3,
Error = 99 public enum TorrentStatus
} {
} Processing = 0,
WaitingForFileSelection = 1,
Downloading = 2,
Finished = 3,
Uploading = 4,
Error = 99
}

View file

@ -0,0 +1,430 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RdtClient.Data.Data;
namespace RdtClient.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20210718222602_Torrents_Add_Actions")]
partial class Torrents_Add_Actions
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.8");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.Property<Guid>("DownloadId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<Guid>("TorrentId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingStarted")
.HasColumnType("TEXT");
b.HasKey("DownloadId");
b.HasIndex("TorrentId");
b.ToTable("Downloads");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b =>
{
b.Property<string>("SettingId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("SettingId");
b.ToTable("Settings");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Property<Guid>("TorrentId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<int>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.HasColumnType("TEXT");
b.Property<bool>("IsFile")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RdAdded")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("RdEnded")
.HasColumnType("TEXT");
b.Property<string>("RdFiles")
.HasColumnType("TEXT");
b.Property<string>("RdHost")
.HasColumnType("TEXT");
b.Property<string>("RdId")
.HasColumnType("TEXT");
b.Property<string>("RdName")
.HasColumnType("TEXT");
b.Property<long>("RdProgress")
.HasColumnType("INTEGER");
b.Property<long?>("RdSeeders")
.HasColumnType("INTEGER");
b.Property<long>("RdSize")
.HasColumnType("INTEGER");
b.Property<long?>("RdSpeed")
.HasColumnType("INTEGER");
b.Property<long>("RdSplit")
.HasColumnType("INTEGER");
b.Property<int>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.HasKey("TorrentId");
b.ToTable("Torrents");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
.WithMany("Downloads")
.HasForeignKey("TorrentId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Torrent");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Navigation("Downloads");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,67 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace RdtClient.Data.Migrations
{
public partial class Torrents_Add_Actions : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AutoDelete",
table: "Torrents");
migrationBuilder.AddColumn<int>(
name: "FinishedAction",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "DownloadAction",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "DownloadManualFiles",
table: "Torrents",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "DownloadMinSize",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DownloadAction",
table: "Torrents");
migrationBuilder.DropColumn(
name: "DownloadManualFiles",
table: "Torrents");
migrationBuilder.DropColumn(
name: "DownloadMinSize",
table: "Torrents");
migrationBuilder.DropColumn(
name: "FinishedAction",
table: "Torrents");
migrationBuilder.AddColumn<int>(
name: "AutoDelete",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
}
}

View file

@ -0,0 +1,433 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RdtClient.Data.Data;
namespace RdtClient.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20210718231758_Torrents_Add_FilesSelected")]
partial class Torrents_Add_FilesSelected
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.8");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.Property<Guid>("DownloadId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<Guid>("TorrentId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingStarted")
.HasColumnType("TEXT");
b.HasKey("DownloadId");
b.HasIndex("TorrentId");
b.ToTable("Downloads");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b =>
{
b.Property<string>("SettingId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("SettingId");
b.ToTable("Settings");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Property<Guid>("TorrentId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<int>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("FilesSelected")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.HasColumnType("TEXT");
b.Property<bool>("IsFile")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RdAdded")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("RdEnded")
.HasColumnType("TEXT");
b.Property<string>("RdFiles")
.HasColumnType("TEXT");
b.Property<string>("RdHost")
.HasColumnType("TEXT");
b.Property<string>("RdId")
.HasColumnType("TEXT");
b.Property<string>("RdName")
.HasColumnType("TEXT");
b.Property<long>("RdProgress")
.HasColumnType("INTEGER");
b.Property<long?>("RdSeeders")
.HasColumnType("INTEGER");
b.Property<long>("RdSize")
.HasColumnType("INTEGER");
b.Property<long?>("RdSpeed")
.HasColumnType("INTEGER");
b.Property<long>("RdSplit")
.HasColumnType("INTEGER");
b.Property<int>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.HasKey("TorrentId");
b.ToTable("Torrents");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
.WithMany("Downloads")
.HasForeignKey("TorrentId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Torrent");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Navigation("Downloads");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,24 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace RdtClient.Data.Migrations
{
public partial class Torrents_Add_FilesSelected : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "FilesSelected",
table: "Torrents",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FilesSelected",
table: "Torrents");
}
}
}

View file

@ -0,0 +1,436 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RdtClient.Data.Data;
namespace RdtClient.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20211009221133_Downloads_Add_RemoteId")]
partial class Downloads_Add_RemoteId
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.10");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.Property<Guid>("DownloadId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("RemoteId")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<Guid>("TorrentId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingStarted")
.HasColumnType("TEXT");
b.HasKey("DownloadId");
b.HasIndex("TorrentId");
b.ToTable("Downloads");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b =>
{
b.Property<string>("SettingId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("SettingId");
b.ToTable("Settings");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Property<Guid>("TorrentId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<int>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("FilesSelected")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.HasColumnType("TEXT");
b.Property<bool>("IsFile")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RdAdded")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("RdEnded")
.HasColumnType("TEXT");
b.Property<string>("RdFiles")
.HasColumnType("TEXT");
b.Property<string>("RdHost")
.HasColumnType("TEXT");
b.Property<string>("RdId")
.HasColumnType("TEXT");
b.Property<string>("RdName")
.HasColumnType("TEXT");
b.Property<long>("RdProgress")
.HasColumnType("INTEGER");
b.Property<long?>("RdSeeders")
.HasColumnType("INTEGER");
b.Property<long>("RdSize")
.HasColumnType("INTEGER");
b.Property<long?>("RdSpeed")
.HasColumnType("INTEGER");
b.Property<long>("RdSplit")
.HasColumnType("INTEGER");
b.Property<int>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.HasKey("TorrentId");
b.ToTable("Torrents");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
.WithMany("Downloads")
.HasForeignKey("TorrentId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Torrent");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Navigation("Downloads");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace RdtClient.Data.Migrations
{
public partial class Downloads_Add_RemoteId : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "RemoteId",
table: "Downloads",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RemoteId",
table: "Downloads");
}
}
}

View file

@ -0,0 +1,439 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RdtClient.Data.Data;
namespace RdtClient.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20211011193848_Torrent_Add_RetryCount")]
partial class Torrent_Add_RetryCount
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.10");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.Property<Guid>("DownloadId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("RemoteId")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<Guid>("TorrentId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingStarted")
.HasColumnType("TEXT");
b.HasKey("DownloadId");
b.HasIndex("TorrentId");
b.ToTable("Downloads");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b =>
{
b.Property<string>("SettingId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("SettingId");
b.ToTable("Settings");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Property<Guid>("TorrentId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<int>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("FilesSelected")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.HasColumnType("TEXT");
b.Property<bool>("IsFile")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RdAdded")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("RdEnded")
.HasColumnType("TEXT");
b.Property<string>("RdFiles")
.HasColumnType("TEXT");
b.Property<string>("RdHost")
.HasColumnType("TEXT");
b.Property<string>("RdId")
.HasColumnType("TEXT");
b.Property<string>("RdName")
.HasColumnType("TEXT");
b.Property<long>("RdProgress")
.HasColumnType("INTEGER");
b.Property<long?>("RdSeeders")
.HasColumnType("INTEGER");
b.Property<long>("RdSize")
.HasColumnType("INTEGER");
b.Property<long?>("RdSpeed")
.HasColumnType("INTEGER");
b.Property<long>("RdSplit")
.HasColumnType("INTEGER");
b.Property<int>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.HasKey("TorrentId");
b.ToTable("Torrents");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
.WithMany("Downloads")
.HasForeignKey("TorrentId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Torrent");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Navigation("Downloads");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace RdtClient.Data.Migrations
{
public partial class Torrent_Add_RetryCount : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "RetryCount",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RetryCount",
table: "Torrents");
}
}
}

Some files were not shown because too many files have changed in this diff Show more