commit
6d65de6f5f
195 changed files with 38825 additions and 16797 deletions
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
39
.gitignore
vendored
|
|
@ -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/
|
||||
|
||||
### DotnetCore ###
|
||||
# .NET Core build folders
|
||||
server/**/bin
|
||||
server/**/obj
|
||||
server/**/*.user
|
||||
server/**/.vs
|
||||
server/RdtClient.Web/wwwroot/
|
||||
|
||||
build/
|
||||
publish/
|
||||
server/Downloader/
|
||||
|
||||
node_modules/
|
||||
RealDebridClient.zip
|
||||
server/RdtClient.Web/appsettings.Development.json
|
||||
|
|
|
|||
254
CHANGELOG.md
254
CHANGELOG.md
|
|
@ -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/),
|
||||
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
|
||||
### Changed
|
||||
- Changed how the docker is built.
|
||||
|
|
|
|||
19
Dockerfile
19
Dockerfile
|
|
@ -1,5 +1,5 @@
|
|||
# Stage 1 - Build the frontend
|
||||
FROM node:15.5-buster AS node-build-env
|
||||
FROM node:16-buster AS node-build-env
|
||||
ARG TARGETPLATFORM
|
||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||
ARG BUILDPLATFORM
|
||||
|
|
@ -14,12 +14,12 @@ RUN \
|
|||
cd client && \
|
||||
echo "**** Building Code ****" && \
|
||||
npm ci && \
|
||||
npx ng build --prod --output-path=out
|
||||
npx ng build --output-path=out
|
||||
|
||||
RUN ls -FCla /appclient/root
|
||||
|
||||
# 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
|
||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||
ARG BUILDPLATFORM
|
||||
|
|
@ -33,11 +33,14 @@ RUN \
|
|||
git clone https://github.com/rogerfar/rdt-client.git . && \
|
||||
echo "**** Building Source Code for $TARGETPLATFORM on $BUILDPLATFORM ****" && \
|
||||
cd server && \
|
||||
if [ "$TARGETPLATFORM" = "linux/arm/v7" -o "$TARGETPLATFORM" = "linux/arm64" ] ; then \
|
||||
echo "**** Building $TARGETPLATFORM arm version" && \
|
||||
if [ "$TARGETPLATFORM" = "linux/arm/v7" ] ; then \
|
||||
echo "**** Building $TARGETPLATFORM arm v7 version" && \
|
||||
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 \
|
||||
echo "**** Building $TARGETPLATFORM x86 version" && \
|
||||
echo "**** Building $TARGETPLATFORM x64 version" && \
|
||||
dotnet restore RdtClient.sln && dotnet publish -c Release -o out ; \
|
||||
fi
|
||||
|
||||
|
|
@ -69,8 +72,8 @@ RUN \
|
|||
echo "**** Installing dotnet ****" && \
|
||||
wget -q https://dot.net/v1/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 Current --runtime aspnetcore --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 LTS -v latest --runtime aspnetcore --install-dir /usr/share/dotnet && \
|
||||
echo "**** Cleaning image ****" && \
|
||||
apt-get -y -qq -o Dpkg::Use-Pty=0 clean && apt-get -y -qq -o Dpkg::Use-Pty=0 purge && \
|
||||
echo "**** Setting permissions ****" && \
|
||||
|
|
|
|||
97
README.md
97
README.md
|
|
@ -1,16 +1,22 @@
|
|||
# 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
|
||||
- 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
|
||||
- Implements a fake qBittorrent API so you can hook up other applications like Sonarr or Couchpotato.
|
||||
- Built with Angular 11 and .NET 5
|
||||
- Implements a fake qBittorrent API so you can hook up other applications like Sonarr, Radarr or Couchpotato.
|
||||
- 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
|
||||
|
||||
|
|
@ -62,15 +68,65 @@ docker-compose up -d
|
|||
|
||||
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
|
||||
|
||||
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. 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. 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
|
||||
|
||||
### 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:
|
||||
|
||||
#### 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:
|
||||
|
||||
- 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:
|
||||
|
||||
- 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.
|
||||
- Maximum parallel downloads: This number indicates how many completed torrents from Real Debrid can be downloaded at the same time.
|
||||
- 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.
|
||||
- Url: The full URL to your Aria2c service. This must end in /jsonrpc. A standard path is `http://192.168.10.2:6800/jsonrpc`.
|
||||
- Secret: Optional secret to connecto to your Aria2c service.
|
||||
|
||||
If Aria2c is selected, none of the above options for `Internal Downloader` are used, you have to configure Aria2c manually.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
|
|
@ -136,8 +195,8 @@ Notice: the progress and ETA reported in Sonarr's Activity tab will not be accur
|
|||
- NodeJS
|
||||
- NPM
|
||||
- Angular CLI
|
||||
- .NET 5
|
||||
- Visual Studio 2019
|
||||
- .NET 6
|
||||
- Visual Studio 2022
|
||||
- (optional) Resharper
|
||||
|
||||
1. Open the client folder project in VS Code and run `npm install`.
|
||||
|
|
|
|||
|
|
@ -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
37
client/.eslintrc.json
Normal 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
46
client/.gitignore
vendored
|
|
@ -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
2
client/.prettierignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.angular
|
||||
.vscode
|
||||
|
|
@ -3,11 +3,33 @@
|
|||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"rdt-client": {
|
||||
"client": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@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": "",
|
||||
|
|
@ -22,96 +44,76 @@
|
|||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
]
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "rdt-client:build",
|
||||
"proxyConfig": "angular-proxy.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "rdt-client:build:production"
|
||||
"browserTarget": "client:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "client:build:development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "rdt-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": []
|
||||
"browserTarget": "client:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"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"
|
||||
}
|
||||
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "rdt-client"
|
||||
"cli": {
|
||||
"analytics": false,
|
||||
"schematicCollections": ["@angular-eslint/schematics"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
28364
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,53 +1,54 @@
|
|||
{
|
||||
"name": "rdt-client",
|
||||
"name": "client",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"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",
|
||||
"e2e": "ng e2e",
|
||||
"update": "ng update --force --allow-dirty @angular/cli @angular/core"
|
||||
"prettier": "prettier --write \"./**/*.{ts,html,json}\""
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~11.2.0",
|
||||
"@angular/common": "~11.2.0",
|
||||
"@angular/compiler": "~11.2.0",
|
||||
"@angular/core": "~11.2.0",
|
||||
"@angular/forms": "~11.2.0",
|
||||
"@angular/platform-browser": "~11.2.0",
|
||||
"@angular/platform-browser-dynamic": "~11.2.0",
|
||||
"@angular/router": "~11.2.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.2",
|
||||
"@microsoft/signalr": "^5.0.3",
|
||||
"bulma": "^0.9.2",
|
||||
"curray": "^1.0.8",
|
||||
"ngx-filesize": "^2.0.16",
|
||||
"rxjs": "~6.6.3",
|
||||
"tslib": "^2.1.0",
|
||||
"zone.js": "~0.11.3"
|
||||
"@angular/animations": "^15.2.2",
|
||||
"@angular/cdk": "^15.2.2",
|
||||
"@angular/common": "^15.2.2",
|
||||
"@angular/compiler": "^15.2.2",
|
||||
"@angular/core": "^15.2.2",
|
||||
"@angular/flex-layout": "^15.0.0-beta.42",
|
||||
"@angular/forms": "^15.2.2",
|
||||
"@angular/platform-browser": "^15.2.2",
|
||||
"@angular/platform-browser-dynamic": "^15.2.2",
|
||||
"@angular/router": "^15.2.2",
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"@microsoft/signalr": "^7.0.3",
|
||||
"bulma": "^0.9.4",
|
||||
"curray": "^1.0.11",
|
||||
"file-saver-es": "^2.0.5",
|
||||
"ngx-filesize": "^3.0.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.5.0",
|
||||
"zone.js": "~0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1102.0",
|
||||
"@angular/cli": "~11.2.0",
|
||||
"@angular/compiler-cli": "~11.2.0",
|
||||
"@angular/language-service": "~11.2.0",
|
||||
"@types/node": "^14.14.27",
|
||||
"@types/jasmine": "~3.6.3",
|
||||
"@types/jasminewd2": "~2.0.8",
|
||||
"codelyzer": "^6.0.1",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~6.0.0",
|
||||
"karma": "~6.1.1",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
||||
"karma-jasmine": "~4.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.5.4",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~9.1.1",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.1.5"
|
||||
"@angular-devkit/build-angular": "^15.2.2",
|
||||
"@angular-eslint/builder": "15.2.1",
|
||||
"@angular-eslint/eslint-plugin": "15.2.1",
|
||||
"@angular-eslint/eslint-plugin-template": "15.2.1",
|
||||
"@angular-eslint/schematics": "15.2.1",
|
||||
"@angular-eslint/template-parser": "15.2.1",
|
||||
"@angular/cli": "^15.2.2",
|
||||
"@angular/compiler-cli": "^15.2.2",
|
||||
"@angular/language-service": "^15.2.2",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/file-saver-es": "^2.0.1",
|
||||
"@types/node": "^18.14.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||
"@typescript-eslint/parser": "5.54.1",
|
||||
"eslint": "^8.35.0",
|
||||
"prettier": "^2.8.4",
|
||||
"typescript": "~4.9.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
215
client/src/app/add-new-torrent/add-new-torrent.component.html
Normal file
215
client/src/app/add-new-torrent/add-new-torrent.component.html
Normal 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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
212
client/src/app/add-new-torrent/add-new-torrent.component.ts
Normal file
212
client/src/app/add-new-torrent/add-new-torrent.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
|
||||
import { AuthResolverService } from './auth-resolver.service';
|
||||
import { LoginComponent } from './login/login.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 { TorrentTableComponent } from './torrent-table/torrent-table.component';
|
||||
import { TorrentComponent } from './torrent/torrent.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
|
@ -20,6 +25,28 @@ const routes: Routes = [
|
|||
resolve: {
|
||||
isLoggedIn: AuthResolverService,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: TorrentTableComponent,
|
||||
},
|
||||
{
|
||||
path: 'torrent/:id',
|
||||
component: TorrentComponent,
|
||||
},
|
||||
{
|
||||
path: 'add',
|
||||
component: AddNewTorrentComponent,
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
component: SettingsComponent,
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
component: ProfileComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { curray } from 'curray';
|
||||
import { FileSizePipe, NgxFilesizeModule } from 'ngx-filesize';
|
||||
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AuthInterceptor } from './auth.interceptor';
|
||||
import { DecodeURIPipe } from './decode-uri.pipe';
|
||||
import { DownloadStatusPipe } from './download-status.pipe';
|
||||
import { LoginComponent } from './login/login.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 { 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 { 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 { TorrentTableComponent } from './torrent-table/torrent-table.component';
|
||||
import { TorrentComponent } from './torrent/torrent.component';
|
||||
|
||||
curray();
|
||||
|
||||
|
|
@ -29,17 +33,30 @@ curray();
|
|||
NavbarComponent,
|
||||
AddNewTorrentComponent,
|
||||
TorrentTableComponent,
|
||||
TorrentRowComponent,
|
||||
TorrentFileComponent,
|
||||
SettingsComponent,
|
||||
TorrentStatusPipe,
|
||||
DownloadStatusPipe,
|
||||
LoginComponent,
|
||||
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],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import { Observable } from 'rxjs/internal/Observable';
|
|||
export class AuthService {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
public isLoggedIn(): Observable<void> {
|
||||
return this.http.get<void>(`/Api/Authentication/IsLoggedIn`);
|
||||
public isLoggedIn(): Observable<boolean> {
|
||||
return this.http.get<boolean>(`/Api/Authentication/IsLoggedIn`);
|
||||
}
|
||||
|
||||
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> {
|
||||
return this.http.post<void>(`/Api/Authentication/Login`, {
|
||||
userName,
|
||||
|
|
@ -29,4 +36,11 @@ export class AuthService {
|
|||
public 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
client/src/app/decode-uri.pipe.ts
Normal file
10
client/src/app/decode-uri.pipe.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ export class DownloadStatusPipe implements PipeTransform {
|
|||
}
|
||||
|
||||
if (value.error) {
|
||||
return `Error: ${value.error}`;
|
||||
return value.error;
|
||||
}
|
||||
|
||||
if (value.completed != null) {
|
||||
|
|
@ -26,8 +26,13 @@ export class DownloadStatusPipe implements PipeTransform {
|
|||
}
|
||||
|
||||
if (value.unpackingStarted) {
|
||||
const progress = ((value.bytesDone / value.bytesTotal) * 100).toFixed(2);
|
||||
return `Unpacking ${progress || 0}%`;
|
||||
let progress = (value.bytesDone / value.bytesTotal) * 100;
|
||||
|
||||
if (isNaN(progress)) {
|
||||
progress = 0;
|
||||
}
|
||||
|
||||
return `Unpacking ${progress.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
if (value.unpackingQueued) {
|
||||
|
|
@ -39,10 +44,15 @@ export class DownloadStatusPipe implements PipeTransform {
|
|||
}
|
||||
|
||||
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');
|
||||
|
||||
return `Downloading ${progress || 0}% (${speed}/s)`;
|
||||
return `Downloading ${progress.toFixed(2)}% (${speed}/s)`;
|
||||
}
|
||||
|
||||
if (value.downloadQueued) {
|
||||
|
|
|
|||
|
|
@ -5,37 +5,46 @@
|
|||
<div class="column is-5-tablet is-5-desktop is-5-widescreen">
|
||||
<img src="../../assets/logo.png" />
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label for="" class="label">Username</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required />
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-envelope"></i>
|
||||
</span>
|
||||
<form (ngSubmit)="login()">
|
||||
<div class="field">
|
||||
<label for="" class="label">Username</label>
|
||||
<div class="control has-icons-left">
|
||||
<input
|
||||
type="text"
|
||||
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 class="field">
|
||||
<label for="" class="label" name="password">Password</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="password" class="input" required name="password" [(ngModel)]="password" />
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-lock"></i>
|
||||
</span>
|
||||
<div class="field">
|
||||
<label for="" class="label" name="password">Password</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="password" class="input" required name="password" (change)="setPassword($event)" />
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-lock"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button
|
||||
class="button is-success"
|
||||
(click)="login()"
|
||||
[ngClass]="{ 'is-loading': loggingIn }"
|
||||
[disabled]="loggingIn"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<button
|
||||
class="button is-success"
|
||||
type="submit"
|
||||
[ngClass]="{ 'is-loading': loggingIn }"
|
||||
[disabled]="loggingIn"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
||||
{{ error }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss'],
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
export class LoginComponent {
|
||||
public userName: string;
|
||||
public password: string;
|
||||
public error: string;
|
||||
|
|
@ -15,7 +15,13 @@ export class LoginComponent implements OnInit {
|
|||
|
||||
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 {
|
||||
this.error = null;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<app-navbar></app-navbar>
|
||||
<section class="section main-list">
|
||||
<div class="container">
|
||||
<app-torrent-table></app-torrent-table>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-layout',
|
||||
templateUrl: './main-layout.component.html',
|
||||
styleUrls: ['./main-layout.component.scss'],
|
||||
})
|
||||
export class MainLayoutComponent implements OnInit {
|
||||
export class MainLayoutComponent {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ export class Download {
|
|||
public bytesTotal: number;
|
||||
public bytesDone: number;
|
||||
public speed: number;
|
||||
public retryCount: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
export class Profile {
|
||||
public provider: string;
|
||||
public userName: string;
|
||||
public expiration: Date;
|
||||
public currentVersion: string;
|
||||
public latestVersion: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
export class Setting {
|
||||
public settingId: string;
|
||||
public value: string;
|
||||
key: string;
|
||||
value: boolean | number | null | string;
|
||||
displayName: null | string;
|
||||
description: null | string;
|
||||
type: string;
|
||||
settings: Setting[];
|
||||
enumValues: { [key: string]: string };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,27 @@ export class Torrent {
|
|||
public torrentId: string;
|
||||
public hash: string;
|
||||
public category: string;
|
||||
public hostDownloadAction: number;
|
||||
public downloadAction: number;
|
||||
public finishedAction: number;
|
||||
public downloadMinSize: number;
|
||||
public downloadManualFiles: string;
|
||||
|
||||
public added: Date;
|
||||
public filesSelected: 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 rdName: string;
|
||||
|
|
@ -35,11 +53,17 @@ export class TorrentFile {
|
|||
public download: Download;
|
||||
}
|
||||
|
||||
export class TorrentFileAvailability {
|
||||
public filename: string;
|
||||
public filesize: number;
|
||||
}
|
||||
|
||||
export enum RealDebridStatus {
|
||||
Processing = 0,
|
||||
WaitingForFileSelection = 1,
|
||||
Downloading = 2,
|
||||
Finished = 3,
|
||||
Uploading = 4,
|
||||
|
||||
Error = 99,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
.file-name {
|
||||
max-width: 24em;
|
||||
width: 24em;
|
||||
}
|
||||
|
||||
.file-modal {
|
||||
width: calc(75% - 40px);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<nav class="navbar is-dark is-fixed-top" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item">
|
||||
<a class="navbar-item" routerLink="/">
|
||||
<img src="../../assets/logo.png" />
|
||||
</a>
|
||||
|
||||
|
|
@ -20,19 +20,25 @@
|
|||
|
||||
<div class="navbar-menu" [class.is-active]="showMobileMenu">
|
||||
<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">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>Add New Torrent</span>
|
||||
</a>
|
||||
<a class="navbar-item" (click)="this.showSettings = true">
|
||||
<a class="navbar-item" routerLink="/settings">
|
||||
<span class="icon">
|
||||
<i class="fas fa-cog" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>Settings</span>
|
||||
</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">
|
||||
<i class="fas fa-euro-sign" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
|
@ -46,15 +52,19 @@
|
|||
<span class="navbar-link"> Account </span>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</nav>
|
||||
|
||||
<app-add-new-torrent [(open)]="showNewTorrent"></app-add-new-torrent>
|
||||
<app-settings [(open)]="showSettings"></app-settings>
|
||||
<div
|
||||
class="notification is-warning"
|
||||
*ngIf="profile && profile.latestVersion && profile.currentVersion !== profile.latestVersion"
|
||||
>
|
||||
Version {{ profile.latestVersion }} of RealDebrid Client was found. You are currently on version
|
||||
{{ profile.currentVersion }}.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
.no-premium {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.notification {
|
||||
margin-top: 52px;
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
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 { AuthService } from '../auth.service';
|
||||
import { Profile } from '../models/profile.model';
|
||||
import { SettingsService } from '../settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
|
|
@ -13,16 +12,26 @@ import { Router } from '@angular/router';
|
|||
export class NavbarComponent implements OnInit {
|
||||
public showMobileMenu = false;
|
||||
|
||||
public showNewTorrent = false;
|
||||
public showSettings = false;
|
||||
|
||||
public profile: Profile;
|
||||
public providerLink: string;
|
||||
|
||||
constructor(private settingsService: SettingsService, private authService: AuthService, private router: Router) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsService.getProfile().subscribe((result) => {
|
||||
this.profile = result;
|
||||
|
||||
switch (result.provider) {
|
||||
case 'RealDebrid':
|
||||
this.providerLink = 'https://real-debrid.com/?id=1348683';
|
||||
break;
|
||||
case 'AllDebrid':
|
||||
this.providerLink = 'https://alldebrid.com/?uid=2v91l&lang=en';
|
||||
break;
|
||||
case 'Premiumize':
|
||||
this.providerLink = 'https://www.premiumize.me/';
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
27
client/src/app/nl2br.pipe.ts
Normal file
27
client/src/app/nl2br.pipe.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
38
client/src/app/profile/profile.component.html
Normal file
38
client/src/app/profile/profile.component.html
Normal 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>
|
||||
36
client/src/app/profile/profile.component.ts
Normal file
36
client/src/app/profile/profile.component.ts
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ export class SettingsService {
|
|||
}
|
||||
|
||||
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> {
|
||||
|
|
@ -33,4 +33,11 @@ export class SettingsService {
|
|||
public testWriteSpeed(): Observable<number> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
150
client/src/app/settings/settings.component.html
Normal file
150
client/src/app/settings/settings.component.html
Normal 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>
|
||||
0
client/src/app/settings/settings.component.scss
Normal file
0
client/src/app/settings/settings.component.scss
Normal file
141
client/src/app/settings/settings.component.ts
Normal file
141
client/src/app/settings/settings.component.ts
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,64 +6,93 @@
|
|||
<img src="../../assets/logo.png" />
|
||||
<div class="box" *ngIf="step === 1">
|
||||
<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 class="field">
|
||||
<label for="" class="label">Username</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required />
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-envelope"></i>
|
||||
</span>
|
||||
<form (ngSubmit)="setup()">
|
||||
<div class="field">
|
||||
<label for="" class="label">Username</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required />
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-envelope"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="" class="label" name="password">Password</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="password" class="input" required name="password" [(ngModel)]="password" />
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-lock"></i>
|
||||
</span>
|
||||
<div class="field">
|
||||
<label for="" class="label" name="password">Password</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="password" class="input" required name="password" [(ngModel)]="password" />
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-lock"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button
|
||||
class="button is-success"
|
||||
(click)="setup()"
|
||||
[ngClass]="{ 'is-loading': working }"
|
||||
[disabled]="working"
|
||||
>
|
||||
Setup
|
||||
</button>
|
||||
</div>
|
||||
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="button is-success" [ngClass]="{ 'is-loading': working }" [disabled]="working">
|
||||
Setup
|
||||
</button>
|
||||
</div>
|
||||
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
||||
{{ error }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="box" *ngIf="step === 2">
|
||||
<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 />
|
||||
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"
|
||||
>Use this link to sign up to RealDebrid.</a
|
||||
>Use this link to sign up to Real-Debrid.</a
|
||||
>
|
||||
<br /><br />
|
||||
To connect to RealDebrid please enter your Personal Prive API Token found here:
|
||||
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener"
|
||||
>https://real-debrid.com/apitoken</a
|
||||
>.
|
||||
<br />
|
||||
<a href="https://alldebrid.com/?uid=2v91l&lang=en" target="_blank" rel="noopener"
|
||||
>Use this link to sign up to AllDebrid.</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 class="field">
|
||||
<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 />
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-envelope"></i>
|
||||
</span>
|
||||
</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 class="field">
|
||||
<button
|
||||
class="button is-success"
|
||||
|
|
@ -81,7 +110,7 @@
|
|||
|
||||
<div class="box" *ngIf="step === 3">
|
||||
<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"
|
||||
>https://github.com/rogerfar/rdt-client/</a
|
||||
>.
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Setting } from '../models/setting.model';
|
||||
import { SettingsService } from '../settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-setup',
|
||||
templateUrl: './setup.component.html',
|
||||
styleUrls: ['./setup.component.scss'],
|
||||
})
|
||||
export class SetupComponent implements OnInit {
|
||||
export class SetupComponent {
|
||||
public userName: string;
|
||||
public password: string;
|
||||
public provider = 'RealDebrid';
|
||||
public token: string;
|
||||
|
||||
public error: string;
|
||||
|
|
@ -19,9 +18,7 @@ export class SetupComponent implements OnInit {
|
|||
|
||||
public step: number = 1;
|
||||
|
||||
constructor(private authService: AuthService, private settingsService: SettingsService, private router: Router) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
constructor(private authService: AuthService, private router: Router) {}
|
||||
|
||||
public setup(): void {
|
||||
this.error = null;
|
||||
|
|
@ -40,16 +37,12 @@ export class SetupComponent implements OnInit {
|
|||
}
|
||||
|
||||
public setToken(): void {
|
||||
const setting = new Setting();
|
||||
setting.settingId = 'RealDebridApiKey';
|
||||
setting.value = this.token;
|
||||
|
||||
this.settingsService.update([setting]).subscribe(
|
||||
this.authService.setupProvider(this.provider, this.token).subscribe(
|
||||
() => {
|
||||
this.step = 3;
|
||||
this.working = false;
|
||||
},
|
||||
(err) => {
|
||||
(err: any) => {
|
||||
this.working = false;
|
||||
this.error = err.error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
td {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
td {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
.delete-icon {
|
||||
color: red;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auto .fas {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,10 @@ export class TorrentStatusPipe implements PipeTransform {
|
|||
constructor(private pipe: FileSizePipe) {}
|
||||
|
||||
transform(torrent: Torrent): string {
|
||||
if (torrent.error) {
|
||||
return torrent.error;
|
||||
}
|
||||
|
||||
if (torrent.downloads.length > 0) {
|
||||
const errors = torrent.downloads.where((m) => m.error != null);
|
||||
|
||||
|
|
@ -22,33 +26,44 @@ export class TorrentStatusPipe implements PipeTransform {
|
|||
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) {
|
||||
const bytesDone = downloading.sum((m) => m.bytesDone);
|
||||
const bytesTotal = downloading.sum((m) => m.bytesTotal);
|
||||
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';
|
||||
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) {
|
||||
const bytesDone = unpacking.sum((m) => m.bytesDone);
|
||||
const bytesTotal = unpacking.sum((m) => m.bytesTotal);
|
||||
let progress = (bytesDone / bytesTotal) * 100;
|
||||
let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length;
|
||||
|
||||
if (allSpeeds > 0) {
|
||||
return `Extracting (${progress.toFixed(2)}%)`;
|
||||
if (isNaN(progress)) {
|
||||
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);
|
||||
|
|
@ -57,20 +72,16 @@ export class TorrentStatusPipe implements PipeTransform {
|
|||
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) {
|
||||
return `Queued for downloading`;
|
||||
}
|
||||
|
||||
const unpacked = torrent.downloads.where((m) => m.unpackingFinished != null);
|
||||
|
||||
if (unpacked.length > 0) {
|
||||
return `Files unpacked`;
|
||||
}
|
||||
|
||||
const downloaded = torrent.downloads.where((m) => m.downloadFinished != null);
|
||||
|
||||
if (downloaded.length > 0) {
|
||||
return `Files downloaded to host`;
|
||||
}
|
||||
|
|
@ -91,7 +102,9 @@ export class TorrentStatusPipe implements PipeTransform {
|
|||
case RealDebridStatus.Error:
|
||||
return `Torrent error: ${torrent.rdStatusRaw}`;
|
||||
case RealDebridStatus.Finished:
|
||||
return `Torrent finished, waiting to download`;
|
||||
return `Torrent finished, waiting for download links`;
|
||||
case RealDebridStatus.Uploading:
|
||||
return `Torrent uploading`;
|
||||
default:
|
||||
return 'Unknown status';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,58 +6,86 @@
|
|||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input
|
||||
type="checkbox"
|
||||
(click)="toggleSelectAll($event)"
|
||||
[checked]="selectedTorrents.length > 0 && selectedTorrents.length === torrents.length"
|
||||
/>
|
||||
</th>
|
||||
<th>Name</th>
|
||||
<th>Category</th>
|
||||
<th>Priority</th>
|
||||
<th>Files</th>
|
||||
<th>Downloads</th>
|
||||
<th>Auto</th>
|
||||
<th>Size</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let torrent of torrents; trackBy: trackByMethod">
|
||||
<tr
|
||||
app-torrent-row
|
||||
[torrent]="torrent"
|
||||
(click)="selectTorrent(torrent)"
|
||||
(delete)="showDeleteModal($event)"
|
||||
(retry)="showRetryModal($event)"
|
||||
></tr>
|
||||
<ng-container *ngIf="showFiles[torrent.torrentId]">
|
||||
<tr class="separator">
|
||||
<td colspan="10">Downloads</td>
|
||||
</tr>
|
||||
<tr app-torrent-download [download]="download" *ngFor="let download of torrent.downloads"></tr>
|
||||
<tr class="separator">
|
||||
<td colspan="10">Files in torrent</td>
|
||||
</tr>
|
||||
<tr app-torrent-file [file]="file" *ngFor="let file of torrent.files"></tr>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<tr *ngFor="let torrent of torrents; trackBy: trackByMethod">
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
(click)="toggleSelect(torrent.torrentId)"
|
||||
[checked]="selectedTorrents.contains(torrent.torrentId)"
|
||||
/>
|
||||
</td>
|
||||
<td (click)="openTorrent(torrent.torrentId)">
|
||||
{{ torrent.rdName }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.category }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.priority }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.files.length | number }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.downloads.length | number }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent.rdSize | filesize }}
|
||||
</td>
|
||||
<td>
|
||||
{{ torrent | status }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<button
|
||||
class="button is-danger"
|
||||
(click)="showDeleteModal()"
|
||||
[disabled]="selectedTorrents.length === 0"
|
||||
*ngIf="torrents.length > 0"
|
||||
>
|
||||
Delete Selected
|
||||
</button>
|
||||
</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>
|
||||
<p class="modal-card-title">Delete selected torrents</p>
|
||||
<button class="delete" aria-label="close" (click)="deleteCancel()"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<p>Are you sure you want to delete these torrent?</p>
|
||||
<div class="field">
|
||||
<label class="label"></label>
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" [(ngModel)]="deleteData" />
|
||||
Delete Torrent from local RealDebrid Client
|
||||
Delete Torrents from client
|
||||
</label>
|
||||
<br />
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
|
||||
Delete Torrent from RealDebrid
|
||||
Delete Torrents from provider
|
||||
</label>
|
||||
<br />
|
||||
<label class="checkbox">
|
||||
|
|
@ -67,7 +95,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<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 class="notification is-danger is-light" *ngIf="deleteError?.length > 0">
|
||||
Error deleting torrent: {{ deleteError }}
|
||||
|
|
@ -80,7 +108,7 @@
|
|||
[disabled]="deleting"
|
||||
[ngClass]="{ 'is-loading': deleting }"
|
||||
>
|
||||
Delete
|
||||
Delete selected
|
||||
</button>
|
||||
<button class="button" (click)="deleteCancel()" [disabled]="deleting" [ngClass]="{ 'is-loading': deleting }">
|
||||
Cancel
|
||||
|
|
@ -88,45 +116,3 @@
|
|||
</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">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,4 @@ table {
|
|||
tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tr.separator {
|
||||
td {
|
||||
font-size: smaller;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { TorrentService } from '../torrent.service';
|
||||
import { forkJoin, Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-torrent-table',
|
||||
templateUrl: './torrent-table.component.html',
|
||||
styleUrls: ['./torrent-table.component.scss'],
|
||||
})
|
||||
export class TorrentTableComponent implements OnInit, OnDestroy {
|
||||
export class TorrentTableComponent implements OnInit {
|
||||
public torrents: Torrent[] = [];
|
||||
public selectedTorrents: string[] = [];
|
||||
public error: string;
|
||||
public showFiles: { [key: string]: boolean } = {};
|
||||
|
||||
public isDeleteModalActive: boolean;
|
||||
public deleteError: string;
|
||||
public deleting: boolean;
|
||||
public deleteTorrentId: string;
|
||||
public deleteData: boolean;
|
||||
public deleteRdTorrent: boolean;
|
||||
public deleteLocalFiles: boolean;
|
||||
|
||||
public isRetryModalActive: boolean;
|
||||
public retryError: string;
|
||||
public retrying: boolean;
|
||||
public retryTorrentId: string;
|
||||
public retry: number;
|
||||
|
||||
constructor(private torrentService: TorrentService) {}
|
||||
constructor(private router: Router, private torrentService: TorrentService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.torrentService.getList().subscribe(
|
||||
(result) => {
|
||||
this.torrents = result;
|
||||
|
||||
this.torrentService.connect();
|
||||
|
||||
this.torrentService.update$.subscribe((result2) => {
|
||||
this.torrents = result2;
|
||||
});
|
||||
|
|
@ -45,24 +38,40 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.torrentService.disconnect();
|
||||
}
|
||||
|
||||
public selectTorrent(torrent: Torrent): void {
|
||||
this.showFiles[torrent.torrentId] = !this.showFiles[torrent.torrentId];
|
||||
public openTorrent(torrentId: string): void {
|
||||
this.router.navigate([`/torrent/${torrentId}`]);
|
||||
}
|
||||
|
||||
public trackByMethod(index: number, el: Torrent): string {
|
||||
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.deleteRdTorrent = false;
|
||||
this.deleteLocalFiles = false;
|
||||
this.deleteError = null;
|
||||
|
||||
this.deleteTorrentId = torrentId;
|
||||
this.isDeleteModalActive = true;
|
||||
}
|
||||
|
||||
|
|
@ -73,43 +82,23 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
|||
public deleteOk(): void {
|
||||
this.deleting = true;
|
||||
|
||||
this.torrentService
|
||||
.delete(this.deleteTorrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.isDeleteModalActive = false;
|
||||
this.deleting = false;
|
||||
},
|
||||
(err) => {
|
||||
this.deleteError = err.error;
|
||||
this.deleting = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
let calls: Observable<void>[] = [];
|
||||
|
||||
public showRetryModal(torrentId: string): void {
|
||||
this.retry = 0;
|
||||
this.selectedTorrents.forEach((torrentId) => {
|
||||
calls.push(this.torrentService.delete(torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles));
|
||||
});
|
||||
|
||||
this.retryTorrentId = torrentId;
|
||||
this.isRetryModalActive = true;
|
||||
}
|
||||
forkJoin(calls).subscribe({
|
||||
complete: () => {
|
||||
this.isDeleteModalActive = false;
|
||||
this.deleting = false;
|
||||
|
||||
public retryCancel(): void {
|
||||
this.isRetryModalActive = false;
|
||||
}
|
||||
|
||||
public retryOk(): void {
|
||||
this.retrying = true;
|
||||
|
||||
this.torrentService.retry(this.retryTorrentId, this.retry).subscribe(
|
||||
() => {
|
||||
this.isRetryModalActive = false;
|
||||
this.retrying = false;
|
||||
this.selectedTorrents = [];
|
||||
},
|
||||
(err) => {
|
||||
this.retryError = err.error;
|
||||
this.retrying = false;
|
||||
}
|
||||
);
|
||||
error: (err) => {
|
||||
this.deleteError = err.error;
|
||||
this.deleting = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,25 @@ import { HttpClient } from '@angular/common/http';
|
|||
import { Injectable } from '@angular/core';
|
||||
import * as signalR from '@microsoft/signalr';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { Torrent } from './models/torrent.model';
|
||||
import { Torrent, TorrentFileAvailability } from './models/torrent.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TorrentService {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
public update$: Subject<Torrent[]> = new Subject();
|
||||
|
||||
private connection: signalR.HubConnection;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
this.connect();
|
||||
}
|
||||
|
||||
public connect(): void {
|
||||
if (this.connection != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build();
|
||||
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[]> {
|
||||
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`, {
|
||||
magnetLink,
|
||||
autoDelete,
|
||||
torrent,
|
||||
});
|
||||
}
|
||||
|
||||
public uploadFile(file: File, autoDelete: boolean): Observable<void> {
|
||||
public uploadFile(file: File, torrent: Torrent): Observable<void> {
|
||||
const formData: FormData = new FormData();
|
||||
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);
|
||||
}
|
||||
|
||||
public checkFilesMagnet(magnetLink: string): Observable<string[]> {
|
||||
return this.http.post<string[]>(`/Api/Torrents/CheckFilesMagnet`, {
|
||||
public checkFilesMagnet(magnetLink: string): Observable<TorrentFileAvailability[]> {
|
||||
return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFilesMagnet`, {
|
||||
magnetLink,
|
||||
});
|
||||
}
|
||||
|
||||
public checkFiles(file: File): Observable<string[]> {
|
||||
public checkFiles(file: File): Observable<TorrentFileAvailability[]> {
|
||||
const formData: FormData = new FormData();
|
||||
formData.append('file', file);
|
||||
return this.http.post<string[]>(`/Api/Torrents/CheckFiles`, formData);
|
||||
return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFiles`, formData);
|
||||
}
|
||||
|
||||
public delete(
|
||||
|
|
@ -70,9 +76,15 @@ export class TorrentService {
|
|||
});
|
||||
}
|
||||
|
||||
public retry(torrentId: string, retry: number): Observable<void> {
|
||||
return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, {
|
||||
retry,
|
||||
});
|
||||
public retry(torrentId: string): Observable<void> {
|
||||
return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, {});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
566
client/src/app/torrent/torrent.component.html
Normal file
566
client/src/app/torrent/torrent.component.html
Normal 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>
|
||||
13
client/src/app/torrent/torrent.component.scss
Normal file
13
client/src/app/torrent/torrent.component.scss
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-download {
|
||||
margin-left :12px;
|
||||
}
|
||||
214
client/src/app/torrent/torrent.component.ts
Normal file
214
client/src/app/torrent/torrent.component.ts
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UsersService {
|
||||
constructor() {}
|
||||
|
||||
public getLoggedInUser() {}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// 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`.
|
||||
|
||||
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
|
||||
* 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.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@
|
|||
<meta charset="utf-8" />
|
||||
<title>Real-Debrid Torrent Client</title>
|
||||
<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" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
* file.
|
||||
*
|
||||
* 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),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
||||
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
|
@ -18,16 +18,6 @@
|
|||
* 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
|
||||
* 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.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,34 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "es2020",
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"strictNullChecks": false,
|
||||
"target": "ES2022",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2018",
|
||||
"es2020",
|
||||
"dom"
|
||||
]
|
||||
],
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"fullTemplateTypeCheck": true,
|
||||
"strictInjectionParameters": true
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -39,12 +39,14 @@ param(
|
|||
|
||||
$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)) {
|
||||
$dockerArgs += @("--tag", "$($imageName):$($Version)" )
|
||||
$dockerApps += @("--build-arg", "VERSION=$($Version)" )
|
||||
if ([string]::IsNullOrEmpty($Version)) {
|
||||
$Version = (Get-Content "package.json" | ConvertFrom-Json).version
|
||||
}
|
||||
|
||||
$dockerArgs += @("--tag", "$($imageName):$($Version)" )
|
||||
$dockerApps += @("--build-arg", "VERSION=$($Version)" )
|
||||
|
||||
Write-Host "Generating docker image $imageName for $Platforms"
|
||||
& docker $dockerArgs
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ $newNavbar = (Get-Content $navbar) -replace 'Version .*?<', "Version $version<"
|
|||
|
||||
cd client
|
||||
npm install
|
||||
ng build --prod --output-path=..\server\RdtClient.Web\wwwroot
|
||||
ng build --configuration production --output-path=..\server\RdtClient.Web\wwwroot
|
||||
|
||||
cd ..
|
||||
cd server
|
||||
|
|
|
|||
3265
package-lock.json
generated
3265
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"name": "rdt-client",
|
||||
"version": "1.7.4",
|
||||
"version": "2.0.22",
|
||||
"description": "This is a web interface to manage your torrents on Real-Debrid.",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"gh-release": "^5.0.0"
|
||||
"gh-release": "^7.0.2"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"repository": {
|
||||
|
|
|
|||
39
server/.gitignore
vendored
Normal file
39
server/.gitignore
vendored
Normal 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/
|
||||
|
|
@ -1,148 +1,32 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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<Setting> Settings { get; set; }
|
||||
public DbSet<Torrent> Torrents { get; set; }
|
||||
|
||||
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);
|
||||
|
||||
foreach (var fk in cascadeFKs)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Download> Downloads { get; set; }
|
||||
public DbSet<Setting> Settings { 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);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
fk.DeleteBehavior = DeleteBehavior.Restrict;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,190 +1,267 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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);
|
||||
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);
|
||||
_dataContext = dataContext;
|
||||
}
|
||||
|
||||
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
|
||||
.AsNoTracking()
|
||||
.Include(m => m.Torrent)
|
||||
.ToListAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
.AsNoTracking()
|
||||
.Where(m => m.TorrentId == torrentId)
|
||||
.ToListAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task<Download> GetById(Guid downloadId)
|
||||
dbDownload.DownloadFinished = dateTime;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
await TorrentData.VoidCache();
|
||||
}
|
||||
|
||||
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime)
|
||||
{
|
||||
var dbDownload = await _dataContext.Downloads
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||
|
||||
if (dbDownload == null)
|
||||
{
|
||||
return await _dataContext.Downloads
|
||||
.Include(m => m.Torrent)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task<Download> Get(Guid torrentId, String path)
|
||||
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)
|
||||
{
|
||||
return await _dataContext.Downloads.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task<Download> Add(Guid torrentId, String path)
|
||||
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 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;
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
|
||||
dbDownload.UnpackingFinished = dateTime;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
await TorrentData.VoidCache();
|
||||
}
|
||||
|
||||
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
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||
|
||||
dbDownload.Link = unrestrictedLink;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime)
|
||||
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
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||
|
||||
dbDownload.DownloadStarted = dateTime;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime)
|
||||
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
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||
|
||||
if (dbDownload == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dbDownload.DownloadFinished = dateTime;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime)
|
||||
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 dbDownload = await _dataContext.Downloads
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||
|
||||
dbDownload.UnpackingQueued = dateTime;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime)
|
||||
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)
|
||||
{
|
||||
var dbDownload = await _dataContext.Downloads
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||
|
||||
dbDownload.UnpackingStarted = dateTime;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
throw new Exception($"Cannot find download with ID {downloadId}");
|
||||
}
|
||||
|
||||
public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime)
|
||||
{
|
||||
var dbDownload = await _dataContext.Downloads
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == 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;
|
||||
|
||||
dbDownload.UnpackingFinished = dateTime;
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime)
|
||||
{
|
||||
var dbDownload = await _dataContext.Downloads
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||
|
||||
dbDownload.Completed = dateTime;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateError(Guid downloadId, String error)
|
||||
{
|
||||
var dbDownload = await _dataContext.Downloads
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||
|
||||
dbDownload.Error = error;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateRetryCount(Guid downloadId, Int32 retryCount)
|
||||
{
|
||||
var dbDownload = await _dataContext.Downloads
|
||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||
|
||||
dbDownload.RetryCount = retryCount;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +1,212 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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();
|
||||
Task Update(IList<Setting> settings);
|
||||
Task<Setting> Get(String key);
|
||||
return GetSettings(Get, null).ToList();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
return await _dataContext.Settings.AsNoTracking().ToListAsync();
|
||||
}
|
||||
|
||||
public async Task Update(IList<Setting> settings)
|
||||
{
|
||||
var dbSettings = await _dataContext.Settings.ToListAsync();
|
||||
|
||||
foreach (var dbSetting in dbSettings)
|
||||
if (setting != null)
|
||||
{
|
||||
var setting = settings.FirstOrDefault(m => m.SettingId == dbSetting.SettingId);
|
||||
|
||||
if (setting != null)
|
||||
{
|
||||
dbSetting.Value = setting.Value;
|
||||
}
|
||||
dbSetting.Value = setting.Value?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +1,309 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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();
|
||||
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);
|
||||
_dataContext = dataContext;
|
||||
}
|
||||
|
||||
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
|
||||
.AsNoTracking()
|
||||
.Include(m => m.Downloads)
|
||||
.ToListAsync();
|
||||
|
||||
return results.OrderByDescending(m => m.Added).ToList();
|
||||
file.Torrent = null;
|
||||
}
|
||||
|
||||
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
|
||||
.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;
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<Torrent> GetByHash(String hash)
|
||||
foreach (var file in dbTorrent.Downloads)
|
||||
{
|
||||
var dbTorrent = await _dataContext.Torrents
|
||||
.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;
|
||||
file.Torrent = null;
|
||||
}
|
||||
|
||||
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,
|
||||
RdId = realDebridId,
|
||||
Hash = hash.ToLower(),
|
||||
Category = category,
|
||||
AutoDelete = autoDelete,
|
||||
FileOrMagnet = fileOrMagnetContents,
|
||||
IsFile = isFile
|
||||
};
|
||||
TorrentId = Guid.NewGuid(),
|
||||
Added = DateTimeOffset.UtcNow,
|
||||
RdId = realDebridId,
|
||||
Hash = hash.ToLower(),
|
||||
Category = torrent.Category,
|
||||
HostDownloadAction = torrent.HostDownloadAction,
|
||||
DownloadAction = torrent.DownloadAction,
|
||||
FinishedAction = torrent.FinishedAction,
|
||||
DownloadMinSize = torrent.DownloadMinSize,
|
||||
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;
|
||||
dbTorrent.RdHost = torrent.RdHost;
|
||||
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;
|
||||
dbTorrent.RdFiles = torrent.RdFiles;
|
||||
|
||||
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 == torrent.TorrentId);
|
||||
|
||||
if (dbTorrent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dbTorrent.RdName = torrent.RdName;
|
||||
dbTorrent.RdSize = torrent.RdSize;
|
||||
dbTorrent.RdHost = torrent.RdHost;
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task UpdateCategory(Guid torrentId, String category)
|
||||
dbTorrent.Category = torrent.Category;
|
||||
dbTorrent.Priority = torrent.Priority;
|
||||
dbTorrent.DownloadRetryAttempts = torrent.DownloadRetryAttempts;
|
||||
dbTorrent.TorrentRetryAttempts = torrent.TorrentRetryAttempts;
|
||||
dbTorrent.DeleteOnError = torrent.DeleteOnError;
|
||||
dbTorrent.Lifetime = torrent.Lifetime;
|
||||
|
||||
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)
|
||||
{
|
||||
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||
|
||||
if (dbTorrent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dbTorrent.Category = category;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task UpdateComplete(Guid torrentId, DateTimeOffset? datetime)
|
||||
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);
|
||||
|
||||
dbTorrent.Completed = datetime;
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task Delete(Guid torrentId)
|
||||
if (String.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||
var downloads = await _dataContext.Downloads.AsNoTracking().Where(m => m.TorrentId == torrentId).ToListAsync();
|
||||
var downloadWithErrors = downloads.Where(m => !String.IsNullOrWhiteSpace(m.Error)).ToList();
|
||||
|
||||
if (dbTorrent == null)
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,19 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
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;
|
||||
|
||||
public UserData(DataContext dataContext)
|
||||
{
|
||||
_dataContext = dataContext;
|
||||
}
|
||||
|
||||
public async Task<IdentityUser> GetUser()
|
||||
{
|
||||
return await _dataContext.Users.FirstOrDefaultAsync();
|
||||
}
|
||||
return await _dataContext.Users.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,25 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
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>();
|
||||
services.AddScoped<ISettingData, SettingData>();
|
||||
services.AddScoped<ITorrentData, TorrentData>();
|
||||
services.AddScoped<IUserData, UserData>();
|
||||
throw new Exception("Invalid database path found in appSettings");
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
}
|
||||
12
server/RdtClient.Data/Enums/AuthenticationType.cs
Normal file
12
server/RdtClient.Data/Enums/AuthenticationType.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace RdtClient.Data.Enums;
|
||||
|
||||
public enum AuthenticationType
|
||||
{
|
||||
[Description("Username + Password")]
|
||||
UserNamePassword,
|
||||
|
||||
[Description("No Authentication")]
|
||||
None
|
||||
}
|
||||
12
server/RdtClient.Data/Enums/DownloadClient.cs
Normal file
12
server/RdtClient.Data/Enums/DownloadClient.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace RdtClient.Data.Enums;
|
||||
|
||||
public enum DownloadClient
|
||||
{
|
||||
[Description("Internal Downloader")]
|
||||
Internal,
|
||||
|
||||
[Description("Aria2c")]
|
||||
Aria2c
|
||||
}
|
||||
18
server/RdtClient.Data/Enums/LogLevel.cs
Normal file
18
server/RdtClient.Data/Enums/LogLevel.cs
Normal 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
|
||||
}
|
||||
15
server/RdtClient.Data/Enums/Provider.cs
Normal file
15
server/RdtClient.Data/Enums/Provider.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace RdtClient.Data.Enums;
|
||||
|
||||
public enum Provider
|
||||
{
|
||||
[Description("RealDebrid")]
|
||||
RealDebrid,
|
||||
|
||||
[Description("AllDebrid")]
|
||||
AllDebrid,
|
||||
|
||||
[Description("Premiumize")]
|
||||
Premiumize
|
||||
}
|
||||
15
server/RdtClient.Data/Enums/TorrentDownloadAction.cs
Normal file
15
server/RdtClient.Data/Enums/TorrentDownloadAction.cs
Normal 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
|
||||
}
|
||||
18
server/RdtClient.Data/Enums/TorrentFinishedAction.cs
Normal file
18
server/RdtClient.Data/Enums/TorrentFinishedAction.cs
Normal 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
|
||||
}
|
||||
12
server/RdtClient.Data/Enums/TorrentHostDownloadAction.cs
Normal file
12
server/RdtClient.Data/Enums/TorrentHostDownloadAction.cs
Normal 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,
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
namespace RdtClient.Data.Enums
|
||||
{
|
||||
public enum RealDebridStatus
|
||||
{
|
||||
Processing = 0,
|
||||
WaitingForFileSelection = 1,
|
||||
Downloading = 2,
|
||||
Finished = 3,
|
||||
namespace RdtClient.Data.Enums;
|
||||
|
||||
Error = 99
|
||||
}
|
||||
public enum TorrentStatus
|
||||
{
|
||||
Processing = 0,
|
||||
WaitingForFileSelection = 1,
|
||||
Downloading = 2,
|
||||
Finished = 3,
|
||||
Uploading = 4,
|
||||
|
||||
Error = 99
|
||||
}
|
||||
430
server/RdtClient.Data/Migrations/20210718222602_Torrents_Add_Actions.Designer.cs
generated
Normal file
430
server/RdtClient.Data/Migrations/20210718222602_Torrents_Add_Actions.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
433
server/RdtClient.Data/Migrations/20210718231758_Torrents_Add_FilesSelected.Designer.cs
generated
Normal file
433
server/RdtClient.Data/Migrations/20210718231758_Torrents_Add_FilesSelected.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
436
server/RdtClient.Data/Migrations/20211009221133_Downloads_Add_RemoteId.Designer.cs
generated
Normal file
436
server/RdtClient.Data/Migrations/20211009221133_Downloads_Add_RemoteId.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
439
server/RdtClient.Data/Migrations/20211011193848_Torrent_Add_RetryCount.Designer.cs
generated
Normal file
439
server/RdtClient.Data/Migrations/20211011193848_Torrent_Add_RetryCount.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue