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/
|
**/.DS_Store/
|
||||||
|
|
||||||
### DotnetCore ###
|
|
||||||
# .NET Core build folders
|
|
||||||
server/**/bin
|
|
||||||
server/**/obj
|
|
||||||
server/**/*.user
|
|
||||||
server/**/.vs
|
|
||||||
server/RdtClient.Web/wwwroot/
|
|
||||||
|
|
||||||
build/
|
build/
|
||||||
publish/
|
publish/
|
||||||
server/Downloader/
|
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
RealDebridClient.zip
|
RealDebridClient.zip
|
||||||
|
server/RdtClient.Web/appsettings.Development.json
|
||||||
|
|
|
||||||
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/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.0.22] - 2023-03-11
|
||||||
|
### Added
|
||||||
|
- Add support for Premiumize
|
||||||
|
|
||||||
|
## [2.0.21] - 2023-03-09
|
||||||
|
### Changed
|
||||||
|
- Fixed docker build by pinning the .NET version to LTS.
|
||||||
|
|
||||||
|
## [2.0.20] - 2023-03-08
|
||||||
|
### Added
|
||||||
|
- Add support for multi-level unpacking.
|
||||||
|
- Add settings to specify the Error and Processed paths for the watch folders.
|
||||||
|
- Option to disable authentication completely.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Removed the Simple Downloader and replaced it with https://github.com/bezzad/Downloader as the Internal Downloader.
|
||||||
|
- Fixed setting the Base Href for sub folder hosting.
|
||||||
|
|
||||||
|
## [2.0.19] - 2022-10-18
|
||||||
|
### Changed
|
||||||
|
- Changed the AllDebrid provider to use HTTPS instead of HTTP.
|
||||||
|
|
||||||
|
## [2.0.18] - 2022-10-18
|
||||||
|
### Added
|
||||||
|
- Added the option to bulk delete torrents, thanks kanazaca!
|
||||||
|
- Added option to remove the torrent only from the client after downloads are completed.
|
||||||
|
- Added option to change the category of an existing torrent.
|
||||||
|
- Added option to not download files to the host.
|
||||||
|
### Changed
|
||||||
|
- If a watched file gives an error when adding, move it to an error folder.
|
||||||
|
|
||||||
|
## [2.0.17] - 2022-05-24
|
||||||
|
### Changed
|
||||||
|
- Fixed issue with some settings not saving.
|
||||||
|
|
||||||
|
## [2.0.16] - 2022-05-24
|
||||||
|
### Changed
|
||||||
|
- Fixed MacOS pre-fill on the login screen.
|
||||||
|
|
||||||
|
## [2.0.15] - 2022-05-15
|
||||||
|
### Changed
|
||||||
|
- Remove settings for Finish Action and Category for the qBittorrent integration, these should always be set to None and the category comes from the integration.
|
||||||
|
|
||||||
|
## [2.0.14] - 2022-05-14
|
||||||
|
### Changed
|
||||||
|
- Fixed Windows Service issue
|
||||||
|
- For service users: the appsettings.json is slightly changed: HostUrl is now Port. Important if you used a non standard (6500) port.
|
||||||
|
|
||||||
|
## [2.0.13] - 2022-05-14
|
||||||
|
### Changed
|
||||||
|
- Rewrote the settings storage. Added more settings to set defaults for importing.
|
||||||
|
- Fixed filtering of torrents when a category is passed to the TorrentsInfo endpoint, fixing Radarr/Sonarr integrations.
|
||||||
|
### Added
|
||||||
|
- Added settings to control the timeout and polling interval to RealDebrid/AllDebrid.
|
||||||
|
|
||||||
|
## [2.0.12] - 2022-03-20
|
||||||
|
### Changed
|
||||||
|
- Fixed the AllDebrid client.
|
||||||
|
|
||||||
|
## [2.0.11] - 2022-03-19
|
||||||
|
### Changed
|
||||||
|
- Fixed the "Progress" for the AllDebrid client, thanks @23doors.
|
||||||
|
|
||||||
|
## [2.0.10] - 2022-03-19
|
||||||
|
### Added
|
||||||
|
- When changing the download speed setting for the simple downloader it will apply the setting to active downloads.
|
||||||
|
- Add running of external applications when the torrent is finished.
|
||||||
|
### Changed
|
||||||
|
- Fixed deserialization of the availability check for RealDebrid.
|
||||||
|
- Fixed the simple downloader download limiter.
|
||||||
|
|
||||||
|
## [2.0.9] - 2022-03-12
|
||||||
|
### Changed
|
||||||
|
- Updated packages, added logging to the RD and AD providers when serialization fails.
|
||||||
|
|
||||||
|
## [2.0.8] - 2022-02-28
|
||||||
|
### Changed
|
||||||
|
- Fixed issue with AllDebrid sometimes returning NULL links.
|
||||||
|
|
||||||
|
## [2.0.7] - 2022-02-06
|
||||||
|
### Changed
|
||||||
|
- Added setting to set the category when a torrent is imported from RealDebrid or other provider.
|
||||||
|
|
||||||
|
## [2.0.6] - 2022-02-06
|
||||||
|
### Added
|
||||||
|
- Added setting to automatically delete torrents in the state of error after a certain amount of time.
|
||||||
|
- Added lifetime setting to automatically expire torrents after a certain amount of time.
|
||||||
|
|
||||||
|
## [2.0.5] - 2022-01-11
|
||||||
|
### Changed
|
||||||
|
- Updated AllDebrid provider to fix issue with ID's not being a number.
|
||||||
|
|
||||||
|
## [2.0.4] - 2022-01-08
|
||||||
|
### Changed
|
||||||
|
- Fixed bug where the QBittorrent API didn't report the error state correctly when an error ocurred in the debrid provider.
|
||||||
|
|
||||||
|
## [2.0.3] - 2022-01-02
|
||||||
|
### Changed
|
||||||
|
- Fixed automatic adding of AllDebrid torrents.
|
||||||
|
### Added
|
||||||
|
- Added update notification.
|
||||||
|
|
||||||
|
## [2.0.2] - 2021-11-24
|
||||||
|
### Changed
|
||||||
|
- Fixed update timer for providers.
|
||||||
|
|
||||||
|
## [2.0.1] - 2021-11-24
|
||||||
|
### Changed
|
||||||
|
- Fixed potential issue with the Real-Debrid mapper.
|
||||||
|
- Fixed serialization issue for AllDebrid.
|
||||||
|
|
||||||
|
## [2.0.0] - 2021-11-21
|
||||||
|
### Changed
|
||||||
|
- Update projects to .NET6 and Angular 13.
|
||||||
|
### Added
|
||||||
|
- Added setting to automatically import torrents from RealDebrid / AllDebrid.
|
||||||
|
- Added setting to automatically delete torrents from RealDebridClient when they have been removed from RealDebrid / AllDebrid.
|
||||||
|
|
||||||
|
## [1.9.8] - 2021-10-30
|
||||||
|
### Added
|
||||||
|
- Add speed limit setting on the Simple downloader.
|
||||||
|
|
||||||
|
## [1.9.7] - 2021-10-30
|
||||||
|
### Added
|
||||||
|
- Add AllDebrid support.
|
||||||
|
|
||||||
|
## [1.9.6] - 2021-10-30
|
||||||
|
### Added
|
||||||
|
- Improved handling of errors on the torrent itself
|
||||||
|
- Added retry counts when adding torrents
|
||||||
|
- Added retry counts on the Sonarr/Radarr settings page
|
||||||
|
|
||||||
|
## [1.9.5] - 2021-10-28
|
||||||
|
### Changed
|
||||||
|
- Fixed issues with the simple downloader.
|
||||||
|
- Fixed issue with retrying downloads.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added torrent uploading status from Real Debrid.
|
||||||
|
- Restored removing of torrents when deleted from Real Debrid.
|
||||||
|
|
||||||
|
## [1.9.4] - 2021-10-28
|
||||||
|
### Changed
|
||||||
|
- Fixed issues retrying torrents after multiple failed downloads in large torrents.
|
||||||
|
|
||||||
|
## [1.9.3] - 2021-10-27
|
||||||
|
### Changed
|
||||||
|
- Fixed issue with the unpack queue.
|
||||||
|
|
||||||
|
## [1.9.2] - 2021-10-27
|
||||||
|
### Changed
|
||||||
|
- Fixed issue where not the correct torrent was used for a download.
|
||||||
|
|
||||||
|
## [1.9.1] - 2021-10-27
|
||||||
|
### Added
|
||||||
|
- Added automatic torrent retrying when RealDebrid reports an error on the torrent.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Changed how Aria2 is polled, this will result in less RPC calls for a large amount of torrents.
|
||||||
|
- Add more and better logging for the torrent runner in Debug.
|
||||||
|
- Add Aria2 checks to see if links get added properly.
|
||||||
|
- Update Aria2.NET and RD.NET, added automatic retrying in case of server failures.
|
||||||
|
|
||||||
|
## [1.9.0] - 2021-10-24
|
||||||
|
### Added
|
||||||
|
- Add priorty for torrents. You can set the priority when adding a new torrent. When added from Sonarr/Radarr it will assume no priority.
|
||||||
|
- Added support for the Sonarr/Radarr/qBittorrent setPrio command. It will set the priority of a torrent to 1.
|
||||||
|
- Added support for the Sonarr/Radarr/qBittorrent pause and resume commands. This only works with the Aria2 downloader.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Fixed issues when downloads get deleted when they are still being checked.
|
||||||
|
- Improved timings and retrying of adding of downloads to Aria2. Will now try 5 times before failing.
|
||||||
|
|
||||||
|
## [1.8.9] - 2021-10-23
|
||||||
|
### Changed
|
||||||
|
- Add delays between adding downloads to Aria2 to avoid Aria2 going down when adding a large amount of downloads.
|
||||||
|
|
||||||
|
## [1.8.8] - 2021-10-21
|
||||||
|
### Changed
|
||||||
|
- Fixed starting downloads when RealDebrid reports ghost links in torrents.
|
||||||
|
|
||||||
|
## [1.8.7] - 2021-10-11
|
||||||
|
### Added
|
||||||
|
- Add Aria2 test connection button.
|
||||||
|
- Add full torrent retry mechanism, by default it will now retry the torrent 2 times when a download fails for more than 3 times.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved Aria2 download handling.
|
||||||
|
|
||||||
|
## [1.8.6] - 2021-10-09
|
||||||
|
### Added
|
||||||
|
- Experimental support for a Aria2 download client. Check the readme for usage.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Fixed potential error when sonarr is querying and the torrent isn't added to RealDebrid yet.
|
||||||
|
- Fixed interface randomly stop updating.
|
||||||
|
- Upgrade dependencies.
|
||||||
|
|
||||||
|
## [1.8.5] - 2021-10-07
|
||||||
|
### Changed
|
||||||
|
- Fixed issue where deleting a torrent could error out.
|
||||||
|
|
||||||
|
## [1.8.4] - 2021-08-05
|
||||||
|
### Changed
|
||||||
|
- Changed the default timeout for Real-Debrid communication to 10 seconds instead of 100 seconds.
|
||||||
|
|
||||||
|
## [1.8.3] - 2021-08-05
|
||||||
|
### Changed
|
||||||
|
- Fixed potential issue with duplicates categories.
|
||||||
|
|
||||||
|
## [1.8.2] - 2021-08-02
|
||||||
|
### Changed
|
||||||
|
- Fixed issue with starting downloads.
|
||||||
|
|
||||||
|
## [1.8.1] - 2021-07-31
|
||||||
|
### Changed
|
||||||
|
- Fixed issue where downloads were hanging the full interface.
|
||||||
|
|
||||||
|
## [1.8.0] - 2021-07-18
|
||||||
|
### Added
|
||||||
|
- Fixed support for categories. They are now saved persistently in the database.
|
||||||
|
- Added new "settings" page.
|
||||||
|
- Added new "add new" page. Added more options when manually adding a torrent, including the ability to manually select files.
|
||||||
|
- Added dedicated torrent pages with more information when selecting a torrent on the main page.
|
||||||
|
- Add ability to retry individual downloads.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Fixed enter key on the login and setup screen.
|
||||||
|
- Fixed an issue with selecting files and getting the links. This process could take a long time and would hang the client while waiting for a response.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Removed the retry and delete button from the main page and moved them to the torrent page.
|
||||||
|
- Removed the ability to retry all downloads.
|
||||||
|
|
||||||
|
## [1.7.8] - 2021-06-17
|
||||||
|
### Changed
|
||||||
|
- Fixed issue for real this time with a broken response from RDT when the available files returns in a format different than normal.
|
||||||
|
|
||||||
|
## [1.7.7] - 2021-06-09
|
||||||
|
### Changed
|
||||||
|
- Fixed some issues with download error handling.
|
||||||
|
- Fixed issue where files aren't always properly selected.
|
||||||
|
|
||||||
|
## [1.7.6] - 2021-06-05
|
||||||
|
### Changed
|
||||||
|
- Fixed build for Raspberry PI.
|
||||||
|
|
||||||
|
## [1.7.5] - 2021-06-05
|
||||||
|
### Changed
|
||||||
|
- Reduced the frequency of database reads and writes by adding caches for settings and torrents.
|
||||||
|
- Updated dependency of RD.NET to improve serialization error reporting.
|
||||||
|
- Changed how the base href is determined to support path proxies.
|
||||||
|
- Fixed issue with handling of renamed torrents.
|
||||||
|
|
||||||
## [1.7.4] - 2021-04-22
|
## [1.7.4] - 2021-04-22
|
||||||
### Changed
|
### Changed
|
||||||
- Changed how the docker is built.
|
- Changed how the docker is built.
|
||||||
|
|
|
||||||
31
Dockerfile
31
Dockerfile
|
|
@ -1,5 +1,5 @@
|
||||||
# Stage 1 - Build the frontend
|
# Stage 1 - Build the frontend
|
||||||
FROM node:15.5-buster AS node-build-env
|
FROM node:16-buster AS node-build-env
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
|
|
@ -9,17 +9,17 @@ RUN mkdir /appclient
|
||||||
WORKDIR /appclient
|
WORKDIR /appclient
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
echo "**** Cloning Source Code ****" && \
|
echo "**** Cloning Source Code ****" && \
|
||||||
git clone https://github.com/rogerfar/rdt-client.git . && \
|
git clone https://github.com/rogerfar/rdt-client.git . && \
|
||||||
cd client && \
|
cd client && \
|
||||||
echo "**** Building Code ****" && \
|
echo "**** Building Code ****" && \
|
||||||
npm ci && \
|
npm ci && \
|
||||||
npx ng build --prod --output-path=out
|
npx ng build --output-path=out
|
||||||
|
|
||||||
RUN ls -FCla /appclient/root
|
RUN ls -FCla /appclient/root
|
||||||
|
|
||||||
# Stage 2 - Build the backend
|
# Stage 2 - Build the backend
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim-amd64 AS dotnet-build-env
|
FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim-amd64 AS dotnet-build-env
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
|
|
@ -29,15 +29,18 @@ RUN mkdir /appserver
|
||||||
WORKDIR /appserver
|
WORKDIR /appserver
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
echo "**** Cloning Source Code ****" && \
|
echo "**** Cloning Source Code ****" && \
|
||||||
git clone https://github.com/rogerfar/rdt-client.git . && \
|
git clone https://github.com/rogerfar/rdt-client.git . && \
|
||||||
echo "**** Building Source Code for $TARGETPLATFORM on $BUILDPLATFORM ****" && \
|
echo "**** Building Source Code for $TARGETPLATFORM on $BUILDPLATFORM ****" && \
|
||||||
cd server && \
|
cd server && \
|
||||||
if [ "$TARGETPLATFORM" = "linux/arm/v7" -o "$TARGETPLATFORM" = "linux/arm64" ] ; then \
|
if [ "$TARGETPLATFORM" = "linux/arm/v7" ] ; then \
|
||||||
echo "**** Building $TARGETPLATFORM arm version" && \
|
echo "**** Building $TARGETPLATFORM arm v7 version" && \
|
||||||
dotnet restore -r linux-arm RdtClient.sln && dotnet publish -r linux-arm -c Release -o out ; \
|
dotnet restore -r linux-arm RdtClient.sln && dotnet publish -r linux-arm -c Release -o out ; \
|
||||||
|
elif [ "$TARGETPLATFORM" = "linux/arm/v8"] ; then \
|
||||||
|
echo "**** Building $TARGETPLATFORM arm v8 version" && \
|
||||||
|
dotnet restore -r linux-arm64 RdtClient.sln && dotnet publish -r linux-arm64 -c Release -o out ; \
|
||||||
else \
|
else \
|
||||||
echo "**** Building $TARGETPLATFORM x86 version" && \
|
echo "**** Building $TARGETPLATFORM x64 version" && \
|
||||||
dotnet restore RdtClient.sln && dotnet publish -c Release -o out ; \
|
dotnet restore RdtClient.sln && dotnet publish -c Release -o out ; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -61,16 +64,16 @@ ENV RDTCLIENT_BRANCH="main"
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
mkdir -p /data/downloads /data/db || true && \
|
mkdir -p /data/downloads /data/db || true && \
|
||||||
echo "**** Updating package information ****" && \
|
echo "**** Updating package information ****" && \
|
||||||
apt-get update -y -qq && \
|
apt-get update -y -qq && \
|
||||||
echo "**** Install pre-reqs ****" && \
|
echo "**** Install pre-reqs ****" && \
|
||||||
apt-get install -y -qq wget dos2unix && \
|
apt-get install -y -qq wget dos2unix && \
|
||||||
apt-get install -y libc6 libgcc1 libgssapi-krb5-2 libssl1.1 libstdc++6 zlib1g libicu66 && \
|
apt-get install -y libc6 libgcc1 libgssapi-krb5-2 libssl1.1 libstdc++6 zlib1g libicu66 && \
|
||||||
echo "**** Installing dotnet ****" && \
|
echo "**** Installing dotnet ****" && \
|
||||||
wget -q https://dot.net/v1/dotnet-install.sh && \
|
wget -q https://dot.net/v1/dotnet-install.sh && \
|
||||||
chmod +x ./dotnet-install.sh && \
|
chmod +x ./dotnet-install.sh && \
|
||||||
bash ./dotnet-install.sh -c Current --runtime dotnet --install-dir /usr/share/dotnet && \
|
bash ./dotnet-install.sh -c LTS -v latest --runtime dotnet --install-dir /usr/share/dotnet && \
|
||||||
bash ./dotnet-install.sh -c Current --runtime aspnetcore --install-dir /usr/share/dotnet && \
|
bash ./dotnet-install.sh -c LTS -v latest --runtime aspnetcore --install-dir /usr/share/dotnet && \
|
||||||
echo "**** Cleaning image ****" && \
|
echo "**** Cleaning image ****" && \
|
||||||
apt-get -y -qq -o Dpkg::Use-Pty=0 clean && apt-get -y -qq -o Dpkg::Use-Pty=0 purge && \
|
apt-get -y -qq -o Dpkg::Use-Pty=0 clean && apt-get -y -qq -o Dpkg::Use-Pty=0 purge && \
|
||||||
echo "**** Setting permissions ****" && \
|
echo "**** Setting permissions ****" && \
|
||||||
|
|
|
||||||
97
README.md
97
README.md
|
|
@ -1,16 +1,22 @@
|
||||||
# Real-Debrid Torrent Client
|
# Real-Debrid Torrent Client
|
||||||
|
|
||||||
This is a web interface to manage your torrents on Real-Debrid. It supports the following features:
|
This is a web interface to manage your torrents on Real-Debrid, AllDebrid or Premiumize. It supports the following features:
|
||||||
|
|
||||||
- Add new torrents through magnets or files
|
- Add new torrents through magnets or files
|
||||||
- Download all files from Real Debrid to your local machine automatically
|
- Download all files from Real-Debrid, AllDebrid or Premiumize to your local machine automatically
|
||||||
- Unpack all files when finished downloading
|
- Unpack all files when finished downloading
|
||||||
- Implements a fake qBittorrent API so you can hook up other applications like Sonarr or Couchpotato.
|
- Implements a fake qBittorrent API so you can hook up other applications like Sonarr, Radarr or Couchpotato.
|
||||||
- Built with Angular 11 and .NET 5
|
- Built with Angular 15 and .NET 6
|
||||||
|
|
||||||
**You will need a Premium service at Real-Debrid!**
|
**You will need a Premium service at Real-Debrid, AllDebrid or Premiumize!**
|
||||||
|
|
||||||
[Click here to sign up (referal link so I can get a few free premium days).](https://real-debrid.com/?id=1348683)
|
[Click here to sign up for Real-Debrid.](https://real-debrid.com/?id=1348683)
|
||||||
|
|
||||||
|
[Click here to sign up for AllDebrid.](https://alldebrid.com/?uid=2v91l)
|
||||||
|
|
||||||
|
[Click here to sign up for Premiumize.](https://www.premiumize.me/)
|
||||||
|
|
||||||
|
<sub>(referal links so I can get a few free premium days)</sub>
|
||||||
|
|
||||||
## Docker Setup
|
## Docker Setup
|
||||||
|
|
||||||
|
|
@ -62,15 +68,65 @@ docker-compose up -d
|
||||||
|
|
||||||
Replace the paths in `volumes` as in the above step.
|
Replace the paths in `volumes` as in the above step.
|
||||||
|
|
||||||
|
## Run as a Service
|
||||||
|
|
||||||
|
Instead of running in Docker you can install it as a service in Windows or Linux.
|
||||||
## Windows Service
|
## Windows Service
|
||||||
|
|
||||||
Instead of running in Docker you can install it as a service in Windows or Linux (not tested).
|
1. Make sure you have the ASP.NET Core Runtime 6 installed: [https://dotnet.microsoft.com/download/dotnet/6.0](https://dotnet.microsoft.com/download/dotnet/6.0)
|
||||||
|
|
||||||
1. Get the latest zip file from the Releases page and extract it to your host.
|
1. Get the latest zip file from the Releases page and extract it to your host.
|
||||||
1. Open the `appsettings.json` file and replace the `LogLevel` `Path` to a path on your host.
|
1. Open the `appsettings.json` file and replace the `LogLevel` `Path` to a path on your host.
|
||||||
1. In `appsettings.json` replace the `Database` `Path` to a path on your host.
|
1. In `appsettings.json` replace the `Database` `Path` to a path on your host.
|
||||||
1. When using Windows paths, make sure to escape the slashes. For example: `D:\\RdtClient\\db\\rdtclient.db`
|
1. When using Windows paths, make sure to escape the slashes. For example: `D:\\RdtClient\\db\\rdtclient.db`
|
||||||
|
|
||||||
|
## Linux Service
|
||||||
|
|
||||||
|
Instead of running in Docker you can install it as a service in Linux.
|
||||||
|
|
||||||
|
1. Install .NET: [https://docs.microsoft.com/en-us/dotnet/core/install/linux](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
|
||||||
|
|
||||||
|
Ubuntu 20.04 example:
|
||||||
|
```wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb```
|
||||||
|
|
||||||
|
```sudo dpkg -i packages-microsoft-prod.deb```
|
||||||
|
|
||||||
|
```rm packages-microsoft-prod.deb```
|
||||||
|
|
||||||
|
```sudo apt-get update && sudo apt-get install -y dotnet-sdk-6.0```
|
||||||
|
|
||||||
|
2. Get latest archive from [releases](https://github.com/rogerfar/rdt-client/releases):
|
||||||
|
```wget <zip_url>```
|
||||||
|
3. Extract to path of your choice (~/rtdc in this example):
|
||||||
|
```unzip RealDebridClient.zip -d ~/rdtc && cd ~/rdtc```
|
||||||
|
4. In appsettings.json replace the Database Path to a path on your host. Any directories in path must already exist. Or just remove "/data/db/" for ease.
|
||||||
|
5. Test rdt client runs ok:
|
||||||
|
```dotnet RdtClient.Web.dll```
|
||||||
|
navigate to http://<ipaddress>:6500, if all is good then we'll create a service
|
||||||
|
6. Create a service (systemd in this example):
|
||||||
|
```sudo nano /etc/systemd/system/rdtc.service```
|
||||||
|
|
||||||
|
paste in this service file content and edit path of your directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=RdtClient Service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
|
||||||
|
WorkingDirectory=/home/<username>/rdtc
|
||||||
|
ExecStart=/usr/bin/dotnet RdtClient.Web.dll
|
||||||
|
SyslogIdentifier=RdtClient
|
||||||
|
User=<username>
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
7. enable and start the service:
|
||||||
|
```sudo systemctl daemon-reload```
|
||||||
|
```sudo systemctl enable rdtc```
|
||||||
|
```sudo systemctl start rdtc```
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### First Login
|
### First Login
|
||||||
|
|
@ -86,24 +142,27 @@ Instead of running in Docker you can install it as a service in Windows or Linux
|
||||||
|
|
||||||
Currently there 2 available download clients:
|
Currently there 2 available download clients:
|
||||||
|
|
||||||
#### Simple Downloader
|
#### Internal Downloader
|
||||||
|
|
||||||
This is a simple 1 connection only download manager. It uses less resources than the multi-part downloader. It downloads straight to the download path.
|
This [downloader](https://github.com/bezzad/Downloader) can be used to download files in parallel and with multiple chunks.
|
||||||
|
|
||||||
It has the following options:
|
It has the following options:
|
||||||
|
|
||||||
- Maximum parallel downloads: This number indicates how many completed torrents from Real Debrid can be downloaded at the same time. On low powered systems it is recommended to keep this number low.
|
- Download speed (in MB/s): This number indicates the speed in MB/s per download over all parallel downloads and chunks.
|
||||||
|
- Parallel connections per download: This number indicates how many parallel it will use per download. This can increase speed, recommended is no more than 8.
|
||||||
|
- Parallel chunks per download: This number indicates in how many chunks each download is split, recommended is no more than 8.
|
||||||
|
- Connection Timeout: This number indicates the timeout in milliseconds before a download chunk times out. It will retry each chunk 5 times before completely failing.
|
||||||
|
|
||||||
#### Multi Part Downloader
|
#### Aria2c downloader
|
||||||
|
|
||||||
This [downloader](https://github.com/bezzad/Downloader) as more options and such uses more resources (memory, CPU) to download files. Recommended more powerful systems.
|
This will use an external Aria2c downloader client. You will need to install this client yourself on your host, it is not included in the docker image.
|
||||||
|
|
||||||
It has the following options:
|
It has the following options:
|
||||||
|
|
||||||
- Temp Download path: Set this path to where the downloader temporarily stores chunks. This path can be an internal path in Docker (i.e. `/data/temp`) but make sure you have enough disk space to complete the whole download. When all chunks are completed the completed file is copied to your download folder.
|
- Url: The full URL to your Aria2c service. This must end in /jsonrpc. A standard path is `http://192.168.10.2:6800/jsonrpc`.
|
||||||
- Maximum parallel downloads: This number indicates how many completed torrents from Real Debrid can be downloaded at the same time.
|
- Secret: Optional secret to connecto to your Aria2c service.
|
||||||
- Parallel connections per download: This number indicates how many threads/connections/parts/chunks it will use per download. This can increase speed, recommended is no more than 8.
|
|
||||||
- Download speed (in MB/s): This number indicates the speed in MB/s per download. If you set this to 10 and `Maximum parallel downloads` to 2, you can download with a maximum of 20MB/s.
|
If Aria2c is selected, none of the above options for `Internal Downloader` are used, you have to configure Aria2c manually.
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
|
|
@ -136,8 +195,8 @@ Notice: the progress and ETA reported in Sonarr's Activity tab will not be accur
|
||||||
- NodeJS
|
- NodeJS
|
||||||
- NPM
|
- NPM
|
||||||
- Angular CLI
|
- Angular CLI
|
||||||
- .NET 5
|
- .NET 6
|
||||||
- Visual Studio 2019
|
- Visual Studio 2022
|
||||||
- (optional) Resharper
|
- (optional) Resharper
|
||||||
|
|
||||||
1. Open the client folder project in VS Code and run `npm install`.
|
1. Open the client folder project in VS Code and run `npm install`.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
"version": 1,
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
"rdt-client": {
|
"client": {
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
"style": "scss"
|
"style": "scss",
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:class": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:directive": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:guard": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:interceptor": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:pipe": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:service": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:application": {
|
||||||
|
"strict": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "",
|
"root": "",
|
||||||
|
|
@ -22,96 +44,76 @@
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"aot": true,
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": ["src/favicon.ico", "src/assets"],
|
"assets": ["src/favicon.ico", "src/assets"],
|
||||||
"styles": ["src/styles.scss"],
|
"styles": ["src/styles.scss"],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
"fileReplacements": [
|
"fileReplacements": [
|
||||||
{
|
{
|
||||||
"replace": "src/environments/environment.ts",
|
"replace": "src/environments/environment.ts",
|
||||||
"with": "src/environments/environment.prod.ts"
|
"with": "src/environments/environment.prod.ts"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"optimization": true,
|
"outputHashing": "all"
|
||||||
"outputHashing": "all",
|
},
|
||||||
"sourceMap": false,
|
"development": {
|
||||||
"namedChunks": false,
|
"buildOptimizer": false,
|
||||||
"extractLicenses": true,
|
"optimization": false,
|
||||||
"vendorChunk": false,
|
"vendorChunk": true,
|
||||||
"buildOptimizer": true,
|
"extractLicenses": false,
|
||||||
"budgets": [
|
"sourceMap": true,
|
||||||
{
|
"namedChunks": true
|
||||||
"type": "initial",
|
|
||||||
"maximumWarning": "2mb",
|
|
||||||
"maximumError": "5mb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "anyComponentStyle",
|
|
||||||
"maximumWarning": "6kb",
|
|
||||||
"maximumError": "10kb"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "rdt-client:build",
|
|
||||||
"proxyConfig": "angular-proxy.json"
|
"proxyConfig": "angular-proxy.json"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "rdt-client:build:production"
|
"browserTarget": "client:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"browserTarget": "client:build:development"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "rdt-client:build"
|
"browserTarget": "client:build"
|
||||||
}
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
|
||||||
"options": {
|
|
||||||
"main": "src/test.ts",
|
|
||||||
"polyfills": "src/polyfills.ts",
|
|
||||||
"tsConfig": "tsconfig.spec.json",
|
|
||||||
"karmaConfig": "karma.conf.js",
|
|
||||||
"assets": ["src/favicon.ico", "src/assets"],
|
|
||||||
"styles": ["src/styles.scss"],
|
|
||||||
"scripts": []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
"builder": "@angular-eslint/builder:lint",
|
||||||
"options": {
|
"options": {
|
||||||
"tsConfig": [
|
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||||
"tsconfig.app.json",
|
|
||||||
"tsconfig.spec.json",
|
|
||||||
"e2e/tsconfig.json"
|
|
||||||
],
|
|
||||||
"exclude": ["**/node_modules/**"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"e2e": {
|
|
||||||
"builder": "@angular-devkit/build-angular:protractor",
|
|
||||||
"options": {
|
|
||||||
"protractorConfig": "e2e/protractor.conf.js",
|
|
||||||
"devServerTarget": "rdt-client:serve"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"devServerTarget": "rdt-client:serve:production"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultProject": "rdt-client"
|
"cli": {
|
||||||
|
"analytics": false,
|
||||||
|
"schematicCollections": ["@angular-eslint/schematics"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"test": "ng test",
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"update": "ng update --force --allow-dirty @angular/cli @angular/core @angular/cdk @angular/flex-layout",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e",
|
"prettier": "prettier --write \"./**/*.{ts,html,json}\""
|
||||||
"update": "ng update --force --allow-dirty @angular/cli @angular/core"
|
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~11.2.0",
|
"@angular/animations": "^15.2.2",
|
||||||
"@angular/common": "~11.2.0",
|
"@angular/cdk": "^15.2.2",
|
||||||
"@angular/compiler": "~11.2.0",
|
"@angular/common": "^15.2.2",
|
||||||
"@angular/core": "~11.2.0",
|
"@angular/compiler": "^15.2.2",
|
||||||
"@angular/forms": "~11.2.0",
|
"@angular/core": "^15.2.2",
|
||||||
"@angular/platform-browser": "~11.2.0",
|
"@angular/flex-layout": "^15.0.0-beta.42",
|
||||||
"@angular/platform-browser-dynamic": "~11.2.0",
|
"@angular/forms": "^15.2.2",
|
||||||
"@angular/router": "~11.2.0",
|
"@angular/platform-browser": "^15.2.2",
|
||||||
"@fortawesome/fontawesome-free": "^5.15.2",
|
"@angular/platform-browser-dynamic": "^15.2.2",
|
||||||
"@microsoft/signalr": "^5.0.3",
|
"@angular/router": "^15.2.2",
|
||||||
"bulma": "^0.9.2",
|
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||||
"curray": "^1.0.8",
|
"@microsoft/signalr": "^7.0.3",
|
||||||
"ngx-filesize": "^2.0.16",
|
"bulma": "^0.9.4",
|
||||||
"rxjs": "~6.6.3",
|
"curray": "^1.0.11",
|
||||||
"tslib": "^2.1.0",
|
"file-saver-es": "^2.0.5",
|
||||||
"zone.js": "~0.11.3"
|
"ngx-filesize": "^3.0.1",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.5.0",
|
||||||
|
"zone.js": "~0.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.1102.0",
|
"@angular-devkit/build-angular": "^15.2.2",
|
||||||
"@angular/cli": "~11.2.0",
|
"@angular-eslint/builder": "15.2.1",
|
||||||
"@angular/compiler-cli": "~11.2.0",
|
"@angular-eslint/eslint-plugin": "15.2.1",
|
||||||
"@angular/language-service": "~11.2.0",
|
"@angular-eslint/eslint-plugin-template": "15.2.1",
|
||||||
"@types/node": "^14.14.27",
|
"@angular-eslint/schematics": "15.2.1",
|
||||||
"@types/jasmine": "~3.6.3",
|
"@angular-eslint/template-parser": "15.2.1",
|
||||||
"@types/jasminewd2": "~2.0.8",
|
"@angular/cli": "^15.2.2",
|
||||||
"codelyzer": "^6.0.1",
|
"@angular/compiler-cli": "^15.2.2",
|
||||||
"jasmine-core": "~3.6.0",
|
"@angular/language-service": "^15.2.2",
|
||||||
"jasmine-spec-reporter": "~6.0.0",
|
"@types/file-saver": "^2.0.5",
|
||||||
"karma": "~6.1.1",
|
"@types/file-saver-es": "^2.0.1",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"@types/node": "^18.14.6",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||||
"karma-jasmine": "~4.0.1",
|
"@typescript-eslint/parser": "5.54.1",
|
||||||
"karma-jasmine-html-reporter": "^1.5.4",
|
"eslint": "^8.35.0",
|
||||||
"protractor": "~7.0.0",
|
"prettier": "^2.8.4",
|
||||||
"ts-node": "~9.1.1",
|
"typescript": "~4.9.5"
|
||||||
"tslint": "~6.1.0",
|
|
||||||
"typescript": "~4.1.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
|
||||||
import { AuthResolverService } from './auth-resolver.service';
|
import { AuthResolverService } from './auth-resolver.service';
|
||||||
import { LoginComponent } from './login/login.component';
|
import { LoginComponent } from './login/login.component';
|
||||||
import { MainLayoutComponent } from './main-layout/main-layout.component';
|
import { MainLayoutComponent } from './main-layout/main-layout.component';
|
||||||
|
import { ProfileComponent } from './profile/profile.component';
|
||||||
|
import { SettingsComponent } from './settings/settings.component';
|
||||||
import { SetupComponent } from './setup/setup.component';
|
import { SetupComponent } from './setup/setup.component';
|
||||||
|
import { TorrentTableComponent } from './torrent-table/torrent-table.component';
|
||||||
|
import { TorrentComponent } from './torrent/torrent.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
|
@ -20,6 +25,28 @@ const routes: Routes = [
|
||||||
resolve: {
|
resolve: {
|
||||||
isLoggedIn: AuthResolverService,
|
isLoggedIn: AuthResolverService,
|
||||||
},
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: TorrentTableComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'torrent/:id',
|
||||||
|
component: TorrentComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'add',
|
||||||
|
component: AddNewTorrentComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
component: SettingsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'profile',
|
||||||
|
component: ProfileComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,28 @@
|
||||||
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { curray } from 'curray';
|
import { curray } from 'curray';
|
||||||
import { FileSizePipe, NgxFilesizeModule } from 'ngx-filesize';
|
import { FileSizePipe, NgxFilesizeModule } from 'ngx-filesize';
|
||||||
|
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { AuthInterceptor } from './auth.interceptor';
|
import { AuthInterceptor } from './auth.interceptor';
|
||||||
|
import { DecodeURIPipe } from './decode-uri.pipe';
|
||||||
import { DownloadStatusPipe } from './download-status.pipe';
|
import { DownloadStatusPipe } from './download-status.pipe';
|
||||||
import { LoginComponent } from './login/login.component';
|
import { LoginComponent } from './login/login.component';
|
||||||
import { MainLayoutComponent } from './main-layout/main-layout.component';
|
import { MainLayoutComponent } from './main-layout/main-layout.component';
|
||||||
import { AddNewTorrentComponent } from './navbar/add-new-torrent/add-new-torrent.component';
|
|
||||||
import { NavbarComponent } from './navbar/navbar.component';
|
import { NavbarComponent } from './navbar/navbar.component';
|
||||||
import { SettingsComponent } from './navbar/settings/settings.component';
|
import { Nl2BrPipe } from './nl2br.pipe';
|
||||||
|
import { ProfileComponent } from './profile/profile.component';
|
||||||
|
import { SettingsComponent } from './settings/settings.component';
|
||||||
import { SetupComponent } from './setup/setup.component';
|
import { SetupComponent } from './setup/setup.component';
|
||||||
import { TorrentDownloadComponent } from './torrent-download/torrent-download.component';
|
|
||||||
import { TorrentFileComponent } from './torrent-file/torrent-file.component';
|
|
||||||
import { TorrentRowComponent } from './torrent-row/torrent-row.component';
|
|
||||||
import { TorrentStatusPipe } from './torrent-status.pipe';
|
import { TorrentStatusPipe } from './torrent-status.pipe';
|
||||||
import { TorrentTableComponent } from './torrent-table/torrent-table.component';
|
import { TorrentTableComponent } from './torrent-table/torrent-table.component';
|
||||||
|
import { TorrentComponent } from './torrent/torrent.component';
|
||||||
|
|
||||||
curray();
|
curray();
|
||||||
|
|
||||||
|
|
@ -29,17 +33,30 @@ curray();
|
||||||
NavbarComponent,
|
NavbarComponent,
|
||||||
AddNewTorrentComponent,
|
AddNewTorrentComponent,
|
||||||
TorrentTableComponent,
|
TorrentTableComponent,
|
||||||
TorrentRowComponent,
|
|
||||||
TorrentFileComponent,
|
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
TorrentStatusPipe,
|
TorrentStatusPipe,
|
||||||
DownloadStatusPipe,
|
DownloadStatusPipe,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
SetupComponent,
|
SetupComponent,
|
||||||
TorrentDownloadComponent,
|
TorrentComponent,
|
||||||
|
DecodeURIPipe,
|
||||||
|
ProfileComponent,
|
||||||
|
Nl2BrPipe,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
FormsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
NgxFilesizeModule,
|
||||||
|
FlexLayoutModule,
|
||||||
|
ClipboardModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
FileSizePipe,
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||||
|
{ provide: APP_BASE_HREF, useValue: (window as any)['_app_base'] || '/' },
|
||||||
],
|
],
|
||||||
imports: [BrowserModule, AppRoutingModule, FormsModule, HttpClientModule, NgxFilesizeModule],
|
|
||||||
providers: [FileSizePipe, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }],
|
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import { Observable } from 'rxjs/internal/Observable';
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
public isLoggedIn(): Observable<void> {
|
public isLoggedIn(): Observable<boolean> {
|
||||||
return this.http.get<void>(`/Api/Authentication/IsLoggedIn`);
|
return this.http.get<boolean>(`/Api/Authentication/IsLoggedIn`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public create(userName: string, password: string): Observable<void> {
|
public create(userName: string, password: string): Observable<void> {
|
||||||
|
|
@ -19,6 +19,13 @@ export class AuthService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setupProvider(provider: string, token: string): Observable<void> {
|
||||||
|
return this.http.post<void>(`/Api/Authentication/SetupProvider`, {
|
||||||
|
provider,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public login(userName: string, password: string): Observable<void> {
|
public login(userName: string, password: string): Observable<void> {
|
||||||
return this.http.post<void>(`/Api/Authentication/Login`, {
|
return this.http.post<void>(`/Api/Authentication/Login`, {
|
||||||
userName,
|
userName,
|
||||||
|
|
@ -29,4 +36,11 @@ export class AuthService {
|
||||||
public logout() {
|
public logout() {
|
||||||
return this.http.post<void>(`/Api/Authentication/Logout`, {});
|
return this.http.post<void>(`/Api/Authentication/Logout`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public update(userName: string, password: string): Observable<void> {
|
||||||
|
return this.http.post<void>(`/Api/Authentication/Update`, {
|
||||||
|
userName,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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) {
|
if (value.error) {
|
||||||
return `Error: ${value.error}`;
|
return value.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.completed != null) {
|
if (value.completed != null) {
|
||||||
|
|
@ -26,8 +26,13 @@ export class DownloadStatusPipe implements PipeTransform {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.unpackingStarted) {
|
if (value.unpackingStarted) {
|
||||||
const progress = ((value.bytesDone / value.bytesTotal) * 100).toFixed(2);
|
let progress = (value.bytesDone / value.bytesTotal) * 100;
|
||||||
return `Unpacking ${progress || 0}%`;
|
|
||||||
|
if (isNaN(progress)) {
|
||||||
|
progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Unpacking ${progress.toFixed(2)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.unpackingQueued) {
|
if (value.unpackingQueued) {
|
||||||
|
|
@ -39,10 +44,15 @@ export class DownloadStatusPipe implements PipeTransform {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.downloadStarted) {
|
if (value.downloadStarted) {
|
||||||
const progress = ((value.bytesDone / value.bytesTotal) * 100).toFixed(2);
|
let progress = (value.bytesDone / value.bytesTotal) * 100;
|
||||||
|
|
||||||
|
if (isNaN(progress)) {
|
||||||
|
progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
const speed = this.pipe.transform(value.speed, 'filesize');
|
const speed = this.pipe.transform(value.speed, 'filesize');
|
||||||
|
|
||||||
return `Downloading ${progress || 0}% (${speed}/s)`;
|
return `Downloading ${progress.toFixed(2)}% (${speed}/s)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.downloadQueued) {
|
if (value.downloadQueued) {
|
||||||
|
|
|
||||||
|
|
@ -5,37 +5,46 @@
|
||||||
<div class="column is-5-tablet is-5-desktop is-5-widescreen">
|
<div class="column is-5-tablet is-5-desktop is-5-widescreen">
|
||||||
<img src="../../assets/logo.png" />
|
<img src="../../assets/logo.png" />
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="field">
|
<form (ngSubmit)="login()">
|
||||||
<label for="" class="label">Username</label>
|
<div class="field">
|
||||||
<div class="control has-icons-left">
|
<label for="" class="label">Username</label>
|
||||||
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required />
|
<div class="control has-icons-left">
|
||||||
<span class="icon is-small is-left">
|
<input
|
||||||
<i class="fa fa-envelope"></i>
|
type="text"
|
||||||
</span>
|
placeholder=""
|
||||||
|
class="input"
|
||||||
|
name="userName"
|
||||||
|
(change)="setUserName($event)"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<i class="fa fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="field">
|
||||||
<div class="field">
|
<label for="" class="label" name="password">Password</label>
|
||||||
<label for="" class="label" name="password">Password</label>
|
<div class="control has-icons-left">
|
||||||
<div class="control has-icons-left">
|
<input type="password" class="input" required name="password" (change)="setPassword($event)" />
|
||||||
<input type="password" class="input" required name="password" [(ngModel)]="password" />
|
<span class="icon is-small is-left">
|
||||||
<span class="icon is-small is-left">
|
<i class="fa fa-lock"></i>
|
||||||
<i class="fa fa-lock"></i>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="field">
|
||||||
<div class="field">
|
<button
|
||||||
<button
|
class="button is-success"
|
||||||
class="button is-success"
|
type="submit"
|
||||||
(click)="login()"
|
[ngClass]="{ 'is-loading': loggingIn }"
|
||||||
[ngClass]="{ 'is-loading': loggingIn }"
|
[disabled]="loggingIn"
|
||||||
[disabled]="loggingIn"
|
>
|
||||||
>
|
Login
|
||||||
Login
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
||||||
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
{{ error }}
|
||||||
{{ error }}
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { AuthService } from '../auth.service';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrls: ['./login.component.scss'],
|
styleUrls: ['./login.component.scss'],
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit {
|
export class LoginComponent {
|
||||||
public userName: string;
|
public userName: string;
|
||||||
public password: string;
|
public password: string;
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
@ -15,7 +15,13 @@ export class LoginComponent implements OnInit {
|
||||||
|
|
||||||
constructor(private authService: AuthService, private router: Router) {}
|
constructor(private authService: AuthService, private router: Router) {}
|
||||||
|
|
||||||
ngOnInit(): void {}
|
public setUserName(event: Event): void {
|
||||||
|
this.userName = (event.target as any).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPassword(event: Event): void {
|
||||||
|
this.password = (event.target as any).value;
|
||||||
|
}
|
||||||
|
|
||||||
public login(): void {
|
public login(): void {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<app-navbar></app-navbar>
|
<app-navbar></app-navbar>
|
||||||
<section class="section main-list">
|
<section class="section main-list">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<app-torrent-table></app-torrent-table>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-main-layout',
|
selector: 'app-main-layout',
|
||||||
templateUrl: './main-layout.component.html',
|
templateUrl: './main-layout.component.html',
|
||||||
styleUrls: ['./main-layout.component.scss'],
|
styleUrls: ['./main-layout.component.scss'],
|
||||||
})
|
})
|
||||||
export class MainLayoutComponent implements OnInit {
|
export class MainLayoutComponent {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,5 @@ export class Download {
|
||||||
public bytesTotal: number;
|
public bytesTotal: number;
|
||||||
public bytesDone: number;
|
public bytesDone: number;
|
||||||
public speed: number;
|
public speed: number;
|
||||||
|
public retryCount: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
export class Profile {
|
export class Profile {
|
||||||
|
public provider: string;
|
||||||
public userName: string;
|
public userName: string;
|
||||||
public expiration: Date;
|
public expiration: Date;
|
||||||
|
public currentVersion: string;
|
||||||
|
public latestVersion: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
export class Setting {
|
export class Setting {
|
||||||
public settingId: string;
|
key: string;
|
||||||
public value: string;
|
value: boolean | number | null | string;
|
||||||
|
displayName: null | string;
|
||||||
|
description: null | string;
|
||||||
|
type: string;
|
||||||
|
settings: Setting[];
|
||||||
|
enumValues: { [key: string]: string };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,27 @@ export class Torrent {
|
||||||
public torrentId: string;
|
public torrentId: string;
|
||||||
public hash: string;
|
public hash: string;
|
||||||
public category: string;
|
public category: string;
|
||||||
|
public hostDownloadAction: number;
|
||||||
|
public downloadAction: number;
|
||||||
|
public finishedAction: number;
|
||||||
|
public downloadMinSize: number;
|
||||||
|
public downloadManualFiles: string;
|
||||||
|
|
||||||
public added: Date;
|
public added: Date;
|
||||||
|
public filesSelected: Date;
|
||||||
public completed: Date;
|
public completed: Date;
|
||||||
public autoDelete: boolean;
|
|
||||||
|
public fileOrMagnet: string;
|
||||||
|
public isFile: boolean;
|
||||||
|
|
||||||
|
public retryCount: number;
|
||||||
|
public downloadRetryAttempts: number;
|
||||||
|
public torrentRetryAttempts: number;
|
||||||
|
public deleteOnError: number;
|
||||||
|
public lifetime: number;
|
||||||
|
|
||||||
|
public priority: number;
|
||||||
|
public error: string;
|
||||||
|
|
||||||
public rdId: string;
|
public rdId: string;
|
||||||
public rdName: string;
|
public rdName: string;
|
||||||
|
|
@ -35,11 +53,17 @@ export class TorrentFile {
|
||||||
public download: Download;
|
public download: Download;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TorrentFileAvailability {
|
||||||
|
public filename: string;
|
||||||
|
public filesize: number;
|
||||||
|
}
|
||||||
|
|
||||||
export enum RealDebridStatus {
|
export enum RealDebridStatus {
|
||||||
Processing = 0,
|
Processing = 0,
|
||||||
WaitingForFileSelection = 1,
|
WaitingForFileSelection = 1,
|
||||||
Downloading = 2,
|
Downloading = 2,
|
||||||
Finished = 3,
|
Finished = 3,
|
||||||
|
Uploading = 4,
|
||||||
|
|
||||||
Error = 99,
|
Error = 99,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
<nav class="navbar is-dark is-fixed-top" role="navigation" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item">
|
<a class="navbar-item" routerLink="/">
|
||||||
<img src="../../assets/logo.png" />
|
<img src="../../assets/logo.png" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
@ -20,19 +20,25 @@
|
||||||
|
|
||||||
<div class="navbar-menu" [class.is-active]="showMobileMenu">
|
<div class="navbar-menu" [class.is-active]="showMobileMenu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" (click)="this.showNewTorrent = true">
|
<a class="navbar-item" routerLink="/">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-table" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<span>Torrents</span>
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item" routerLink="/add">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>Add New Torrent</span>
|
<span>Add New Torrent</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="navbar-item" (click)="this.showSettings = true">
|
<a class="navbar-item" routerLink="/settings">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-cog" aria-hidden="true"></i>
|
<i class="fas fa-cog" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="navbar-item" *ngIf="profile" href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener">
|
<a class="navbar-item" *ngIf="profile" href="{{ providerLink }}" target="_blank" rel="noopener">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-euro-sign" aria-hidden="true"></i>
|
<i class="fas fa-euro-sign" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -46,15 +52,19 @@
|
||||||
<span class="navbar-link"> Account </span>
|
<span class="navbar-link"> Account </span>
|
||||||
|
|
||||||
<div class="navbar-dropdown is-right">
|
<div class="navbar-dropdown is-right">
|
||||||
<a class="navbar-item"> Profile </a>
|
<a class="navbar-item" routerLink="profile"> Profile </a>
|
||||||
<a class="navbar-item" (click)="logout()"> Logout </a>
|
<a class="navbar-item" (click)="logout()"> Logout </a>
|
||||||
<hr class="navbar-divider" />
|
<hr class="navbar-divider" />
|
||||||
<div class="navbar-item">Version 1.7.4</div>
|
<div class="navbar-item">Version 2.0.22</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div
|
||||||
<app-add-new-torrent [(open)]="showNewTorrent"></app-add-new-torrent>
|
class="notification is-warning"
|
||||||
<app-settings [(open)]="showSettings"></app-settings>
|
*ngIf="profile && profile.latestVersion && profile.currentVersion !== profile.latestVersion"
|
||||||
|
>
|
||||||
|
Version {{ profile.latestVersion }} of RealDebrid Client was found. You are currently on version
|
||||||
|
{{ profile.currentVersion }}.
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
.no-premium {
|
.no-premium {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
margin-top: 52px;
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { TorrentService } from '../torrent.service';
|
|
||||||
import { SettingsService } from '../settings.service';
|
|
||||||
import { Profile } from '../models/profile.model';
|
|
||||||
import { AuthService } from '../auth.service';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../auth.service';
|
||||||
|
import { Profile } from '../models/profile.model';
|
||||||
|
import { SettingsService } from '../settings.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-navbar',
|
||||||
|
|
@ -13,16 +12,26 @@ import { Router } from '@angular/router';
|
||||||
export class NavbarComponent implements OnInit {
|
export class NavbarComponent implements OnInit {
|
||||||
public showMobileMenu = false;
|
public showMobileMenu = false;
|
||||||
|
|
||||||
public showNewTorrent = false;
|
|
||||||
public showSettings = false;
|
|
||||||
|
|
||||||
public profile: Profile;
|
public profile: Profile;
|
||||||
|
public providerLink: string;
|
||||||
|
|
||||||
constructor(private settingsService: SettingsService, private authService: AuthService, private router: Router) {}
|
constructor(private settingsService: SettingsService, private authService: AuthService, private router: Router) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.settingsService.getProfile().subscribe((result) => {
|
this.settingsService.getProfile().subscribe((result) => {
|
||||||
this.profile = result;
|
this.profile = result;
|
||||||
|
|
||||||
|
switch (result.provider) {
|
||||||
|
case 'RealDebrid':
|
||||||
|
this.providerLink = 'https://real-debrid.com/?id=1348683';
|
||||||
|
break;
|
||||||
|
case 'AllDebrid':
|
||||||
|
this.providerLink = 'https://alldebrid.com/?uid=2v91l&lang=en';
|
||||||
|
break;
|
||||||
|
case 'Premiumize':
|
||||||
|
this.providerLink = 'https://www.premiumize.me/';
|
||||||
|
break;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
public update(settings: Setting[]): Observable<void> {
|
||||||
return this.http.put<void>(`/Api/Settings`, { settings });
|
return this.http.put<void>(`/Api/Settings`, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getProfile(): Observable<Profile> {
|
public getProfile(): Observable<Profile> {
|
||||||
|
|
@ -33,4 +33,11 @@ export class SettingsService {
|
||||||
public testWriteSpeed(): Observable<number> {
|
public testWriteSpeed(): Observable<number> {
|
||||||
return this.http.get<number>(`/Api/Settings/TestWriteSpeed`);
|
return this.http.get<number>(`/Api/Settings/TestWriteSpeed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public testAria2cConnection(url: string, secret: string): Observable<{ version: string }> {
|
||||||
|
return this.http.post<{ version: string }>(`/Api/Settings/TestAria2cConnection`, {
|
||||||
|
url,
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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" />
|
<img src="../../assets/logo.png" />
|
||||||
<div class="box" *ngIf="step === 1">
|
<div class="box" *ngIf="step === 1">
|
||||||
<div class="notification is-primary">
|
<div class="notification is-primary">
|
||||||
Welcome to RealDebrid Client. Please create your account by entering a username and password.
|
Welcome to Real-Debrid Client. Please create your account by entering a username and password.
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<form (ngSubmit)="setup()">
|
||||||
<label for="" class="label">Username</label>
|
<div class="field">
|
||||||
<div class="control has-icons-left">
|
<label for="" class="label">Username</label>
|
||||||
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required />
|
<div class="control has-icons-left">
|
||||||
<span class="icon is-small is-left">
|
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required />
|
||||||
<i class="fa fa-envelope"></i>
|
<span class="icon is-small is-left">
|
||||||
</span>
|
<i class="fa fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="field">
|
||||||
<div class="field">
|
<label for="" class="label" name="password">Password</label>
|
||||||
<label for="" class="label" name="password">Password</label>
|
<div class="control has-icons-left">
|
||||||
<div class="control has-icons-left">
|
<input type="password" class="input" required name="password" [(ngModel)]="password" />
|
||||||
<input type="password" class="input" required name="password" [(ngModel)]="password" />
|
<span class="icon is-small is-left">
|
||||||
<span class="icon is-small is-left">
|
<i class="fa fa-lock"></i>
|
||||||
<i class="fa fa-lock"></i>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="field">
|
||||||
<div class="field">
|
<button class="button is-success" [ngClass]="{ 'is-loading': working }" [disabled]="working">
|
||||||
<button
|
Setup
|
||||||
class="button is-success"
|
</button>
|
||||||
(click)="setup()"
|
</div>
|
||||||
[ngClass]="{ 'is-loading': working }"
|
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
||||||
[disabled]="working"
|
{{ error }}
|
||||||
>
|
</div>
|
||||||
Setup
|
</form>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" *ngIf="step === 2">
|
<div class="box" *ngIf="step === 2">
|
||||||
<div class="notification is-primary">
|
<div class="notification is-primary">
|
||||||
To be able to use the RealDebrid Client you need a premium subscription to download torrents.
|
To be able to use the Real-Debrid Client you need a premium subscription to download torrents.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
Not premium yet?
|
Not premium yet? You have the choice of 2 providers:
|
||||||
|
<br />
|
||||||
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener"
|
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener"
|
||||||
>Use this link to sign up to RealDebrid.</a
|
>Use this link to sign up to Real-Debrid.</a
|
||||||
>
|
>
|
||||||
<br /><br />
|
<br />
|
||||||
To connect to RealDebrid please enter your Personal Prive API Token found here:
|
<a href="https://alldebrid.com/?uid=2v91l&lang=en" target="_blank" rel="noopener"
|
||||||
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener"
|
>Use this link to sign up to AllDebrid.</a
|
||||||
>https://real-debrid.com/apitoken</a
|
>
|
||||||
>.
|
<br />
|
||||||
|
<a href="https://www.premiumize.me/" target="_blank" rel="noopener"
|
||||||
|
>Use this link to sign up to Premiumize.</a
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
<small>(Referal links)</small>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Provider</label>
|
||||||
|
<div class="control select is-fullwidth">
|
||||||
|
<select [(ngModel)]="provider">
|
||||||
|
<option [value]="0">Real-Debrid</option>
|
||||||
|
<option [value]="1">AllDebrid</option>
|
||||||
|
<option [value]="2">Premiumize</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="" class="label">API Private Token</label>
|
<label for="" class="label">API Private Token</label>
|
||||||
<div class="control has-icons-left">
|
<div class="control">
|
||||||
<input type="text" placeholder="" class="input" name="token" [(ngModel)]="token" required />
|
<input type="text" placeholder="" class="input" name="token" [(ngModel)]="token" required />
|
||||||
<span class="icon is-small is-left">
|
|
||||||
<i class="fa fa-envelope"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help" *ngIf="provider === 'RealDebrid'">
|
||||||
|
You can find your API key here:
|
||||||
|
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener"
|
||||||
|
>https://real-debrid.com/apitoken</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p class="help" *ngIf="provider === 'AllDebrid'">
|
||||||
|
You can find your API key here:
|
||||||
|
<a href="https://alldebrid.com/apikeys/" target="_blank" rel="noopener"
|
||||||
|
>https://alldebrid.com/apikeys/</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p class="help" *ngIf="provider === 'Premiumize'">
|
||||||
|
You can find your API key here:
|
||||||
|
<a href="https://www.premiumize.me/account" target="_blank" rel="noopener"
|
||||||
|
>https://www.premiumize.me/account</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<button
|
<button
|
||||||
class="button is-success"
|
class="button is-success"
|
||||||
|
|
@ -81,7 +110,7 @@
|
||||||
|
|
||||||
<div class="box" *ngIf="step === 3">
|
<div class="box" *ngIf="step === 3">
|
||||||
<div class="notification is-primary">
|
<div class="notification is-primary">
|
||||||
You are now connected to RealDebrid! To setup Radarr or Sonarr, please following the instructions here:
|
You are now connected to Real-Debrid! To setup Radarr or Sonarr, please following the instructions here:
|
||||||
<a href="https://github.com/rogerfar/rdt-client/" target="_blank" rel="noopener"
|
<a href="https://github.com/rogerfar/rdt-client/" target="_blank" rel="noopener"
|
||||||
>https://github.com/rogerfar/rdt-client/</a
|
>https://github.com/rogerfar/rdt-client/</a
|
||||||
>.
|
>.
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
import { Setting } from '../models/setting.model';
|
|
||||||
import { SettingsService } from '../settings.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-setup',
|
selector: 'app-setup',
|
||||||
templateUrl: './setup.component.html',
|
templateUrl: './setup.component.html',
|
||||||
styleUrls: ['./setup.component.scss'],
|
styleUrls: ['./setup.component.scss'],
|
||||||
})
|
})
|
||||||
export class SetupComponent implements OnInit {
|
export class SetupComponent {
|
||||||
public userName: string;
|
public userName: string;
|
||||||
public password: string;
|
public password: string;
|
||||||
|
public provider = 'RealDebrid';
|
||||||
public token: string;
|
public token: string;
|
||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
@ -19,9 +18,7 @@ export class SetupComponent implements OnInit {
|
||||||
|
|
||||||
public step: number = 1;
|
public step: number = 1;
|
||||||
|
|
||||||
constructor(private authService: AuthService, private settingsService: SettingsService, private router: Router) {}
|
constructor(private authService: AuthService, private router: Router) {}
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
|
|
||||||
public setup(): void {
|
public setup(): void {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
@ -40,16 +37,12 @@ export class SetupComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setToken(): void {
|
public setToken(): void {
|
||||||
const setting = new Setting();
|
this.authService.setupProvider(this.provider, this.token).subscribe(
|
||||||
setting.settingId = 'RealDebridApiKey';
|
|
||||||
setting.value = this.token;
|
|
||||||
|
|
||||||
this.settingsService.update([setting]).subscribe(
|
|
||||||
() => {
|
() => {
|
||||||
this.step = 3;
|
this.step = 3;
|
||||||
this.working = false;
|
this.working = false;
|
||||||
},
|
},
|
||||||
(err) => {
|
(err: any) => {
|
||||||
this.working = false;
|
this.working = false;
|
||||||
this.error = err.error;
|
this.error = err.error;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {}
|
constructor(private pipe: FileSizePipe) {}
|
||||||
|
|
||||||
transform(torrent: Torrent): string {
|
transform(torrent: Torrent): string {
|
||||||
|
if (torrent.error) {
|
||||||
|
return torrent.error;
|
||||||
|
}
|
||||||
|
|
||||||
if (torrent.downloads.length > 0) {
|
if (torrent.downloads.length > 0) {
|
||||||
const errors = torrent.downloads.where((m) => m.error != null);
|
const errors = torrent.downloads.where((m) => m.error != null);
|
||||||
|
|
||||||
|
|
@ -22,33 +26,44 @@ export class TorrentStatusPipe implements PipeTransform {
|
||||||
return 'Finished';
|
return 'Finished';
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished);
|
const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished && m.bytesDone > 0);
|
||||||
|
const downloaded = torrent.downloads.where((m) => m.downloadFinished != null);
|
||||||
|
|
||||||
if (downloading.length > 0) {
|
if (downloading.length > 0) {
|
||||||
const bytesDone = downloading.sum((m) => m.bytesDone);
|
const bytesDone = downloading.sum((m) => m.bytesDone);
|
||||||
const bytesTotal = downloading.sum((m) => m.bytesTotal);
|
const bytesTotal = downloading.sum((m) => m.bytesTotal);
|
||||||
let progress = (bytesDone / bytesTotal) * 100;
|
let progress = (bytesDone / bytesTotal) * 100;
|
||||||
let allSpeeds = downloading.sum((m) => m.speed) / downloading.length;
|
|
||||||
|
if (isNaN(progress)) {
|
||||||
|
progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let allSpeeds = downloading.sum((m) => m.speed);
|
||||||
|
|
||||||
let speed: string | string[] = '0';
|
let speed: string | string[] = '0';
|
||||||
if (allSpeeds > 0) {
|
|
||||||
speed = this.pipe.transform(allSpeeds, 'filesize');
|
|
||||||
|
|
||||||
return `Downloading (${progress.toFixed(2)}% - ${speed}/s)`;
|
speed = this.pipe.transform(allSpeeds, 'filesize');
|
||||||
}
|
|
||||||
|
return `Downloading file ${downloading.length + downloaded.length}/${
|
||||||
|
torrent.downloads.length
|
||||||
|
} (${progress.toFixed(2)}% - ${speed}/s)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished);
|
const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished && m.bytesDone > 0);
|
||||||
|
const unpacked = torrent.downloads.where((m) => m.unpackingFinished != null);
|
||||||
|
|
||||||
if (unpacking.length > 0) {
|
if (unpacking.length > 0) {
|
||||||
const bytesDone = unpacking.sum((m) => m.bytesDone);
|
const bytesDone = unpacking.sum((m) => m.bytesDone);
|
||||||
const bytesTotal = unpacking.sum((m) => m.bytesTotal);
|
const bytesTotal = unpacking.sum((m) => m.bytesTotal);
|
||||||
let progress = (bytesDone / bytesTotal) * 100;
|
let progress = (bytesDone / bytesTotal) * 100;
|
||||||
let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length;
|
|
||||||
|
|
||||||
if (allSpeeds > 0) {
|
if (isNaN(progress)) {
|
||||||
return `Extracting (${progress.toFixed(2)}%)`;
|
progress = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return `Extracting file ${unpacking.length + unpacked.length}/${torrent.downloads.length} (${progress.toFixed(
|
||||||
|
2
|
||||||
|
)}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queuedForUnpacking = torrent.downloads.where((m) => m.unpackingQueued && !m.unpackingStarted);
|
const queuedForUnpacking = torrent.downloads.where((m) => m.unpackingQueued && !m.unpackingStarted);
|
||||||
|
|
@ -57,20 +72,16 @@ export class TorrentStatusPipe implements PipeTransform {
|
||||||
return `Queued for unpacking`;
|
return `Queued for unpacking`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queuedForDownload = torrent.downloads.where((m) => m.downloadQueued && !m.downloadStarted);
|
const queuedForDownload = torrent.downloads.where((m) => !m.downloadStarted && !m.downloadFinished);
|
||||||
|
|
||||||
if (queuedForDownload.length > 0) {
|
if (queuedForDownload.length > 0) {
|
||||||
return `Queued for downloading`;
|
return `Queued for downloading`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpacked = torrent.downloads.where((m) => m.unpackingFinished != null);
|
|
||||||
|
|
||||||
if (unpacked.length > 0) {
|
if (unpacked.length > 0) {
|
||||||
return `Files unpacked`;
|
return `Files unpacked`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloaded = torrent.downloads.where((m) => m.downloadFinished != null);
|
|
||||||
|
|
||||||
if (downloaded.length > 0) {
|
if (downloaded.length > 0) {
|
||||||
return `Files downloaded to host`;
|
return `Files downloaded to host`;
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +102,9 @@ export class TorrentStatusPipe implements PipeTransform {
|
||||||
case RealDebridStatus.Error:
|
case RealDebridStatus.Error:
|
||||||
return `Torrent error: ${torrent.rdStatusRaw}`;
|
return `Torrent error: ${torrent.rdStatusRaw}`;
|
||||||
case RealDebridStatus.Finished:
|
case RealDebridStatus.Finished:
|
||||||
return `Torrent finished, waiting to download`;
|
return `Torrent finished, waiting for download links`;
|
||||||
|
case RealDebridStatus.Uploading:
|
||||||
|
return `Torrent uploading`;
|
||||||
default:
|
default:
|
||||||
return 'Unknown status';
|
return 'Unknown status';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,58 +6,86 @@
|
||||||
<table class="table is-fullwidth is-hoverable">
|
<table class="table is-fullwidth is-hoverable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
(click)="toggleSelectAll($event)"
|
||||||
|
[checked]="selectedTorrents.length > 0 && selectedTorrents.length === torrents.length"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Priority</th>
|
||||||
<th>Files</th>
|
<th>Files</th>
|
||||||
<th>Downloads</th>
|
<th>Downloads</th>
|
||||||
<th>Auto</th>
|
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<ng-container *ngFor="let torrent of torrents; trackBy: trackByMethod">
|
<tr *ngFor="let torrent of torrents; trackBy: trackByMethod">
|
||||||
<tr
|
<td>
|
||||||
app-torrent-row
|
<input
|
||||||
[torrent]="torrent"
|
type="checkbox"
|
||||||
(click)="selectTorrent(torrent)"
|
(click)="toggleSelect(torrent.torrentId)"
|
||||||
(delete)="showDeleteModal($event)"
|
[checked]="selectedTorrents.contains(torrent.torrentId)"
|
||||||
(retry)="showRetryModal($event)"
|
/>
|
||||||
></tr>
|
</td>
|
||||||
<ng-container *ngIf="showFiles[torrent.torrentId]">
|
<td (click)="openTorrent(torrent.torrentId)">
|
||||||
<tr class="separator">
|
{{ torrent.rdName }}
|
||||||
<td colspan="10">Downloads</td>
|
</td>
|
||||||
</tr>
|
<td>
|
||||||
<tr app-torrent-download [download]="download" *ngFor="let download of torrent.downloads"></tr>
|
{{ torrent.category }}
|
||||||
<tr class="separator">
|
</td>
|
||||||
<td colspan="10">Files in torrent</td>
|
<td>
|
||||||
</tr>
|
{{ torrent.priority }}
|
||||||
<tr app-torrent-file [file]="file" *ngFor="let file of torrent.files"></tr>
|
</td>
|
||||||
</ng-container>
|
<td>
|
||||||
</ng-container>
|
{{ torrent.files.length | number }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ torrent.downloads.length | number }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ torrent.rdSize | filesize }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ torrent | status }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="button is-danger"
|
||||||
|
(click)="showDeleteModal()"
|
||||||
|
[disabled]="selectedTorrents.length === 0"
|
||||||
|
*ngIf="torrents.length > 0"
|
||||||
|
>
|
||||||
|
Delete Selected
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal" [class.is-active]="isDeleteModalActive">
|
<div class="modal" [class.is-active]="isDeleteModalActive">
|
||||||
<div class="modal-background"></div>
|
<div class="modal-background"></div>
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
<p class="modal-card-title">Delete torrent</p>
|
<p class="modal-card-title">Delete selected torrents</p>
|
||||||
<button class="delete" aria-label="close" (click)="deleteCancel()"></button>
|
<button class="delete" aria-label="close" (click)="deleteCancel()"></button>
|
||||||
</header>
|
</header>
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
|
<p>Are you sure you want to delete these torrent?</p>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label"></label>
|
<label class="label"></label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="deleteData" />
|
<input type="checkbox" [(ngModel)]="deleteData" />
|
||||||
Delete Torrent from local RealDebrid Client
|
Delete Torrents from client
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
|
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
|
||||||
Delete Torrent from RealDebrid
|
Delete Torrents from provider
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
|
|
@ -67,7 +95,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="notification is-primary">
|
<div class="notification is-primary">
|
||||||
Deleting a torrent from RealDebrid will automatically delete it here too.
|
Deleting a torrent from Real-Debrid will automatically delete it here too.
|
||||||
</div>
|
</div>
|
||||||
<div class="notification is-danger is-light" *ngIf="deleteError?.length > 0">
|
<div class="notification is-danger is-light" *ngIf="deleteError?.length > 0">
|
||||||
Error deleting torrent: {{ deleteError }}
|
Error deleting torrent: {{ deleteError }}
|
||||||
|
|
@ -80,7 +108,7 @@
|
||||||
[disabled]="deleting"
|
[disabled]="deleting"
|
||||||
[ngClass]="{ 'is-loading': deleting }"
|
[ngClass]="{ 'is-loading': deleting }"
|
||||||
>
|
>
|
||||||
Delete
|
Delete selected
|
||||||
</button>
|
</button>
|
||||||
<button class="button" (click)="deleteCancel()" [disabled]="deleting" [ngClass]="{ 'is-loading': deleting }">
|
<button class="button" (click)="deleteCancel()" [disabled]="deleting" [ngClass]="{ 'is-loading': deleting }">
|
||||||
Cancel
|
Cancel
|
||||||
|
|
@ -88,45 +116,3 @@
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal" [class.is-active]="isRetryModalActive">
|
|
||||||
<div class="modal-background"></div>
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<p class="modal-card-title">Retry torrent</p>
|
|
||||||
<button class="delete" aria-label="close" (click)="retryCancel()"></button>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Retry</label>
|
|
||||||
<div class="control">
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" name="retry" [value]="0" [(ngModel)]="retry" />
|
|
||||||
Retry on Real-Debrid
|
|
||||||
</label>
|
|
||||||
<br />
|
|
||||||
<label class="radio">
|
|
||||||
<input type="radio" name="retry" [value]="1" [(ngModel)]="retry" />
|
|
||||||
Retry downloading locally
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="notification is-danger is-light" *ngIf="retryError?.length > 0">
|
|
||||||
Error retrying torrent: {{ retryError }}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<button
|
|
||||||
class="button is-success"
|
|
||||||
(click)="retryOk()"
|
|
||||||
[disabled]="retrying"
|
|
||||||
[ngClass]="{ 'is-loading': retrying }"
|
|
||||||
>
|
|
||||||
Retry
|
|
||||||
</button>
|
|
||||||
<button class="button" (click)="retryCancel()" [disabled]="retrying" [ngClass]="{ 'is-loading': retrying }">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,4 @@ table {
|
||||||
tr {
|
tr {
|
||||||
cursor: pointer;
|
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 { Torrent } from '../models/torrent.model';
|
||||||
import { TorrentService } from '../torrent.service';
|
import { TorrentService } from '../torrent.service';
|
||||||
|
import { forkJoin, Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-torrent-table',
|
selector: 'app-torrent-table',
|
||||||
templateUrl: './torrent-table.component.html',
|
templateUrl: './torrent-table.component.html',
|
||||||
styleUrls: ['./torrent-table.component.scss'],
|
styleUrls: ['./torrent-table.component.scss'],
|
||||||
})
|
})
|
||||||
export class TorrentTableComponent implements OnInit, OnDestroy {
|
export class TorrentTableComponent implements OnInit {
|
||||||
public torrents: Torrent[] = [];
|
public torrents: Torrent[] = [];
|
||||||
|
public selectedTorrents: string[] = [];
|
||||||
public error: string;
|
public error: string;
|
||||||
public showFiles: { [key: string]: boolean } = {};
|
|
||||||
|
|
||||||
public isDeleteModalActive: boolean;
|
public isDeleteModalActive: boolean;
|
||||||
public deleteError: string;
|
public deleteError: string;
|
||||||
public deleting: boolean;
|
public deleting: boolean;
|
||||||
public deleteTorrentId: string;
|
|
||||||
public deleteData: boolean;
|
public deleteData: boolean;
|
||||||
public deleteRdTorrent: boolean;
|
public deleteRdTorrent: boolean;
|
||||||
public deleteLocalFiles: boolean;
|
public deleteLocalFiles: boolean;
|
||||||
|
|
||||||
public isRetryModalActive: boolean;
|
constructor(private router: Router, private torrentService: TorrentService) {}
|
||||||
public retryError: string;
|
|
||||||
public retrying: boolean;
|
|
||||||
public retryTorrentId: string;
|
|
||||||
public retry: number;
|
|
||||||
|
|
||||||
constructor(private torrentService: TorrentService) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.torrentService.getList().subscribe(
|
this.torrentService.getList().subscribe(
|
||||||
(result) => {
|
(result) => {
|
||||||
this.torrents = result;
|
this.torrents = result;
|
||||||
|
|
||||||
this.torrentService.connect();
|
|
||||||
|
|
||||||
this.torrentService.update$.subscribe((result2) => {
|
this.torrentService.update$.subscribe((result2) => {
|
||||||
this.torrents = result2;
|
this.torrents = result2;
|
||||||
});
|
});
|
||||||
|
|
@ -45,24 +38,40 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
public openTorrent(torrentId: string): void {
|
||||||
this.torrentService.disconnect();
|
this.router.navigate([`/torrent/${torrentId}`]);
|
||||||
}
|
|
||||||
|
|
||||||
public selectTorrent(torrent: Torrent): void {
|
|
||||||
this.showFiles[torrent.torrentId] = !this.showFiles[torrent.torrentId];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public trackByMethod(index: number, el: Torrent): string {
|
public trackByMethod(index: number, el: Torrent): string {
|
||||||
return el.torrentId;
|
return el.torrentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public showDeleteModal(torrentId: string): void {
|
public toggleSelectAll(event: any) {
|
||||||
|
this.selectedTorrents = [];
|
||||||
|
|
||||||
|
if (event.target.checked) {
|
||||||
|
this.torrents.map((torrent) => {
|
||||||
|
this.selectedTorrents.push(torrent.torrentId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleSelect(torrentId: string) {
|
||||||
|
const index = this.selectedTorrents.indexOf(torrentId);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
this.selectedTorrents.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.selectedTorrents.push(torrentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public showDeleteModal(): void {
|
||||||
this.deleteData = false;
|
this.deleteData = false;
|
||||||
this.deleteRdTorrent = false;
|
this.deleteRdTorrent = false;
|
||||||
this.deleteLocalFiles = false;
|
this.deleteLocalFiles = false;
|
||||||
|
this.deleteError = null;
|
||||||
|
|
||||||
this.deleteTorrentId = torrentId;
|
|
||||||
this.isDeleteModalActive = true;
|
this.isDeleteModalActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,43 +82,23 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
|
||||||
public deleteOk(): void {
|
public deleteOk(): void {
|
||||||
this.deleting = true;
|
this.deleting = true;
|
||||||
|
|
||||||
this.torrentService
|
let calls: Observable<void>[] = [];
|
||||||
.delete(this.deleteTorrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
|
|
||||||
.subscribe(
|
|
||||||
() => {
|
|
||||||
this.isDeleteModalActive = false;
|
|
||||||
this.deleting = false;
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
this.deleteError = err.error;
|
|
||||||
this.deleting = false;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public showRetryModal(torrentId: string): void {
|
this.selectedTorrents.forEach((torrentId) => {
|
||||||
this.retry = 0;
|
calls.push(this.torrentService.delete(torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles));
|
||||||
|
});
|
||||||
|
|
||||||
this.retryTorrentId = torrentId;
|
forkJoin(calls).subscribe({
|
||||||
this.isRetryModalActive = true;
|
complete: () => {
|
||||||
}
|
this.isDeleteModalActive = false;
|
||||||
|
this.deleting = false;
|
||||||
|
|
||||||
public retryCancel(): void {
|
this.selectedTorrents = [];
|
||||||
this.isRetryModalActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public retryOk(): void {
|
|
||||||
this.retrying = true;
|
|
||||||
|
|
||||||
this.torrentService.retry(this.retryTorrentId, this.retry).subscribe(
|
|
||||||
() => {
|
|
||||||
this.isRetryModalActive = false;
|
|
||||||
this.retrying = false;
|
|
||||||
},
|
},
|
||||||
(err) => {
|
error: (err) => {
|
||||||
this.retryError = err.error;
|
this.deleteError = err.error;
|
||||||
this.retrying = false;
|
this.deleting = false;
|
||||||
}
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,25 @@ import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import * as signalR from '@microsoft/signalr';
|
import * as signalR from '@microsoft/signalr';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import { Torrent } from './models/torrent.model';
|
import { Torrent, TorrentFileAvailability } from './models/torrent.model';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class TorrentService {
|
export class TorrentService {
|
||||||
constructor(private http: HttpClient) {}
|
|
||||||
|
|
||||||
public update$: Subject<Torrent[]> = new Subject();
|
public update$: Subject<Torrent[]> = new Subject();
|
||||||
|
|
||||||
private connection: signalR.HubConnection;
|
private connection: signalR.HubConnection;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
public connect(): void {
|
public connect(): void {
|
||||||
|
if (this.connection != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build();
|
this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build();
|
||||||
this.connection.start().catch((err) => console.error(err));
|
this.connection.start().catch((err) => console.error(err));
|
||||||
|
|
||||||
|
|
@ -23,38 +29,38 @@ export class TorrentService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnect(): void {
|
|
||||||
this.connection?.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getList(): Observable<Torrent[]> {
|
public getList(): Observable<Torrent[]> {
|
||||||
return this.http.get<Torrent[]>(`/Api/Torrents`);
|
return this.http.get<Torrent[]>(`/Api/Torrents`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public uploadMagnet(magnetLink: string, autoDelete: boolean): Observable<void> {
|
public get(torrentId: string): Observable<Torrent> {
|
||||||
|
return this.http.get<Torrent>(`/Api/Torrents/Get/${torrentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public uploadMagnet(magnetLink: string, torrent: Torrent): Observable<void> {
|
||||||
return this.http.post<void>(`/Api/Torrents/UploadMagnet`, {
|
return this.http.post<void>(`/Api/Torrents/UploadMagnet`, {
|
||||||
magnetLink,
|
magnetLink,
|
||||||
autoDelete,
|
torrent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public uploadFile(file: File, autoDelete: boolean): Observable<void> {
|
public uploadFile(file: File, torrent: Torrent): Observable<void> {
|
||||||
const formData: FormData = new FormData();
|
const formData: FormData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('formData', JSON.stringify({ autoDelete }));
|
formData.append('formData', JSON.stringify({ torrent }));
|
||||||
return this.http.post<void>(`/Api/Torrents/UploadFile`, formData);
|
return this.http.post<void>(`/Api/Torrents/UploadFile`, formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkFilesMagnet(magnetLink: string): Observable<string[]> {
|
public checkFilesMagnet(magnetLink: string): Observable<TorrentFileAvailability[]> {
|
||||||
return this.http.post<string[]>(`/Api/Torrents/CheckFilesMagnet`, {
|
return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFilesMagnet`, {
|
||||||
magnetLink,
|
magnetLink,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkFiles(file: File): Observable<string[]> {
|
public checkFiles(file: File): Observable<TorrentFileAvailability[]> {
|
||||||
const formData: FormData = new FormData();
|
const formData: FormData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
return this.http.post<string[]>(`/Api/Torrents/CheckFiles`, formData);
|
return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFiles`, formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public delete(
|
public delete(
|
||||||
|
|
@ -70,9 +76,15 @@ export class TorrentService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public retry(torrentId: string, retry: number): Observable<void> {
|
public retry(torrentId: string): Observable<void> {
|
||||||
return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, {
|
return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, {});
|
||||||
retry,
|
}
|
||||||
});
|
|
||||||
|
public retryDownload(downloadId: string): Observable<void> {
|
||||||
|
return this.http.post<void>(`/Api/Torrents/RetryDownload/${downloadId}`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(torrent: Torrent): Observable<void> {
|
||||||
|
return this.http.put<void>(`/Api/Torrents/Update`, torrent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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.
|
// This file can be replaced during build by using the `fileReplacements` array.
|
||||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||||
// The list of file replacements can be found in `angular.json`.
|
// The list of file replacements can be found in `angular.json`.
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
|
|
@ -13,4 +13,4 @@ export const environment = {
|
||||||
* This import should be commented out in production mode because it will have a negative impact
|
* This import should be commented out in production mode because it will have a negative impact
|
||||||
* on performance if an error is thrown.
|
* on performance if an error is thrown.
|
||||||
*/
|
*/
|
||||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Real-Debrid Torrent Client</title>
|
<title>Real-Debrid Torrent Client</title>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
window["_app_base"] = "/" + window.location.pathname.split("/")[1];
|
||||||
|
console.log("setting base href to " + window["_app_base"]);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
* file.
|
* file.
|
||||||
*
|
*
|
||||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
||||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
||||||
*
|
*
|
||||||
* Learn more in https://angular.io/guide/browser-support
|
* Learn more in https://angular.io/guide/browser-support
|
||||||
*/
|
*/
|
||||||
|
|
@ -18,16 +18,6 @@
|
||||||
* BROWSER POLYFILLS
|
* BROWSER POLYFILLS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
|
||||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Web Animations `@angular/platform-browser/animations`
|
|
||||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
|
||||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
|
||||||
*/
|
|
||||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||||
|
|
@ -55,7 +45,7 @@
|
||||||
/***************************************************************************************************
|
/***************************************************************************************************
|
||||||
* Zone JS is required by default for Angular itself.
|
* Zone JS is required by default for Angular itself.
|
||||||
*/
|
*/
|
||||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
import 'zone.js'; // Included with Angular CLI.
|
||||||
|
|
||||||
/***************************************************************************************************
|
/***************************************************************************************************
|
||||||
* APPLICATION IMPORTS
|
* APPLICATION IMPORTS
|
||||||
|
|
|
||||||
|
|
@ -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",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/app",
|
"outDir": "./out-tsc/app",
|
||||||
"types": []
|
"types": []
|
||||||
},
|
},
|
||||||
"files": [
|
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||||
"src/main.ts",
|
"include": ["src/**/*.d.ts"]
|
||||||
"src/polyfills.ts"
|
|
||||||
],
|
|
||||||
"include": [
|
|
||||||
"src/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,34 @@
|
||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"outDir": "./dist/out-tsc",
|
"outDir": "./dist/out-tsc",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"module": "es2020",
|
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"target": "es2015",
|
"strictNullChecks": false,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "es2020",
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2018",
|
"es2020",
|
||||||
"dom"
|
"dom"
|
||||||
]
|
],
|
||||||
|
"useDefineForClassFields": false
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"fullTemplateTypeCheck": true,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
"strictInjectionParameters": true
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
$imageName = "$($DockerAccount)/rdtclient"
|
||||||
|
|
||||||
$dockerArgs = @( "buildx", "build", "--push", "--platform", $Platforms, "--tag", "$($imageName):latest", "." )
|
$dockerArgs = @( "buildx", "build", "--no-cache", "--push", "--platform", $Platforms, "--tag", "$($imageName):latest", "." )
|
||||||
|
|
||||||
if (![string]::IsNullOrEmpty($Version)) {
|
if ([string]::IsNullOrEmpty($Version)) {
|
||||||
$dockerArgs += @("--tag", "$($imageName):$($Version)" )
|
$Version = (Get-Content "package.json" | ConvertFrom-Json).version
|
||||||
$dockerApps += @("--build-arg", "VERSION=$($Version)" )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dockerArgs += @("--tag", "$($imageName):$($Version)" )
|
||||||
|
$dockerApps += @("--build-arg", "VERSION=$($Version)" )
|
||||||
|
|
||||||
Write-Host "Generating docker image $imageName for $Platforms"
|
Write-Host "Generating docker image $imageName for $Platforms"
|
||||||
& docker $dockerArgs
|
& docker $dockerArgs
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ $newNavbar = (Get-Content $navbar) -replace 'Version .*?<', "Version $version<"
|
||||||
|
|
||||||
cd client
|
cd client
|
||||||
npm install
|
npm install
|
||||||
ng build --prod --output-path=..\server\RdtClient.Web\wwwroot
|
ng build --configuration production --output-path=..\server\RdtClient.Web\wwwroot
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
cd server
|
cd server
|
||||||
|
|
|
||||||
3265
package-lock.json
generated
3265
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "rdt-client",
|
"name": "rdt-client",
|
||||||
"version": "1.7.4",
|
"version": "2.0.22",
|
||||||
"description": "This is a web interface to manage your torrents on Real-Debrid.",
|
"description": "This is a web interface to manage your torrents on Real-Debrid.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gh-release": "^5.0.0"
|
"gh-release": "^7.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
||||||
39
server/.gitignore
vendored
Normal file
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 Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using RdtClient.Data.Models.Data;
|
using RdtClient.Data.Models.Data;
|
||||||
|
|
||||||
namespace RdtClient.Data.Data
|
namespace RdtClient.Data.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
public class DataContext : IdentityDbContext
|
||||||
{
|
{
|
||||||
public class DataContext : IdentityDbContext
|
public DataContext(DbContextOptions options) : base(options)
|
||||||
{
|
{
|
||||||
public DataContext(DbContextOptions options) : base(options)
|
}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public DbSet<Download> Downloads { get; set; }
|
public DbSet<Download> Downloads { get; set; }
|
||||||
public DbSet<Setting> Settings { get; set; }
|
public DbSet<Setting> Settings { get; set; }
|
||||||
public DbSet<Torrent> Torrents { get; set; }
|
public DbSet<Torrent> Torrents { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
}
|
var cascadeFKs = builder.Model.GetEntityTypes()
|
||||||
|
.SelectMany(t => t.GetForeignKeys())
|
||||||
|
.Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade);
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
foreach (var fk in cascadeFKs)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
fk.DeleteBehavior = DeleteBehavior.Restrict;
|
||||||
|
|
||||||
var cascadeFKs = builder.Model.GetEntityTypes()
|
|
||||||
.SelectMany(t => t.GetForeignKeys())
|
|
||||||
.Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
foreach (var fk in cascadeFKs)
|
|
||||||
{
|
|
||||||
fk.DeleteBehavior = DeleteBehavior.Restrict;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Seed()
|
|
||||||
{
|
|
||||||
var seedSettings = new List<Setting>
|
|
||||||
{
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "RealDebridApiKey",
|
|
||||||
Type = "String",
|
|
||||||
Value = ""
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "DownloadPath",
|
|
||||||
Type = "String",
|
|
||||||
#if DEBUG
|
|
||||||
Value = @"C:\Temp\rdtclient"
|
|
||||||
#else
|
|
||||||
Value = "/data/downloads"
|
|
||||||
#endif
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "DownloadClient",
|
|
||||||
Type = "String",
|
|
||||||
Value = @"Simple"
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "TempPath",
|
|
||||||
Type = "String",
|
|
||||||
#if DEBUG
|
|
||||||
Value = @"C:\Temp\rdtclient"
|
|
||||||
#else
|
|
||||||
Value = "/data/downloads"
|
|
||||||
#endif
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "MappedPath",
|
|
||||||
Type = "String",
|
|
||||||
#if DEBUG
|
|
||||||
Value = @"C:\Temp\rdtclient"
|
|
||||||
#else
|
|
||||||
Value = @"C:\Downloads"
|
|
||||||
#endif
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "DownloadLimit",
|
|
||||||
Type = "Int32",
|
|
||||||
Value = "2"
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "UnpackLimit",
|
|
||||||
Type = "Int32",
|
|
||||||
Value = "1"
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "MinFileSize",
|
|
||||||
Type = "Int32",
|
|
||||||
Value = "0"
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "OnlyDownloadAvailableFiles",
|
|
||||||
Type = "Int32",
|
|
||||||
Value = "1"
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "DownloadChunkCount",
|
|
||||||
Type = "Int32",
|
|
||||||
Value = "8"
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "DownloadMaxSpeed",
|
|
||||||
Type = "Int32",
|
|
||||||
Value = "0"
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "ProxyServer",
|
|
||||||
Type = "String",
|
|
||||||
Value = ""
|
|
||||||
},
|
|
||||||
new Setting
|
|
||||||
{
|
|
||||||
SettingId = "LogLevel",
|
|
||||||
Type = "String",
|
|
||||||
Value = "Warning"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var dbSettings = await Settings.ToListAsync();
|
|
||||||
foreach (var seedSetting in seedSettings)
|
|
||||||
{
|
|
||||||
var dbSetting = dbSettings.FirstOrDefault(m => m.SettingId == seedSetting.SettingId);
|
|
||||||
|
|
||||||
if (dbSetting == null)
|
|
||||||
{
|
|
||||||
await Settings.AddAsync(seedSetting);
|
|
||||||
await SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,190 +1,267 @@
|
||||||
using System;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Download = RdtClient.Data.Models.Data.Download;
|
using Download = RdtClient.Data.Models.Data.Download;
|
||||||
|
|
||||||
namespace RdtClient.Data.Data
|
namespace RdtClient.Data.Data;
|
||||||
|
|
||||||
|
public class DownloadData
|
||||||
{
|
{
|
||||||
public interface IDownloadData
|
private readonly DataContext _dataContext;
|
||||||
|
|
||||||
|
public DownloadData(DataContext dataContext)
|
||||||
{
|
{
|
||||||
Task<Download> GetById(Guid downloadId);
|
_dataContext = dataContext;
|
||||||
Task<Download> Get(Guid torrentId, String path);
|
|
||||||
Task<Download> Add(Guid torrentId, String path);
|
|
||||||
Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink);
|
|
||||||
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);
|
|
||||||
Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime);
|
|
||||||
Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime);
|
|
||||||
Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime);
|
|
||||||
Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime);
|
|
||||||
Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime);
|
|
||||||
Task UpdateError(Guid downloadId, String error);
|
|
||||||
Task UpdateRetryCount(Guid downloadId, Int32 retryCount);
|
|
||||||
Task DeleteForTorrent(Guid torrentId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DownloadData : IDownloadData
|
public async Task<List<Download>> GetForTorrent(Guid torrentId)
|
||||||
{
|
{
|
||||||
private readonly DataContext _dataContext;
|
return await _dataContext.Downloads
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(m => m.TorrentId == torrentId)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public DownloadData(DataContext dataContext)
|
public async Task<Download?> GetById(Guid downloadId)
|
||||||
|
{
|
||||||
|
return await _dataContext.Downloads
|
||||||
|
.Include(m => m.Torrent)
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Download?> Get(Guid torrentId, String path)
|
||||||
|
{
|
||||||
|
return await _dataContext.Downloads
|
||||||
|
.Include(m => m.Torrent)
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Download> Add(Guid torrentId, String path)
|
||||||
|
{
|
||||||
|
var download = new Download
|
||||||
{
|
{
|
||||||
_dataContext = dataContext;
|
DownloadId = Guid.NewGuid(),
|
||||||
|
TorrentId = torrentId,
|
||||||
|
Path = path,
|
||||||
|
Added = DateTimeOffset.UtcNow,
|
||||||
|
DownloadQueued = DateTimeOffset.UtcNow,
|
||||||
|
RetryCount = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
await _dataContext.Downloads.AddAsync(download);
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
|
|
||||||
|
return download;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<Download>> Get()
|
dbDownload.Link = unrestrictedLink;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
{
|
{
|
||||||
return await _dataContext.Downloads
|
return;
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.Torrent)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<Download>> GetForTorrent(Guid torrentId)
|
dbDownload.DownloadStarted = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
{
|
{
|
||||||
return await _dataContext.Downloads
|
return;
|
||||||
.AsNoTracking()
|
|
||||||
.Where(m => m.TorrentId == torrentId)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Download> GetById(Guid downloadId)
|
|
||||||
{
|
|
||||||
return await _dataContext.Downloads
|
|
||||||
.Include(m => m.Torrent)
|
|
||||||
.AsNoTracking()
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Download> Get(Guid torrentId, String path)
|
|
||||||
{
|
|
||||||
return await _dataContext.Downloads.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Download> Add(Guid torrentId, String path)
|
|
||||||
{
|
|
||||||
var download = new Download
|
|
||||||
{
|
|
||||||
DownloadId = Guid.NewGuid(),
|
|
||||||
TorrentId = torrentId,
|
|
||||||
Path = path,
|
|
||||||
Added = DateTimeOffset.UtcNow,
|
|
||||||
DownloadQueued = DateTimeOffset.UtcNow,
|
|
||||||
RetryCount = 0
|
|
||||||
};
|
|
||||||
|
|
||||||
await _dataContext.Downloads.AddAsync(download);
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
|
|
||||||
return download;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
|
|
||||||
{
|
|
||||||
var dbDownload = await _dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
dbDownload.Link = unrestrictedLink;
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime)
|
|
||||||
{
|
|
||||||
var dbDownload = await _dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
dbDownload.DownloadStarted = dateTime;
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime)
|
|
||||||
{
|
|
||||||
var dbDownload = await _dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbDownload.DownloadFinished = dateTime;
|
dbDownload.DownloadFinished = dateTime;
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
await _dataContext.SaveChangesAsync();
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime)
|
await TorrentData.VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
{
|
{
|
||||||
var dbDownload = await _dataContext.Downloads
|
return;
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
dbDownload.UnpackingQueued = dateTime;
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime)
|
dbDownload.UnpackingQueued = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
{
|
{
|
||||||
var dbDownload = await _dataContext.Downloads
|
return;
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
dbDownload.UnpackingStarted = dateTime;
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime)
|
dbDownload.UnpackingStarted = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
{
|
{
|
||||||
var dbDownload = await _dataContext.Downloads
|
return;
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
dbDownload.UnpackingFinished = dateTime;
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbDownload.UnpackingFinished = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime)
|
public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
{
|
{
|
||||||
var dbDownload = await _dataContext.Downloads
|
return;
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
dbDownload.Completed = dateTime;
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateError(Guid downloadId, String error)
|
dbDownload.Completed = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateError(Guid downloadId, String? error)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
{
|
{
|
||||||
var dbDownload = await _dataContext.Downloads
|
return;
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
dbDownload.Error = error;
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateRetryCount(Guid downloadId, Int32 retryCount)
|
dbDownload.Error = error;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateRetryCount(Guid downloadId, Int32 retryCount)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
{
|
{
|
||||||
var dbDownload = await _dataContext.Downloads
|
return;
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
dbDownload.RetryCount = retryCount;
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteForTorrent(Guid torrentId)
|
dbDownload.RetryCount = retryCount;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateRemoteId(Guid downloadId, String remoteId)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
{
|
{
|
||||||
var downloads = await _dataContext.Downloads
|
return;
|
||||||
.Where(m => m.TorrentId == torrentId)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
_dataContext.Downloads.RemoveRange(downloads);
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbDownload.RemoteId = remoteId;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteForTorrent(Guid torrentId)
|
||||||
|
{
|
||||||
|
var downloads = await _dataContext.Downloads
|
||||||
|
.Where(m => m.TorrentId == torrentId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
_dataContext.Downloads.RemoveRange(downloads);
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Reset(Guid downloadId)
|
||||||
|
{
|
||||||
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"Cannot find download with ID {downloadId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
dbDownload.RetryCount = 0;
|
||||||
|
dbDownload.Link = null;
|
||||||
|
dbDownload.Added = DateTimeOffset.UtcNow;
|
||||||
|
dbDownload.DownloadQueued = DateTimeOffset.UtcNow;
|
||||||
|
dbDownload.DownloadStarted = null;
|
||||||
|
dbDownload.DownloadFinished = null;
|
||||||
|
dbDownload.UnpackingQueued = null;
|
||||||
|
dbDownload.UnpackingStarted = null;
|
||||||
|
dbDownload.UnpackingFinished = null;
|
||||||
|
dbDownload.Completed = null;
|
||||||
|
dbDownload.Error = null;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,53 +1,212 @@
|
||||||
using System;
|
using System.ComponentModel;
|
||||||
using System.Collections.Generic;
|
using System.Reflection;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using RdtClient.Data.Models.Data;
|
using RdtClient.Data.Models.Data;
|
||||||
|
using RdtClient.Data.Models.Internal;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace RdtClient.Data.Data
|
namespace RdtClient.Data.Data;
|
||||||
|
|
||||||
|
public class SettingData
|
||||||
{
|
{
|
||||||
public interface ISettingData
|
private readonly DataContext _dataContext;
|
||||||
|
|
||||||
|
public static DbSettings Get { get; } = new DbSettings();
|
||||||
|
|
||||||
|
public static IList<SettingProperty> GetAll()
|
||||||
{
|
{
|
||||||
Task<IList<Setting>> GetAll();
|
return GetSettings(Get, null).ToList();
|
||||||
Task Update(IList<Setting> settings);
|
|
||||||
Task<Setting> Get(String key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SettingData : ISettingData
|
public SettingData(DataContext dataContext)
|
||||||
{
|
{
|
||||||
private readonly DataContext _dataContext;
|
_dataContext = dataContext;
|
||||||
|
}
|
||||||
|
|
||||||
public SettingData(DataContext dataContext)
|
public async Task Update(IList<SettingProperty> settings)
|
||||||
|
{
|
||||||
|
var dbSettings = await _dataContext.Settings.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var dbSetting in dbSettings)
|
||||||
{
|
{
|
||||||
_dataContext = dataContext;
|
var setting = settings.FirstOrDefault(m => m.Key == dbSetting.SettingId);
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IList<Setting>> GetAll()
|
if (setting != null)
|
||||||
{
|
|
||||||
return await _dataContext.Settings.AsNoTracking().ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Update(IList<Setting> settings)
|
|
||||||
{
|
|
||||||
var dbSettings = await _dataContext.Settings.ToListAsync();
|
|
||||||
|
|
||||||
foreach (var dbSetting in dbSettings)
|
|
||||||
{
|
{
|
||||||
var setting = settings.FirstOrDefault(m => m.SettingId == dbSetting.SettingId);
|
dbSetting.Value = setting.Value?.ToString();
|
||||||
|
|
||||||
if (setting != null)
|
|
||||||
{
|
|
||||||
dbSetting.Value = setting.Value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await ResetCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Update(String settingId, Object? value)
|
||||||
|
{
|
||||||
|
var dbSetting = await _dataContext.Settings.FirstOrDefaultAsync(m => m.SettingId == settingId);
|
||||||
|
|
||||||
|
if (dbSetting == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbSetting.Value = value?.ToString();
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await ResetCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ResetCache()
|
||||||
|
{
|
||||||
|
var settings = await _dataContext.Settings.AsNoTracking().ToListAsync();
|
||||||
|
|
||||||
|
if (settings.Count == 0)
|
||||||
|
{
|
||||||
|
throw new Exception("No settings found, please restart");
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSettings(settings, Get, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Seed()
|
||||||
|
{
|
||||||
|
var dbSettings = await _dataContext.Settings.AsNoTracking().ToListAsync();
|
||||||
|
|
||||||
|
var expectedSettings = GetSettings(Get, null).Where(m => m.Type != "Object").Select(m => new Setting
|
||||||
|
{
|
||||||
|
SettingId = m.Key,
|
||||||
|
Value = m.Value?.ToString()
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var newSettings = expectedSettings.Where(m => dbSettings.All(p => p.SettingId != m.SettingId)).ToList();
|
||||||
|
|
||||||
|
if (newSettings.Any())
|
||||||
|
{
|
||||||
|
await _dataContext.Settings.AddRangeAsync(newSettings);
|
||||||
await _dataContext.SaveChangesAsync();
|
await _dataContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Setting> Get(String key)
|
var oldSettings = dbSettings.Where(m => expectedSettings.All(p => p.SettingId != m.SettingId)).ToList();
|
||||||
|
|
||||||
|
if (oldSettings.Any())
|
||||||
{
|
{
|
||||||
return await _dataContext.Settings.AsNoTracking().FirstOrDefaultAsync(m => m.SettingId == key);
|
_dataContext.Settings.RemoveRange(oldSettings);
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static IEnumerable<SettingProperty> GetSettings(Object defaultSetting, String? parent)
|
||||||
|
{
|
||||||
|
var result = new List<SettingProperty>();
|
||||||
|
|
||||||
|
var properties = defaultSetting.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
var displayName = Attribute.GetCustomAttribute(property, typeof(DisplayNameAttribute)) as DisplayNameAttribute;
|
||||||
|
var description = Attribute.GetCustomAttribute(property, typeof(DescriptionAttribute)) as DescriptionAttribute;
|
||||||
|
var propertyName = property.Name;
|
||||||
|
|
||||||
|
if (parent != null)
|
||||||
|
{
|
||||||
|
propertyName = $"{parent}:{propertyName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingProperty = new SettingProperty
|
||||||
|
{
|
||||||
|
Key = propertyName,
|
||||||
|
DisplayName = displayName?.DisplayName,
|
||||||
|
Description = description?.Description,
|
||||||
|
Type = property.PropertyType.Name
|
||||||
|
};
|
||||||
|
|
||||||
|
if (property.PropertyType.IsEnum ||
|
||||||
|
property.PropertyType.IsValueType ||
|
||||||
|
property.PropertyType == typeof(String))
|
||||||
|
{
|
||||||
|
settingProperty.Value = property.GetValue(defaultSetting);
|
||||||
|
|
||||||
|
if (property.PropertyType.IsEnum)
|
||||||
|
{
|
||||||
|
settingProperty.Type = "Enum";
|
||||||
|
settingProperty.EnumValues = new Dictionary<Int32, String>();
|
||||||
|
|
||||||
|
foreach (var e in Enum.GetValues(property.PropertyType).Cast<Enum>())
|
||||||
|
{
|
||||||
|
var enumMember = property.PropertyType.GetMember(e.ToString()).First();
|
||||||
|
var enumDescriptionAttribute = enumMember.GetCustomAttribute<DescriptionAttribute>();
|
||||||
|
var enumName = enumDescriptionAttribute?.Description ?? Enum.GetName(property.PropertyType, e) ?? "Unknown value";
|
||||||
|
settingProperty.EnumValues.Add((Int32)(Object)e, enumName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(settingProperty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
settingProperty.Type = "Object";
|
||||||
|
result.Add(settingProperty);
|
||||||
|
|
||||||
|
var childResults = GetSettings(property.GetValue(defaultSetting)!, propertyName);
|
||||||
|
result.AddRange(childResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetSettings(IList<Setting> settings, Object defaultSetting, String? parent)
|
||||||
|
{
|
||||||
|
var properties = defaultSetting.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
var propertyName = property.Name;
|
||||||
|
|
||||||
|
if (parent != null)
|
||||||
|
{
|
||||||
|
propertyName = $"{parent}:{propertyName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.PropertyType.IsEnum ||
|
||||||
|
property.PropertyType.IsValueType ||
|
||||||
|
property.PropertyType == typeof(String))
|
||||||
|
{
|
||||||
|
var setting = settings.FirstOrDefault(m => m.SettingId == propertyName);
|
||||||
|
|
||||||
|
if (setting != null)
|
||||||
|
{
|
||||||
|
if (property.PropertyType.IsEnum)
|
||||||
|
{
|
||||||
|
var newValue = Enum.Parse(property.PropertyType, setting.Value ?? "0");
|
||||||
|
property.SetValue(defaultSetting, newValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var converter = TypeDescriptor.GetConverter(property.PropertyType);
|
||||||
|
|
||||||
|
if (setting.Value == null)
|
||||||
|
{
|
||||||
|
property.SetValue(defaultSetting, null);
|
||||||
|
}
|
||||||
|
else if (converter.IsValid(setting.Value))
|
||||||
|
{
|
||||||
|
var newValue = converter.ConvertFrom(setting.Value);
|
||||||
|
property.SetValue(defaultSetting, newValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Warning($"Invalid value for setting {propertyName}: {setting.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetSettings(settings, property.GetValue(defaultSetting)!, propertyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,168 +1,309 @@
|
||||||
using System;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using RdtClient.Data.Models.Data;
|
using RdtClient.Data.Models.Data;
|
||||||
|
|
||||||
namespace RdtClient.Data.Data
|
namespace RdtClient.Data.Data;
|
||||||
|
|
||||||
|
public class TorrentData
|
||||||
{
|
{
|
||||||
public interface ITorrentData
|
private static IList<Torrent>? _torrentCache;
|
||||||
|
|
||||||
|
private static readonly SemaphoreSlim TorrentCacheLock = new(1, 1);
|
||||||
|
|
||||||
|
private readonly DataContext _dataContext;
|
||||||
|
|
||||||
|
public TorrentData(DataContext dataContext)
|
||||||
{
|
{
|
||||||
Task<IList<Torrent>> Get();
|
_dataContext = dataContext;
|
||||||
Task<Torrent> GetById(Guid torrentId);
|
|
||||||
Task<Torrent> GetByHash(String hash);
|
|
||||||
Task<Torrent> Add(String realDebridId, String hash, String category, Boolean autoDelete, String fileOrMagnetContents, Boolean isFile);
|
|
||||||
Task UpdateRdData(Torrent torrent);
|
|
||||||
Task UpdateCategory(Guid torrentId, String category);
|
|
||||||
Task UpdateComplete(Guid torrentId, DateTimeOffset? datetime);
|
|
||||||
Task Delete(Guid torrentId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TorrentData : ITorrentData
|
public async Task<IList<Torrent>> Get()
|
||||||
{
|
{
|
||||||
private readonly DataContext _dataContext;
|
await TorrentCacheLock.WaitAsync();
|
||||||
|
|
||||||
public TorrentData(DataContext dataContext)
|
try
|
||||||
{
|
{
|
||||||
_dataContext = dataContext;
|
_torrentCache ??= await _dataContext.Torrents
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(m => m.Downloads)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return _torrentCache.OrderBy(m => m.Priority ?? 9999).ThenBy(m => m.Added).ToList();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TorrentCacheLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Torrent?> GetById(Guid torrentId)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(m => m.Downloads)
|
||||||
|
.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<Torrent>> Get()
|
foreach (var file in dbTorrent.Downloads)
|
||||||
{
|
{
|
||||||
var results = await _dataContext.Torrents
|
file.Torrent = null;
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.Downloads)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return results.OrderByDescending(m => m.Added).ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Torrent> GetById(Guid torrentId)
|
return dbTorrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Torrent?> GetByHash(String hash)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(m => m.Downloads)
|
||||||
|
.FirstOrDefaultAsync(m => m.Hash.ToLower() == hash.ToLower());
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
{
|
{
|
||||||
var dbTorrent = await _dataContext.Torrents
|
return null;
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.Downloads)
|
|
||||||
.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var file in dbTorrent.Downloads)
|
|
||||||
{
|
|
||||||
file.Torrent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbTorrent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Torrent> GetByHash(String hash)
|
foreach (var file in dbTorrent.Downloads)
|
||||||
{
|
{
|
||||||
var dbTorrent = await _dataContext.Torrents
|
file.Torrent = null;
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.Downloads)
|
|
||||||
.FirstOrDefaultAsync(m => m.Hash.ToLower() == hash.ToLower());
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var file in dbTorrent.Downloads)
|
|
||||||
{
|
|
||||||
file.Torrent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbTorrent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Torrent> Add(String realDebridId, String hash, String category, Boolean autoDelete, String fileOrMagnetContents, Boolean isFile)
|
return dbTorrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Torrent> Add(String realDebridId,
|
||||||
|
String hash,
|
||||||
|
String? fileOrMagnetContents,
|
||||||
|
Boolean isFile,
|
||||||
|
Torrent torrent)
|
||||||
|
{
|
||||||
|
var newTorrent = new Torrent
|
||||||
{
|
{
|
||||||
var torrent = new Torrent
|
TorrentId = Guid.NewGuid(),
|
||||||
{
|
Added = DateTimeOffset.UtcNow,
|
||||||
TorrentId = Guid.NewGuid(),
|
RdId = realDebridId,
|
||||||
Added = DateTimeOffset.UtcNow,
|
Hash = hash.ToLower(),
|
||||||
RdId = realDebridId,
|
Category = torrent.Category,
|
||||||
Hash = hash.ToLower(),
|
HostDownloadAction = torrent.HostDownloadAction,
|
||||||
Category = category,
|
DownloadAction = torrent.DownloadAction,
|
||||||
AutoDelete = autoDelete,
|
FinishedAction = torrent.FinishedAction,
|
||||||
FileOrMagnet = fileOrMagnetContents,
|
DownloadMinSize = torrent.DownloadMinSize,
|
||||||
IsFile = isFile
|
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||||
};
|
FileOrMagnet = fileOrMagnetContents,
|
||||||
|
IsFile = isFile,
|
||||||
|
Priority = torrent.Priority,
|
||||||
|
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||||
|
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
|
||||||
|
DeleteOnError = torrent.DeleteOnError,
|
||||||
|
Lifetime = torrent.Lifetime
|
||||||
|
};
|
||||||
|
|
||||||
await _dataContext.Torrents.AddAsync(torrent);
|
await _dataContext.Torrents.AddAsync(newTorrent);
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
return torrent;
|
await VoidCache();
|
||||||
|
|
||||||
|
return newTorrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateRdData(Torrent torrent)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateRdData(Torrent torrent)
|
dbTorrent.RdName = torrent.RdName;
|
||||||
{
|
dbTorrent.RdSize = torrent.RdSize;
|
||||||
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
|
dbTorrent.RdHost = torrent.RdHost;
|
||||||
|
dbTorrent.RdSplit = torrent.RdSplit;
|
||||||
if (dbTorrent == null)
|
dbTorrent.RdProgress = torrent.RdProgress;
|
||||||
{
|
dbTorrent.RdStatus = torrent.RdStatus;
|
||||||
return;
|
dbTorrent.RdStatusRaw = torrent.RdStatusRaw;
|
||||||
}
|
dbTorrent.RdAdded = torrent.RdAdded;
|
||||||
|
dbTorrent.RdEnded = torrent.RdEnded;
|
||||||
dbTorrent.RdName = torrent.RdName;
|
dbTorrent.RdSpeed = torrent.RdSpeed;
|
||||||
dbTorrent.RdSize = torrent.RdSize;
|
dbTorrent.RdSeeders = torrent.RdSeeders;
|
||||||
dbTorrent.RdHost = torrent.RdHost;
|
dbTorrent.RdFiles = torrent.RdFiles;
|
||||||
dbTorrent.RdSplit = torrent.RdSplit;
|
|
||||||
dbTorrent.RdProgress = torrent.RdProgress;
|
|
||||||
dbTorrent.RdStatus = torrent.RdStatus;
|
|
||||||
dbTorrent.RdStatusRaw = torrent.RdStatusRaw;
|
|
||||||
dbTorrent.RdAdded = torrent.RdAdded;
|
|
||||||
dbTorrent.RdEnded = torrent.RdEnded;
|
|
||||||
dbTorrent.RdSpeed = torrent.RdSpeed;
|
|
||||||
dbTorrent.RdSeeders = torrent.RdSeeders;
|
|
||||||
|
|
||||||
if (torrent.Files != null)
|
|
||||||
{
|
|
||||||
dbTorrent.RdFiles = torrent.RdFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateCategory(Guid torrentId, String category)
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Update(Torrent torrent)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
{
|
{
|
||||||
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
return;
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbTorrent.Category = category;
|
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateComplete(Guid torrentId, DateTimeOffset? datetime)
|
dbTorrent.Category = torrent.Category;
|
||||||
{
|
dbTorrent.Priority = torrent.Priority;
|
||||||
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
dbTorrent.DownloadRetryAttempts = torrent.DownloadRetryAttempts;
|
||||||
|
dbTorrent.TorrentRetryAttempts = torrent.TorrentRetryAttempts;
|
||||||
dbTorrent.Completed = datetime;
|
dbTorrent.DeleteOnError = torrent.DeleteOnError;
|
||||||
|
dbTorrent.Lifetime = torrent.Lifetime;
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateCategory(Guid torrentId, String? category)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Delete(Guid torrentId)
|
dbTorrent.Category = category;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateComplete(Guid torrentId, String? error, DateTimeOffset? datetime, Boolean retry)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
{
|
{
|
||||||
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (dbTorrent == null)
|
if (String.IsNullOrWhiteSpace(error))
|
||||||
|
{
|
||||||
|
var downloads = await _dataContext.Downloads.AsNoTracking().Where(m => m.TorrentId == torrentId).ToListAsync();
|
||||||
|
var downloadWithErrors = downloads.Where(m => !String.IsNullOrWhiteSpace(m.Error)).ToList();
|
||||||
|
|
||||||
|
if (downloadWithErrors.Any())
|
||||||
{
|
{
|
||||||
return;
|
error = $"{downloadWithErrors.Count}/{downloads.Count} downloads failed with errors";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_dataContext.Torrents.Remove(dbTorrent);
|
if (!String.IsNullOrWhiteSpace(error) && retry)
|
||||||
|
{
|
||||||
|
if (dbTorrent.RetryCount < dbTorrent.TorrentRetryAttempts)
|
||||||
|
{
|
||||||
|
dbTorrent.RetryCount += 1;
|
||||||
|
dbTorrent.Retry = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await _dataContext.SaveChangesAsync();
|
dbTorrent.Completed = datetime;
|
||||||
|
dbTorrent.Error = error;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbTorrent.FilesSelected = datetime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdatePriority(Guid torrentId, Int32? priority)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbTorrent.Priority = priority;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateRetry(Guid torrentId, DateTimeOffset? dateTime, Int32 retryCount)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbTorrent.RetryCount = retryCount;
|
||||||
|
dbTorrent.Retry = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateError(Guid torrentId, String error)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbTorrent.Error = error;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Delete(Guid torrentId)
|
||||||
|
{
|
||||||
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dataContext.Torrents.Remove(dbTorrent);
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task VoidCache()
|
||||||
|
{
|
||||||
|
await TorrentCacheLock.WaitAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_torrentCache = null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TorrentCacheLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,26 +1,19 @@
|
||||||
using System.Threading.Tasks;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace RdtClient.Data.Data
|
namespace RdtClient.Data.Data;
|
||||||
|
|
||||||
|
public class UserData
|
||||||
{
|
{
|
||||||
public interface IUserData
|
private readonly DataContext _dataContext;
|
||||||
|
|
||||||
|
public UserData(DataContext dataContext)
|
||||||
{
|
{
|
||||||
Task<IdentityUser> GetUser();
|
_dataContext = dataContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserData : IUserData
|
public async Task<IdentityUser?> GetUser()
|
||||||
{
|
{
|
||||||
private readonly DataContext _dataContext;
|
return await _dataContext.Users.FirstOrDefaultAsync();
|
||||||
|
|
||||||
public UserData(DataContext dataContext)
|
|
||||||
{
|
|
||||||
_dataContext = dataContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IdentityUser> GetUser()
|
|
||||||
{
|
|
||||||
return await _dataContext.Users.FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using RdtClient.Data.Data;
|
using RdtClient.Data.Data;
|
||||||
|
using RdtClient.Data.Models.Internal;
|
||||||
|
|
||||||
namespace RdtClient.Data
|
namespace RdtClient.Data;
|
||||||
|
|
||||||
|
public static class DiConfig
|
||||||
{
|
{
|
||||||
public static class DiConfig
|
public static void Config(IServiceCollection services, AppSettings appSettings)
|
||||||
{
|
{
|
||||||
public static void Config(IServiceCollection services)
|
if (String.IsNullOrWhiteSpace(appSettings.Database?.Path))
|
||||||
{
|
{
|
||||||
services.AddScoped<IDownloadData, DownloadData>();
|
throw new Exception("Invalid database path found in appSettings");
|
||||||
services.AddScoped<ISettingData, SettingData>();
|
|
||||||
services.AddScoped<ITorrentData, TorrentData>();
|
|
||||||
services.AddScoped<IUserData, UserData>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var connectionString = $"Data Source={appSettings.Database.Path}";
|
||||||
|
services.AddDbContext<DataContext>(options => options.UseSqlite(connectionString));
|
||||||
|
|
||||||
|
services.AddScoped<DownloadData>();
|
||||||
|
services.AddScoped<SettingData>();
|
||||||
|
services.AddScoped<TorrentData>();
|
||||||
|
services.AddScoped<UserData>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
namespace RdtClient.Data.Enums;
|
||||||
{
|
|
||||||
public enum RealDebridStatus
|
|
||||||
{
|
|
||||||
Processing = 0,
|
|
||||||
WaitingForFileSelection = 1,
|
|
||||||
Downloading = 2,
|
|
||||||
Finished = 3,
|
|
||||||
|
|
||||||
Error = 99
|
public enum TorrentStatus
|
||||||
}
|
{
|
||||||
}
|
Processing = 0,
|
||||||
|
WaitingForFileSelection = 1,
|
||||||
|
Downloading = 2,
|
||||||
|
Finished = 3,
|
||||||
|
Uploading = 4,
|
||||||
|
|
||||||
|
Error = 99
|
||||||
|
}
|
||||||
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