Compare commits
No commits in common. "main" and "v2.0.5" have entirely different histories.
293 changed files with 29609 additions and 40473 deletions
|
|
@ -1,14 +0,0 @@
|
||||||
FROM mcr.microsoft.com/devcontainers/dotnet:2-10.0-noble
|
|
||||||
|
|
||||||
ARG NODE_MAJOR=22
|
|
||||||
ARG ANGULAR_CLI_VERSION=21
|
|
||||||
|
|
||||||
RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends nodejs sqlite3 \
|
|
||||||
&& npm install -g "@angular/cli@^${ANGULAR_CLI_VERSION}" \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN mkdir -p /data/db /data/downloads \
|
|
||||||
&& chown -R vscode:vscode /data
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
{
|
|
||||||
"name": "rdt-client",
|
|
||||||
"build": {
|
|
||||||
"dockerfile": "Dockerfile",
|
|
||||||
"args": {
|
|
||||||
"NODE_MAJOR": "22"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"remoteUser": "vscode",
|
|
||||||
"runArgs": ["--init"],
|
|
||||||
"containerEnv": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
|
||||||
"BASE_PATH": "",
|
|
||||||
"DOTNET_USE_POLLING_FILE_WATCHER": "1",
|
|
||||||
"CHOKIDAR_USEPOLLING": "true"
|
|
||||||
},
|
|
||||||
"forwardPorts": [4200, 6500],
|
|
||||||
"portsAttributes": {
|
|
||||||
"4200": {
|
|
||||||
"label": "Angular client",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
},
|
|
||||||
"6500": {
|
|
||||||
"label": "RdtClient backend",
|
|
||||||
"onAutoForward": "silent"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mounts": [
|
|
||||||
"source=rdtclient-data-db,target=/data/db,type=volume",
|
|
||||||
"source=rdtclient-data-downloads,target=/data/downloads,type=volume",
|
|
||||||
"source=rdtclient-nuget,target=/home/vscode/.nuget,type=volume",
|
|
||||||
"source=rdtclient-npm-cache,target=/home/vscode/.npm,type=volume"
|
|
||||||
],
|
|
||||||
"postCreateCommand": "/bin/bash .devcontainer/post-create.sh",
|
|
||||||
"postStartCommand": "/bin/bash .devcontainer/post-start.sh",
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
|
||||||
"ms-dotnettools.csharp",
|
|
||||||
"ms-dotnettools.csdevkit",
|
|
||||||
"Angular.ng-template",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"esbenp.prettier-vscode"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"dotnet.defaultSolution": "server/RdtClient.sln",
|
|
||||||
"terminal.integrated.defaultProfile.linux": "zsh"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
sudo install -d -m 0775 -o vscode -g vscode \
|
|
||||||
/data/db \
|
|
||||||
/data/downloads \
|
|
||||||
/home/vscode/.nuget \
|
|
||||||
/home/vscode/.nuget/NuGet \
|
|
||||||
/home/vscode/.nuget/packages \
|
|
||||||
/home/vscode/.npm
|
|
||||||
|
|
||||||
sudo chown -R vscode:vscode /data /home/vscode/.nuget /home/vscode/.npm
|
|
||||||
|
|
||||||
dotnet restore server/RdtClient.sln
|
|
||||||
npm install --prefix client
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
sudo install -d -m 0775 -o vscode -g vscode \
|
|
||||||
/data/db \
|
|
||||||
/data/downloads \
|
|
||||||
/home/vscode/.nuget \
|
|
||||||
/home/vscode/.nuget/NuGet \
|
|
||||||
/home/vscode/.nuget/packages \
|
|
||||||
/home/vscode/.npm
|
|
||||||
|
|
||||||
sudo chown -R vscode:vscode /data /home/vscode/.nuget /home/vscode/.npm
|
|
||||||
|
|
@ -37,4 +37,4 @@ server/**/.vs
|
||||||
server/RdtClient.Web/wwwroot/
|
server/RdtClient.Web/wwwroot/
|
||||||
|
|
||||||
build/
|
build/
|
||||||
publish/data
|
publish/
|
||||||
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Use this template to report actual bugs. If you are looking for general help, please post a discussion.
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
@ -9,12 +9,8 @@ assignees: ''
|
||||||
|
|
||||||
**What version are you using?**
|
**What version are you using?**
|
||||||
|
|
||||||
**What OS are you running?**
|
**Wat OS are you running?**
|
||||||
|
|
||||||
**Are you using Docker or as a service?**
|
**Are you using Docker or as a service?**
|
||||||
|
|
||||||
**Which debrid provider are you using?**
|
|
||||||
|
|
||||||
**Which downloader are you using?**
|
|
||||||
|
|
||||||
**Please attach a log file here with the log setting set to debug**
|
**Please attach a log file here with the log setting set to debug**
|
||||||
|
|
|
||||||
141
.github/workflows/build-docker-image.yml
vendored
141
.github/workflows/build-docker-image.yml
vendored
|
|
@ -1,141 +0,0 @@
|
||||||
name: Release Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
APP_NAME: rdtclient
|
|
||||||
DOCKER_FILE: ./Dockerfile
|
|
||||||
ENABLE_DOCKERHUB: 1
|
|
||||||
ENABLE_GHCR: 1
|
|
||||||
|
|
||||||
concurrency: ${{ github.sha }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
outputs:
|
|
||||||
digest-amd64: ${{ steps.digest.outputs.digest-amd64 }}
|
|
||||||
digest-arm64: ${{ steps.digest.outputs.digest-arm64 }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
config:
|
|
||||||
- arch: amd64
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
platform: linux/amd64
|
|
||||||
- arch: arm64
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
platform: linux/arm64
|
|
||||||
runs-on: ${{ matrix.config.runs-on }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Extract version and patch .csproj
|
|
||||||
run: |
|
|
||||||
VERSION="${GITHUB_REF_NAME#v}"
|
|
||||||
sed -i "s|<Version>.*</Version>|<Version>$VERSION</Version>|" server/RdtClient.Web/RdtClient.Web.csproj
|
|
||||||
sed -i "s|<AssemblyVersion>.*</AssemblyVersion>|<AssemblyVersion>$VERSION</AssemblyVersion>|" server/RdtClient.Web/RdtClient.Web.csproj
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
if: ${{ env.ENABLE_DOCKERHUB == 1 }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
|
||||||
if: ${{ env.ENABLE_GHCR == 1 }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Docker Metadata action
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}
|
|
||||||
ghcr.io/${{ github.repository_owner }}/${{ env.APP_NAME }}
|
|
||||||
|
|
||||||
- name: Build and push by digest
|
|
||||||
id: build
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}
|
|
||||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
|
||||||
|
|
||||||
- name: Export digest
|
|
||||||
id: digest
|
|
||||||
run: |
|
|
||||||
echo "digest-${{ matrix.config.arch }}=${{ steps.build.outputs.digest }}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
push-images:
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Login to DockerHub
|
|
||||||
if: ${{ env.ENABLE_DOCKERHUB == 1 }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
|
||||||
if: ${{ env.ENABLE_GHCR == 1 }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Setup Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Docker Metadata action
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}
|
|
||||||
ghcr.io/${{ github.repository_owner }}/${{ env.APP_NAME }}
|
|
||||||
tags: |
|
|
||||||
type=schedule
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
type=sha
|
|
||||||
|
|
||||||
- name: Create manifest list and push
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create \
|
|
||||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}') \
|
|
||||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}@${{ needs.build.outputs.digest-amd64 }} \
|
|
||||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}@${{ needs.build.outputs.digest-arm64 }}
|
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Docker Hub Registry Description
|
|
||||||
if: ${{ env.ENABLE_DOCKERHUB == 1 }}
|
|
||||||
uses: peter-evans/dockerhub-description@v4
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
repository: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}
|
|
||||||
short-description: |
|
|
||||||
A web interface to manage your torrents on debrid providers.
|
|
||||||
readme-filepath: ./README-DOCKER.md
|
|
||||||
99
.github/workflows/build-release.yaml
vendored
99
.github/workflows/build-release.yaml
vendored
|
|
@ -1,99 +0,0 @@
|
||||||
name: Create GitHub Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Test, Build, and Bundle
|
|
||||||
runs-on: windows-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: "lts/*"
|
|
||||||
|
|
||||||
- name: Set up .NET
|
|
||||||
uses: actions/setup-dotnet@v5
|
|
||||||
with:
|
|
||||||
dotnet-version: '10'
|
|
||||||
|
|
||||||
- name: Install Frontend Dependencies
|
|
||||||
working-directory: client
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Build Frontend
|
|
||||||
working-directory: client
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Restore Backend Dependencies
|
|
||||||
working-directory: server
|
|
||||||
run: dotnet restore
|
|
||||||
|
|
||||||
- name: Build and Publish Backend
|
|
||||||
working-directory: server
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$v = "${{ github.ref_name }}".TrimStart('v')
|
|
||||||
dotnet build -c Release --no-restore -p:Version=$v -p:AssemblyVersion=$v
|
|
||||||
dotnet publish RdtClient.Web/RdtClient.Web.csproj -c Release --no-build -p:Version=$v -p:AssemblyVersion=$v -o ../publish
|
|
||||||
|
|
||||||
- name: Create ZIP
|
|
||||||
shell: pwsh
|
|
||||||
run: Compress-Archive -Path publish\* -DestinationPath RealDebridClient.zip
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: RealDebridClient.zip
|
|
||||||
path: RealDebridClient.zip
|
|
||||||
|
|
||||||
release:
|
|
||||||
name: Create GitHub release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Strip 'v' from tag
|
|
||||||
id: version
|
|
||||||
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Get latest release info
|
|
||||||
id: query-release-info
|
|
||||||
uses: release-flow/keep-a-changelog-action@v2
|
|
||||||
with:
|
|
||||||
command: query
|
|
||||||
version: ${{ steps.version.outputs.version }}
|
|
||||||
|
|
||||||
- name: Download Artifact
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: RealDebridClient.zip
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ github.ref_name }}
|
|
||||||
name: ${{ github.ref_name }}
|
|
||||||
body: ${{ steps.query-release-info.outputs.release-notes }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
files: RealDebridClient.zip
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
24
.github/workflows/dotnet-test.yml
vendored
24
.github/workflows/dotnet-test.yml
vendored
|
|
@ -1,24 +0,0 @@
|
||||||
name: dotnet test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Setup .NET
|
|
||||||
uses: actions/setup-dotnet@v5
|
|
||||||
with:
|
|
||||||
dotnet-version: 10.0.x
|
|
||||||
- name: Restore dependencies
|
|
||||||
run: dotnet restore server
|
|
||||||
- name: Build
|
|
||||||
run: dotnet build --no-restore server
|
|
||||||
- name: Test
|
|
||||||
run: dotnet test --no-build --verbosity normal server
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -3,9 +3,4 @@
|
||||||
build/
|
build/
|
||||||
publish/
|
publish/
|
||||||
node_modules/
|
node_modules/
|
||||||
RealDebridClient.zip
|
RealDebridClient.zip
|
||||||
server/RdtClient.Web/appsettings.Development.json
|
|
||||||
data
|
|
||||||
Dockerfile.dev
|
|
||||||
test.bat
|
|
||||||
.DS_Store
|
|
||||||
708
CHANGELOG.md
708
CHANGELOG.md
|
|
@ -4,701 +4,6 @@ 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).
|
||||||
|
|
||||||
## [Unreleased
|
|
||||||
|
|
||||||
## [2.0.138] - 2026-06-05
|
|
||||||
### Fixed
|
|
||||||
- SABnzbd fixes.
|
|
||||||
|
|
||||||
## [2.0.137] - 2026-06-05
|
|
||||||
### Fixed
|
|
||||||
- Fix torbox save path mapping for single file downloads.
|
|
||||||
|
|
||||||
## [2.0.136] - 2026-05-30
|
|
||||||
### Added
|
|
||||||
- Added devcontainer development workflow.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Upgraded torbox.net dependency.
|
|
||||||
|
|
||||||
## [2.0.135] - 2026-05-27
|
|
||||||
### Changed
|
|
||||||
- When adding a torrent through the qBittorrent endpoints, it will wait to see if the torrent gets added properly or errors out, resulting in a better experience for infringing files and Sonarr / Radarr.
|
|
||||||
- Upgraded torbox.net dependency.
|
|
||||||
- Support SABnzbd API key auth, thanks to @ALenfant!
|
|
||||||
|
|
||||||
## [2.0.134] - 2026-05-22
|
|
||||||
### Changed
|
|
||||||
- Upgraded torbox.net dependency.
|
|
||||||
|
|
||||||
## [2.0.133] - 2026-05-17
|
|
||||||
### Added
|
|
||||||
- Added NZB support for Premiumize, thanks to @ALenfant!
|
|
||||||
|
|
||||||
## [2.0.132] - 2026-05-17
|
|
||||||
### Changed
|
|
||||||
- Fixed for Cleanuparr
|
|
||||||
- Upgrade Torbox.NET dependency
|
|
||||||
|
|
||||||
## [2.0.131] - 2026-05-14
|
|
||||||
### Changed
|
|
||||||
- Fixes to the rate limiting.
|
|
||||||
|
|
||||||
## [2.0.130] - 2026-04-28
|
|
||||||
### Changed
|
|
||||||
- Fixes to unit tests
|
|
||||||
- Upgraded .NET and Angular dependencies
|
|
||||||
- Added better qBittorrent capabilities for Cleanuparr
|
|
||||||
- Performance fixes
|
|
||||||
|
|
||||||
## [2.0.129] - 2026-04-06
|
|
||||||
### Added
|
|
||||||
- Optimized mobile views, thanks to @sylvaindd!
|
|
||||||
|
|
||||||
## [2.0.128] - 2026-04-06
|
|
||||||
### Changed
|
|
||||||
- Fixed some qBittorrent endpoints.
|
|
||||||
|
|
||||||
## [2.0.127] - 2026-03-15
|
|
||||||
### Added
|
|
||||||
- Implement qBittorrent endpoint for counting torrents, thanks to @vinodmishra!
|
|
||||||
|
|
||||||
## [2.0.126] - 2026-03-14
|
|
||||||
### Added
|
|
||||||
- qBittorrent hash filtering on the /torrrents/info endpoint, thanks to @jfrconley!
|
|
||||||
- Category select as a dropdown when adding a new torrent, thanks to @sylvaindd!
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Rate limiting fixes, thanks to @omgbeez!
|
|
||||||
- AllDebrid Symlink fixes, thanks to @AlexandreVassard!
|
|
||||||
|
|
||||||
## [2.0.125] - 2026-03-01
|
|
||||||
### Added
|
|
||||||
- Added extra qBittorrent endpoints for Cleanuparr compatibility.
|
|
||||||
|
|
||||||
## [2.0.124] - 2026-02-23
|
|
||||||
### Added
|
|
||||||
- Added /version/api for qBittorrent to support clients that still want to talk to the old API.
|
|
||||||
- Improved rate limiting handling.
|
|
||||||
|
|
||||||
## [2.0.123] - 2026-02-21
|
|
||||||
### Changed
|
|
||||||
- Reverted SharpCompress to 0.42.1 due to file locking issues.
|
|
||||||
|
|
||||||
## [2.0.122] - 2026-02-21
|
|
||||||
### Added
|
|
||||||
- Sort torrents by added date by default, remember sort order in the browser.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Fixed ETA calculations.
|
|
||||||
- Fixed Torrentbox statuses.
|
|
||||||
- Fixed download progress reports.
|
|
||||||
- Fixed NZB delete actions.
|
|
||||||
|
|
||||||
## [2.0.121] - 2026-02-18
|
|
||||||
### Added
|
|
||||||
- NZB/Usernet support thanks to @omgbeez!
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Make some qBittorrent endpoints accessible without authentication.
|
|
||||||
- Improved ETA calculations.
|
|
||||||
- Few performance improvements when the download page has a lot of torrents.
|
|
||||||
- Performance improvements for the sqlite connection.
|
|
||||||
|
|
||||||
## [2.0.120] - 2026-02-11
|
|
||||||
### Changed
|
|
||||||
- Upgrade to .NET 10.
|
|
||||||
- Upgrade to Angular 21.
|
|
||||||
|
|
||||||
## [2.0.119] - 2025-10-13
|
|
||||||
### Removed
|
|
||||||
- Removed internal downloader from the GUI.
|
|
||||||
|
|
||||||
## [2.0.118] - 2025-10-06
|
|
||||||
### Added
|
|
||||||
- Added some fake qBittorrent API calls for decluttarr.
|
|
||||||
|
|
||||||
## [2.0.117] - 2025-10-06
|
|
||||||
### Removed
|
|
||||||
- Removed internal downloader and migrated to the Bezzad downloader.
|
|
||||||
|
|
||||||
## [2.0.116] - 2025-08-04
|
|
||||||
### Added
|
|
||||||
- Added setting to ban certain trackers from being added. Will filter by the torrent source or announcement urls.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Upgraded to Angular 20.
|
|
||||||
|
|
||||||
## [2.0.115] - 2025-07-28
|
|
||||||
### Added
|
|
||||||
- Added setting to delay the finish action.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Make sure the Real-Debrid provider times out when trying to add a new torrent.
|
|
||||||
|
|
||||||
## [2.0.114] - 2025-06-21
|
|
||||||
### Added
|
|
||||||
- Add Select All functionality to the delete dialog in individual torrent screen, thanks @mentalblank
|
|
||||||
- Add setting to add a list of trackers (from a URL) to every torrent and magnet that's added to rdt-client, thanks @mentalblank
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- The `User-Agent` header is now set on all requests to debrid providers' APIs.
|
|
||||||
|
|
||||||
## [2.0.113] - 2025-05-22
|
|
||||||
### Fixed
|
|
||||||
- Revert Synolog.Api.Client because of breaking changes.
|
|
||||||
|
|
||||||
## [2.0.112] - 2025-05-18
|
|
||||||
### Added
|
|
||||||
- Add ability to disable the built in unpacking process by setting the "Maximum unpack processes" to 0.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Upgraded Angular to use control flow.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Fixed dequeing issue.
|
|
||||||
- Fixed logging handler for ProviderUpdater.
|
|
||||||
|
|
||||||
## [2.0.111] - 2025-05-03
|
|
||||||
### Added
|
|
||||||
- Added button to register rdt-client as a handler for magnet links on [supported browsers](https://caniuse.com/mdn-api_navigator_registerprotocolhandler_scheme_parameter_magnet).
|
|
||||||
|
|
||||||
## [2.0.110] - 2025-04-24
|
|
||||||
### Fixed
|
|
||||||
- Fixed build number in the app.
|
|
||||||
|
|
||||||
## [2.0.109] - 2025-04-23
|
|
||||||
### Fixed
|
|
||||||
- Debrid Queue fixes (don't auto delete queued torrents, handle errors when dequeueing).
|
|
||||||
- Censor download station password when logging settings at startup.
|
|
||||||
- Set `HostDownloadAction` when auto-importing torrents.
|
|
||||||
- Build GitHub release .zip on windows not linux.
|
|
||||||
- Use arm GitHub Actions runner to build arm docker image.
|
|
||||||
|
|
||||||
## [2.0.108] - 2025-04-13
|
|
||||||
### Fixed
|
|
||||||
- Fixed websocket UI updating.
|
|
||||||
|
|
||||||
## [2.0.107] - 2025-04-13
|
|
||||||
### Fixed
|
|
||||||
- Fixed Docker release versioning.
|
|
||||||
|
|
||||||
## [2.0.106] - 2025-04-13
|
|
||||||
### Fixed
|
|
||||||
- Changed how the GitHub release is created and how the changelog is generated.
|
|
||||||
|
|
||||||
## [2.0.105] - 2025-04-13
|
|
||||||
### Added
|
|
||||||
- Add feature to limit the amount of torrents that get sent to the provider at the same time.
|
|
||||||
### Fixed
|
|
||||||
- Moved the websocket update process to its own background thread to improve UI update consistency.
|
|
||||||
|
|
||||||
## [2.0.104] - 2025-04-12
|
|
||||||
### Fixed
|
|
||||||
- Update the version number
|
|
||||||
|
|
||||||
## [2.0.103] - 2025-04-12
|
|
||||||
### Added
|
|
||||||
- Button to select all options when deleting a torrent, thanks @EugeneKallis
|
|
||||||
- Add setting to ignore update notifications. A notification will appear regardless of this setting if any GitHub Security Advisories are published in this repo.
|
|
||||||
### Changed
|
|
||||||
- Download .zip of torrent files from TorBox when possible, thanks @asylumexp
|
|
||||||
- Users of AllDebrid and RealDebrid will now have no files downloaded when all files are excluded by filters. Before, if all files were excluded, rdt-client would download all the files in the torrent.
|
|
||||||
- Reduce number of calls to debrid provider API when no torrents need updating
|
|
||||||
### Fixed
|
|
||||||
- The dropdown navigation menu on mobile will now close when you navigate to another page
|
|
||||||
- Long torrent names without spaces will now wrap across lines
|
|
||||||
### Security
|
|
||||||
- Require auth to change debrid api key
|
|
||||||
|
|
||||||
## [2.0.102] - 2025-03-07
|
|
||||||
### Changed
|
|
||||||
- Fixed Angular build for Docker.
|
|
||||||
|
|
||||||
## [2.0.101] - 2025-03-07
|
|
||||||
### Changed
|
|
||||||
- Fixed Angular build (again).
|
|
||||||
|
|
||||||
## [2.0.100] - 2025-03-07
|
|
||||||
### Changed
|
|
||||||
- Fixed Angular build.
|
|
||||||
|
|
||||||
## [2.0.99] - 2025-03-07
|
|
||||||
### Security fix
|
|
||||||
- The Api/Authentication/Update was not protected by authentication, meaning everyone could reset your password and gain access.
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Set the useragent for the Bezadd downloader to avoid getting blacklisted by Torbox.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Upgraded to Angular 19.
|
|
||||||
- Upgraded to Torbox 1.5.
|
|
||||||
|
|
||||||
|
|
||||||
## [2.0.98] - 2025-02-16
|
|
||||||
### Added
|
|
||||||
- Added unit tests, thanks @Cucumberrbob!
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Fixed symlinks for AllDebrid.
|
|
||||||
- Upgraded DebridLink.fr to the latest version.
|
|
||||||
- Fixed nested files in the Premiumize Provider.
|
|
||||||
- Fixed deleting of torrents in the watch folders.
|
|
||||||
|
|
||||||
## [2.0.97] - 2025-02-16
|
|
||||||
### Added
|
|
||||||
- Added support for DebridLink.fr.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Fixed for the internal downloader.
|
|
||||||
- Added a column for torrent add date.
|
|
||||||
- Upgraded AllDebrid API.
|
|
||||||
|
|
||||||
## [2.0.96] - 2025-01-29
|
|
||||||
### Added
|
|
||||||
- Added support for the synology download manager.
|
|
||||||
- Added a column for torrent add date.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Fixed for the Symlink downloader and AllDebrid.
|
|
||||||
- Fixed setting the downloader when adding a torrent through the GUI.
|
|
||||||
|
|
||||||
## [2.0.95] - 2025-01-19
|
|
||||||
### Added
|
|
||||||
- Added the /api/v2/transfer/info qBittorrent endpoint.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- AllDebrid Symlink path fixes.
|
|
||||||
|
|
||||||
## [2.0.94] - 2025-01-05
|
|
||||||
### Changed
|
|
||||||
- AllDebrid path fixes.
|
|
||||||
|
|
||||||
## [2.0.93] - 2025-01-03
|
|
||||||
### Changed
|
|
||||||
- Torbox fixes.
|
|
||||||
|
|
||||||
## [2.0.92] - 2024-12-18
|
|
||||||
### Changed
|
|
||||||
- Torbox fixes.
|
|
||||||
|
|
||||||
## [2.0.91] - 2024-12-11
|
|
||||||
### Changed
|
|
||||||
- Torbox fixes.
|
|
||||||
|
|
||||||
## [2.0.90] - 2024-12-06
|
|
||||||
### Changed
|
|
||||||
- Download individual files from Torbox instead of a zip file.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Removed ability to select instant files from AllDebrid.
|
|
||||||
|
|
||||||
## [2.0.89] - 2024-11-24
|
|
||||||
### Changed
|
|
||||||
- Disabled selecting of files as Real-Debrid was the only provider that supported that.
|
|
||||||
|
|
||||||
## [2.0.88] - 2024-11-24
|
|
||||||
### Changed
|
|
||||||
- Catch disabled instant availability endpoint from Real Debrid.
|
|
||||||
|
|
||||||
## [2.0.87] - 2024-11-18
|
|
||||||
### Added
|
|
||||||
- Torbox support.
|
|
||||||
- qBittorrent API authentication when no authentication is used.
|
|
||||||
### Changed
|
|
||||||
- .NET version changed to .NET 9.
|
|
||||||
- Changed download limit to be split by active torrents.
|
|
||||||
|
|
||||||
## [2.0.86] - 2024-09-03
|
|
||||||
### Changed
|
|
||||||
- Add potential fix for BASE_PATH.
|
|
||||||
- Fixed creation of empty folders.
|
|
||||||
|
|
||||||
## [2.0.85] - 2024-09-03
|
|
||||||
### Changed
|
|
||||||
- Reverted: Prevent the creation of download folders when using symlink.
|
|
||||||
|
|
||||||
## [2.0.84] - 2024-09-02
|
|
||||||
### Changed
|
|
||||||
- Replace docker libssl1.1 with libssl3
|
|
||||||
|
|
||||||
## [2.0.83] - 2024-09-02
|
|
||||||
### Changed
|
|
||||||
- Fixed progress reporting to the qBittorrent API endpoint.
|
|
||||||
- Prevent the creation of download folders when using symlink.
|
|
||||||
|
|
||||||
## [2.0.82] - 2024-09-02
|
|
||||||
### Changed
|
|
||||||
- Update packages and docker alpine version.
|
|
||||||
|
|
||||||
## [2.0.81] - 2024-07-28
|
|
||||||
### Changed
|
|
||||||
- Improved handling of infringed torrents from real debrid.
|
|
||||||
- Force update of torrent data from real-debrid when no filename is found in the local DB.
|
|
||||||
- Fixed real-debrid deserialization issue when checking for instant available files.
|
|
||||||
|
|
||||||
## [2.0.80] - 2024-07-13
|
|
||||||
### Changed
|
|
||||||
- Add rate limiter to retry requests that are rate limited from Real-Debrid.
|
|
||||||
- Optimize calls to Real-Debrid API when torrents are finished and periodic updates.
|
|
||||||
- Update to .NET 8.0.7
|
|
||||||
|
|
||||||
## [2.0.79] - 2024-06-03
|
|
||||||
### Changed
|
|
||||||
- Fixed issue with qBittorrent progress sometimes throwing errors.
|
|
||||||
|
|
||||||
## [2.0.78] - 2024-05-04
|
|
||||||
### Changed
|
|
||||||
- Fixed Aria2c download path issue when a category is set.
|
|
||||||
|
|
||||||
## [2.0.77] - 2024-05-03
|
|
||||||
### Changed
|
|
||||||
- Fixed Aria2c download path issue when a category is set.
|
|
||||||
|
|
||||||
## [2.0.76] - 2024-05-02
|
|
||||||
### Changed
|
|
||||||
- Fixed issues with the qBittorrent endpoint.
|
|
||||||
- Fixed issue that could crash the torrent runner.
|
|
||||||
|
|
||||||
## [2.0.75] - 2024-04-24
|
|
||||||
### Changed
|
|
||||||
- Fixed broken recursive symlink searching.
|
|
||||||
|
|
||||||
## [2.0.74] - 2024-04-20
|
|
||||||
### Added
|
|
||||||
- Added support for symlink recursive searching.
|
|
||||||
|
|
||||||
## [2.0.73] - 2024-04-11
|
|
||||||
### Changed
|
|
||||||
- Fixed another issue with the symlinker and file resolver.
|
|
||||||
|
|
||||||
## [2.0.72] - 2024-04-10
|
|
||||||
### Changed
|
|
||||||
- Fixed issue with download speed test when the symlink downloader is selected.
|
|
||||||
|
|
||||||
## [2.0.71] - 2024-04-10
|
|
||||||
### Changed
|
|
||||||
- Fixed symlink path matching bug.
|
|
||||||
|
|
||||||
## [2.0.70] - 2024-04-10
|
|
||||||
### Added
|
|
||||||
- Added symlink logging.
|
|
||||||
|
|
||||||
## [2.0.69] - 2024-04-09
|
|
||||||
### Added
|
|
||||||
- Added sorting to the GUI columns.
|
|
||||||
### Changed
|
|
||||||
- Fixed reloading on the /settings and other pages.
|
|
||||||
|
|
||||||
## [2.0.68] - 2024-04-09
|
|
||||||
### Changed
|
|
||||||
- Base Href middleware fix that throws error when a response is not 200.
|
|
||||||
|
|
||||||
## [2.0.67] - 2024-04-09
|
|
||||||
### Changed
|
|
||||||
- Symlink fixes.
|
|
||||||
|
|
||||||
## [2.0.66] - 2024-04-08
|
|
||||||
### Changed
|
|
||||||
- Symlink fixes and improvements.
|
|
||||||
|
|
||||||
## [2.0.65] - 2024-04-07
|
|
||||||
### Added
|
|
||||||
- Added option to configure the buffersize for the internal downloader.
|
|
||||||
|
|
||||||
## [2.0.64] - 2024-04-06
|
|
||||||
### Added
|
|
||||||
- Add log level Verbose and add logging for the internal downloader, only works when both log levels are set to Verbose.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Add fixes for the symlink downloader
|
|
||||||
- Add better indication when a torrent is stalled
|
|
||||||
- Fixed download client selection on the torrents
|
|
||||||
|
|
||||||
## [2.0.63] - 2024-03-05
|
|
||||||
### Changed
|
|
||||||
- When Sonarr/Radarr requests a torrent to be deleted, and its files too, then delete those files instead of ingoring it.
|
|
||||||
|
|
||||||
## [2.0.62] - 2024-02-17
|
|
||||||
### Changed
|
|
||||||
- Fixed reporting a torrent as error when some downloads have failed but still need to be retried.
|
|
||||||
- Fixed issue where downloads could get started over and over.
|
|
||||||
|
|
||||||
## [2.0.61] - 2024-01-21
|
|
||||||
### Added
|
|
||||||
- Added setting to include or exclude files based on a given regex.
|
|
||||||
- Add logging.
|
|
||||||
|
|
||||||
## [2.0.60] - 2024-01-21
|
|
||||||
### Changed
|
|
||||||
- Fixed bug where downloads could get stuck in active state while deleted.
|
|
||||||
- Use the RD folder structure when downloading a file.
|
|
||||||
|
|
||||||
## [2.0.59] - 2024-01-21
|
|
||||||
### Changed
|
|
||||||
- Added the simple downloader back and moved the current internal downloader to a new setting.
|
|
||||||
|
|
||||||
## [2.0.58] - 2024-01-10
|
|
||||||
### Changed
|
|
||||||
- Don't automatically count the chunk count.
|
|
||||||
|
|
||||||
## [2.0.57] - 2024-01-09
|
|
||||||
### Changed
|
|
||||||
- Fixed symlink retry.
|
|
||||||
|
|
||||||
## [2.0.56] - 2024-01-07
|
|
||||||
### Changed
|
|
||||||
- Add retry mechanism for the downloaders.
|
|
||||||
|
|
||||||
## [2.0.55] - 2024-01-07
|
|
||||||
### Changed
|
|
||||||
- Tweaked the internal downloader to prevent memory issues.
|
|
||||||
- Add retry mechanism for the symlink downloader.
|
|
||||||
|
|
||||||
## [2.0.54] - 2024-01-07
|
|
||||||
### Changed
|
|
||||||
- Added some logging for the symlink downloader to troubleshoot.
|
|
||||||
- Added some logging when deleting torrents and the symlinker overrides the finish action.
|
|
||||||
|
|
||||||
## [2.0.53] - 2024-01-05
|
|
||||||
### Added
|
|
||||||
- Add setting to set the download path on the aria2 instance.
|
|
||||||
|
|
||||||
## [2.0.52] - 2024-01-05
|
|
||||||
### Added
|
|
||||||
- Add BASE_PATH environment variable for the base path setting.
|
|
||||||
- Expose the Post Torrent Download Action setting on the Provider settings.
|
|
||||||
|
|
||||||
## [2.0.51] - 2024-01-05
|
|
||||||
### Added
|
|
||||||
- Added setting to store magnets and torrents to a directory after adding.
|
|
||||||
- Added bulk settings change on the index pages.
|
|
||||||
### Changed
|
|
||||||
- Swapped the internal downloader back to the one that was before, this one is giving too many headaches.
|
|
||||||
- Prevent deleting torrents from the debrid provider when the symlink downloader is used.
|
|
||||||
- Fixed %F parameter on the external program.
|
|
||||||
- Run the external program before the deletion process is ran.
|
|
||||||
- Remove the 100 char limit on inputs.
|
|
||||||
|
|
||||||
## [2.0.50] - 2023-11-25
|
|
||||||
### Changed
|
|
||||||
- Fixed Docker Builds for arm64.
|
|
||||||
|
|
||||||
## [2.0.49] - 2023-11-24
|
|
||||||
### Changed
|
|
||||||
- Fixed memory issue in internal downloader.
|
|
||||||
- Changed unpack process to handle cancels.
|
|
||||||
|
|
||||||
## [2.0.48] - 2023-11-15
|
|
||||||
### Changed
|
|
||||||
- Reverted dockerfile again as the packagemanager still doesn't have .net 8.
|
|
||||||
|
|
||||||
## [2.0.47] - 2023-11-15
|
|
||||||
### Changed
|
|
||||||
- Changed docker to use the package manager again.
|
|
||||||
|
|
||||||
## [2.0.46] - 2023-11-15
|
|
||||||
### Changed
|
|
||||||
- Fix in internal downloader.
|
|
||||||
|
|
||||||
## [2.0.45] - 2023-11-15
|
|
||||||
### Changed
|
|
||||||
- Optimizations to the internal downloader.
|
|
||||||
|
|
||||||
## [2.0.44] - 2023-11-15
|
|
||||||
### Changed
|
|
||||||
- Revert broken upgrade.
|
|
||||||
|
|
||||||
## [2.0.43] - 2023-11-15
|
|
||||||
### Changed
|
|
||||||
- Improvements to the internal downloader.
|
|
||||||
|
|
||||||
## [2.0.42] - 2023-11-14
|
|
||||||
### Changed
|
|
||||||
- Fixed docker build as .NET8 isn't published yet.
|
|
||||||
|
|
||||||
## [2.0.41] - 2023-11-14
|
|
||||||
### Changed
|
|
||||||
- Upgraded to .NET8 to see if downloader perf improves.
|
|
||||||
|
|
||||||
## [2.0.40] - 2023-10-03
|
|
||||||
### Changed
|
|
||||||
- Symlink downloader fixes.
|
|
||||||
|
|
||||||
## [2.0.39] - 2023-09-21
|
|
||||||
### Changed
|
|
||||||
- Fixed docker build
|
|
||||||
|
|
||||||
## [2.0.38] - 2023-09-20
|
|
||||||
### Added
|
|
||||||
- Added Symlink downloader to allow the use of rclone.
|
|
||||||
- Added extra logging.
|
|
||||||
- Added bulk retry on the index.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Lower password requirements
|
|
||||||
|
|
||||||
## [2.0.37] - 2023-08-03
|
|
||||||
### Changed
|
|
||||||
- Fixed docker build process.
|
|
||||||
|
|
||||||
## [2.0.36] - 2023-08-02
|
|
||||||
### Changed
|
|
||||||
- Fixed docker build process and upgraded to Node18.
|
|
||||||
|
|
||||||
## [2.0.35] - 2023-08-02
|
|
||||||
### Removed
|
|
||||||
- Docker arm32/v7 images because the base image does not support it anymore: https://www.linuxserver.io/blog/a-farewell-to-arm-hf
|
|
||||||
|
|
||||||
## [2.0.34] - 2023-08-01
|
|
||||||
### Changed
|
|
||||||
- Update docker image to alpine 3.14.
|
|
||||||
|
|
||||||
## [2.0.33] - 2023-08-01
|
|
||||||
### Changed
|
|
||||||
- Fixed sub folders on Premiumize.
|
|
||||||
- Fixed serialization errors on Premiumize.
|
|
||||||
- Filter illegal path and filename characters when setting the download paths.
|
|
||||||
### Added
|
|
||||||
- Add the option "Post Download Action" to the Torrent settings popup.
|
|
||||||
- Add a 2nd "Add Torrent" button on the add torrent page.
|
|
||||||
- Add the Aria2c downloader to the Docker container and set it as the default downloader when running in docker.
|
|
||||||
|
|
||||||
## [2.0.32] - 2023-06-27
|
|
||||||
### Changed
|
|
||||||
- Fixed the BaseURL content-length setting.
|
|
||||||
|
|
||||||
## [2.0.31] - 2023-06-11
|
|
||||||
### Added
|
|
||||||
- Added setting to set the BaseURL.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Added some logging.
|
|
||||||
|
|
||||||
## [2.0.30] - 2023-04-09
|
|
||||||
### Changed
|
|
||||||
- Improved the internal downloader bandwidth limiter.
|
|
||||||
|
|
||||||
## [2.0.29] - 2023-04-09
|
|
||||||
### Changed
|
|
||||||
- Testing Github actions automatic build to Docker.
|
|
||||||
|
|
||||||
## [2.0.28] - 2023-04-09
|
|
||||||
### Changed
|
|
||||||
- Added link to repository from the version link.
|
|
||||||
|
|
||||||
## [2.0.27] - 2023-04-08
|
|
||||||
### Changed
|
|
||||||
- Fixed Premiumize selecting of files.
|
|
||||||
- Updated internal downloader to report download speeds better.
|
|
||||||
|
|
||||||
## [2.0.26] - 2023-03-30
|
|
||||||
### Changed
|
|
||||||
- Add some logging for Premiumize to better understand errors.
|
|
||||||
- Changed Premiumize to download each file individually instead of creating a zip first.
|
|
||||||
- Swapped the internal downloader for a new version.
|
|
||||||
- Fixed issue when switching providers and it not switching over properly.
|
|
||||||
|
|
||||||
## [2.0.25] - 2023-03-17
|
|
||||||
### Changed
|
|
||||||
- Fixed docker run issues.
|
|
||||||
|
|
||||||
## [2.0.24] - 2023-03-16
|
|
||||||
### Changed
|
|
||||||
- Fixed docker run issues.
|
|
||||||
|
|
||||||
## [2.0.23] - 2023-03-15
|
|
||||||
### Changed
|
|
||||||
- Fixed docker build issues.
|
|
||||||
|
|
||||||
## [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
|
## [2.0.5] - 2022-01-11
|
||||||
### Changed
|
### Changed
|
||||||
- Updated AllDebrid provider to fix issue with ID's not being a number.
|
- Updated AllDebrid provider to fix issue with ID's not being a number.
|
||||||
|
|
@ -981,3 +286,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Added
|
### Added
|
||||||
- First release
|
- First release
|
||||||
- Add unraring progress and default auto download / auto remove options.
|
- Add unraring progress and default auto download / auto remove options.
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/rogerfar/rdt-client/compare/1.5.5...HEAD
|
||||||
|
[1.5.5]: https://github.com/rogerfar/rdt-client/releases/tag/1.5.5
|
||||||
|
[1.5.4]: https://github.com/rogerfar/rdt-client/releases/tag/1.5.4
|
||||||
|
[1.5.3]: https://github.com/rogerfar/rdt-client/releases/tag/1.5.3
|
||||||
|
[1.5.2]: https://github.com/rogerfar/rdt-client/releases/tag/1.5.2
|
||||||
|
[1.5.1]: https://github.com/rogerfar/rdt-client/releases/tag/1.5.1
|
||||||
|
[1.5.0]: https://github.com/rogerfar/rdt-client/releases/tag/1.5
|
||||||
|
[1.4.0]: https://github.com/rogerfar/rdt-client/releases/tag/1.4
|
||||||
|
[1.3.0]: https://github.com/rogerfar/rdt-client/releases/tag/1.3
|
||||||
|
[1.2.0]: https://github.com/rogerfar/rdt-client/releases/tag/1.2
|
||||||
|
[1.1.0]: https://github.com/rogerfar/rdt-client/releases/tag/1.1
|
||||||
|
[1.0.0]: https://github.com/rogerfar/rdt-client/releases/tag/v1.0
|
||||||
83
Dockerfile
83
Dockerfile
|
|
@ -1,5 +1,5 @@
|
||||||
# Stage 1 - Build the frontend
|
# Stage 1 - Build the frontend
|
||||||
FROM node:lts-alpine 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
|
||||||
|
|
@ -8,20 +8,18 @@ ENV BUILDPLATFORM=${BUILDPLATFORM:-linux/amd64}
|
||||||
RUN mkdir /appclient
|
RUN mkdir /appclient
|
||||||
WORKDIR /appclient
|
WORKDIR /appclient
|
||||||
|
|
||||||
RUN apk add --no-cache git python3 py3-pip make g++
|
|
||||||
|
|
||||||
COPY client ./client
|
|
||||||
COPY root ./root
|
|
||||||
RUN \
|
RUN \
|
||||||
|
echo "**** Cloning Source Code ****" && \
|
||||||
|
git clone https://github.com/rogerfar/rdt-client.git . && \
|
||||||
cd client && \
|
cd client && \
|
||||||
echo "**** Building Code ****" && \
|
echo "**** Building Code ****" && \
|
||||||
npm install && \
|
npm ci && \
|
||||||
npx ng build --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:10.0-alpine 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
|
||||||
|
|
@ -30,16 +28,24 @@ ENV BUILDPLATFORM=${BUILDPLATFORM:-linux/amd64}
|
||||||
RUN mkdir /appserver
|
RUN mkdir /appserver
|
||||||
WORKDIR /appserver
|
WORKDIR /appserver
|
||||||
|
|
||||||
COPY server ./server
|
|
||||||
RUN \
|
RUN \
|
||||||
|
echo "**** Cloning Source Code ****" && \
|
||||||
|
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 && \
|
||||||
dotnet restore --no-cache RdtClient.sln && \
|
if [ "$TARGETPLATFORM" = "linux/arm/v7" ] ; then \
|
||||||
dotnet test && \
|
echo "**** Building $TARGETPLATFORM arm v7 version" && \
|
||||||
dotnet publish --no-restore -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 \
|
||||||
|
echo "**** Building $TARGETPLATFORM x64 version" && \
|
||||||
|
dotnet restore RdtClient.sln && dotnet publish -c Release -o out ; \
|
||||||
|
fi
|
||||||
|
|
||||||
# Stage 3 - Build runtime image
|
# Stage 3 - Build runtime image
|
||||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.20
|
FROM ghcr.io/linuxserver/baseimage-ubuntu:focal
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
|
|
@ -57,44 +63,33 @@ ENV XDG_CONFIG_HOME="/config/xdg"
|
||||||
ENV RDTCLIENT_BRANCH="main"
|
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 ****" && \
|
||||||
apk update && \
|
apt-get update -y -qq && \
|
||||||
echo "**** Install pre-reqs ****" && \
|
echo "**** Install pre-reqs ****" && \
|
||||||
apk add bash icu-libs krb5-libs libgcc libintl libssl3 libstdc++ zlib && \
|
apt-get install -y -qq wget dos2unix && \
|
||||||
echo "**** Installing dotnet ****" && \
|
apt-get install -y libc6 libgcc1 libgssapi-krb5-2 libssl1.1 libstdc++6 zlib1g libicu66 && \
|
||||||
mkdir -p /usr/share/dotnet
|
echo "**** Installing dotnet ****" && \
|
||||||
|
wget -q https://dot.net/v1/dotnet-install.sh && \
|
||||||
RUN \
|
chmod +x ./dotnet-install.sh && \
|
||||||
if [ "$TARGETPLATFORM" = "linux/arm/v7" ] ; then \
|
bash ./dotnet-install.sh -c Current --runtime dotnet --install-dir /usr/share/dotnet && \
|
||||||
wget https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/10.0.0/aspnetcore-runtime-10.0.0-linux-musl-arm.tar.gz && \
|
bash ./dotnet-install.sh -c Current --runtime aspnetcore --install-dir /usr/share/dotnet && \
|
||||||
tar zxf aspnetcore-runtime-10.0.0-linux-musl-arm.tar.gz -C /usr/share/dotnet ; \
|
echo "**** Cleaning image ****" && \
|
||||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ] ; then \
|
apt-get -y -qq -o Dpkg::Use-Pty=0 clean && apt-get -y -qq -o Dpkg::Use-Pty=0 purge && \
|
||||||
wget https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/10.0.0/aspnetcore-runtime-10.0.0-linux-musl-arm64.tar.gz && \
|
echo "**** Setting permissions ****" && \
|
||||||
tar zxf aspnetcore-runtime-10.0.0-linux-musl-arm64.tar.gz -C /usr/share/dotnet ; \
|
chown -R abc:abc /data && \
|
||||||
else \
|
rm -rf \
|
||||||
wget https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/10.0.0/aspnetcore-runtime-10.0.0-linux-musl-x64.tar.gz && \
|
/tmp/* \
|
||||||
tar zxf aspnetcore-runtime-10.0.0-linux-musl-x64.tar.gz -C /usr/share/dotnet ; \
|
/var/lib/apt/lists/* \
|
||||||
fi
|
/var/tmp/* || true
|
||||||
|
|
||||||
RUN \
|
|
||||||
echo "**** Setting permissions ****" && \
|
|
||||||
chown -R abc:abc /data && \
|
|
||||||
rm -rf \
|
|
||||||
/tmp/* \
|
|
||||||
/var/cache/apk/* \
|
|
||||||
/var/tmp/* || true
|
|
||||||
|
|
||||||
ENV PATH "$PATH:/usr/share/dotnet"
|
ENV PATH "$PATH:/usr/share/dotnet"
|
||||||
|
|
||||||
# Copy files for app
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=dotnet-build-env /appserver/server/out .
|
COPY --from=dotnet-build-env /appserver/server/out .
|
||||||
COPY --from=node-build-env /appclient/client/out/browser ./wwwroot
|
COPY --from=node-build-env /appclient/client/out ./wwwroot
|
||||||
COPY --from=node-build-env /appclient/root/ /
|
COPY --from=node-build-env /appclient/root/ /
|
||||||
|
|
||||||
# ports and volumes
|
# ports and volumes
|
||||||
EXPOSE 6500
|
EXPOSE 6500
|
||||||
|
VOLUME ["/config", "/data" ]
|
||||||
# Check Status
|
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD curl --fail http://localhost:6500 || exit
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ rdt-client is a web a web interface to manage your torrents on Real-Debrid. It s
|
||||||
|
|
||||||
Our images support multiple architectures such as `x86-64`, `arm64` and `armhf`. We utilise the docker manifest for multi-platform awareness. More information is available from docker [here](https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-2.md#manifest-list) and our announcement [here](https://blog.linuxserver.io/2019/02/21/the-lsio-pipeline-project/).
|
Our images support multiple architectures such as `x86-64`, `arm64` and `armhf`. We utilise the docker manifest for multi-platform awareness. More information is available from docker [here](https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-2.md#manifest-list) and our announcement [here](https://blog.linuxserver.io/2019/02/21/the-lsio-pipeline-project/).
|
||||||
|
|
||||||
Simply pulling `rogerfar/rdtclient` should retrieve the correct image for your arch, but you can also pull specific arch images via tags.
|
Simply pulling `rogerfar/rdt-client` should retrieve the correct image for your arch, but you can also pull specific arch images via tags.
|
||||||
|
|
||||||
The architectures supported by this image are:
|
The architectures supported by this image are:
|
||||||
|
|
||||||
|
|
|
||||||
242
README.md
242
README.md
|
|
@ -1,101 +1,80 @@
|
||||||
# Real-Debrid Torrent & Usenet Client
|
# Real-Debrid Torrent Client
|
||||||
|
|
||||||
This is a web interface to manage your torrents on Real-Debrid, AllDebrid, Premiumize, TorBox or DebridLink. It supports the following features:
|
This is a web interface to manage your torrents on Real-Debrid or AllDebrid. It supports the following features:
|
||||||
|
|
||||||
- Add new torrents through magnets or files
|
- Add new torrents through magnets or files
|
||||||
- Add usenet downloads through NZB files (TorBox and Premiumize only)
|
- Download all files from Real-Debrid or AllDebrid to your local machine automatically
|
||||||
- Download all files from Real-Debrid, AllDebrid, Premiumize or TorBox 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, Radarr or Couchpotato.
|
- Implements a fake qBittorrent API so you can hook up other applications like Sonarr or Couchpotato.
|
||||||
- Built with Angular 21 and .NET 10
|
- Built with Angular 13 and .NET 6
|
||||||
|
|
||||||
**You will need a Premium service at Real-Debrid, AllDebrid, Premiumize, Torbox or DebridLink!**
|
**You will need a Premium service at Real-Debrid or AllDebrid!**
|
||||||
|
|
||||||
[Click here to sign up for Real-Debrid.](https://real-debrid.com/?id=1348683)
|
[Click here to sign up at 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 AllDebrid.](https://real-debrid.com/?id=1348683)
|
||||||
|
|
||||||
[Click here to sign up for Premiumize.](https://www.premiumize.me/)
|
|
||||||
|
|
||||||
[Click here to sign up for TorBox.](https://torbox.app/subscription?referral=3d25018e-f30d-4c4b-a714-48c04bc76765)
|
|
||||||
|
|
||||||
[Click here to sign up for DebridLink.](https://debrid-link.fr/id/6duif)
|
|
||||||
|
|
||||||
<sub>(referal links so I can get a few free premium days)</sub>
|
<sub>(referal links so I can get a few free premium days)</sub>
|
||||||
|
|
||||||
## Docker Setup
|
## Docker Setup
|
||||||
|
|
||||||
Please see our separate Docker setup Read Me.
|
You can run the docker container on Windows, Linux. To get started either use _Docker Run_ or _Docker Compose_.
|
||||||
|
|
||||||
[Readme for Docker](README-DOCKER.md)
|
### Docker Run
|
||||||
|
|
||||||
## Run as a Service
|
```console
|
||||||
|
docker run --pull=always
|
||||||
|
--volume /your/download/path/:/data/downloads \
|
||||||
|
--volume /your/storage/path/:/data/db \
|
||||||
|
--log-driver json-file \
|
||||||
|
--log-opt max-size=10m \
|
||||||
|
-p 6500:6500 \
|
||||||
|
--name rdtclient \
|
||||||
|
rogerfar/rdtclient:latest
|
||||||
|
```
|
||||||
|
|
||||||
Instead of running in Docker you can install it as a service in Windows or Linux.
|
Replace `/your/download/path/` with your local path to download files to. For Windows i.e. `C:/Downloads`.
|
||||||
|
Replace `/your/storage/path/` with your local path to store persistent database and log files in. For Windows i.e. `C:/Docker/rdt-client`.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
You can use the provided docker compose to run:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
rdtclient:
|
||||||
|
container_name: rdtclient
|
||||||
|
volumes:
|
||||||
|
- 'D:/Downloads/:/data/downloads'
|
||||||
|
- 'D:/Docker/rdt-client/:/data/db'
|
||||||
|
image: rogerfar/rdtclient
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 10m
|
||||||
|
ports:
|
||||||
|
- '6500:6500'
|
||||||
|
```
|
||||||
|
|
||||||
|
And to run:
|
||||||
|
|
||||||
|
```console
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the paths in `volumes` as in the above step.
|
||||||
|
|
||||||
## Windows Service
|
## Windows Service
|
||||||
|
|
||||||
1. Make sure you have the **ASP.NET Core Runtime 10.0.0** and the **SDK** installed: [https://dotnet.microsoft.com/download/dotnet/10.0](https://dotnet.microsoft.com/download/dotnet/10.0)
|
Instead of running in Docker you can install it as a service in Windows or Linux (not tested).
|
||||||
2. Get the latest zip file from the Releases page and extract it to your host.
|
|
||||||
3. Open the `appsettings.json` file and replace the `LogLevel` `Path` to a path on your host.
|
|
||||||
4. In `appsettings.json` replace the `Database` `Path` to a path on your host.
|
|
||||||
5. When using Windows paths, make sure to escape the slashes. For example: `D:\\RdtClient\\db\\rdtclient.db`
|
|
||||||
6. Do one of these:
|
|
||||||
* Run `RdtClient.Web.exe` to start the client.
|
|
||||||
* Run `service-install.bat` to install the client as a service. This will install `RdtClient.Web.exe` as a service which make the client start in the backgorund when the computer starts. (You probably want to do this if you are going to use this with Sonarr etc...)
|
|
||||||
|
|
||||||
## Linux Service
|
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.
|
||||||
Instead of running in Docker you can install it as a service in Linux.
|
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. Install .NET: [https://docs.microsoft.com/en-us/dotnet/core/install/linux](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
|
1. When using Windows paths, make sure to escape the slashes. For example: `D:\\RdtClient\\db\\rdtclient.db`
|
||||||
|
|
||||||
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-10.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```
|
|
||||||
|
|
||||||
## Proxmox LXC
|
|
||||||
|
|
||||||
If you use Proxmox for your homelab, you can run rdt-client in a linux container (LXC), check it here:
|
|
||||||
[https://tteck.github.io/Proxmox/](https://tteck.github.io/Proxmox/)
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|
@ -110,18 +89,26 @@ If you use Proxmox for your homelab, you can run rdt-client in a linux container
|
||||||
|
|
||||||
### Download Clients
|
### Download Clients
|
||||||
|
|
||||||
Currently there 4 available download clients:
|
Currently there 2 available download clients:
|
||||||
|
|
||||||
#### Bezzad Downloader
|
#### Simple Downloader
|
||||||
|
|
||||||
This [downloader](https://github.com/bezzad/Downloader) can be used to download files in parallel and with multiple chunks.
|
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.
|
||||||
|
|
||||||
It has the following options:
|
It has the following options:
|
||||||
|
|
||||||
- Download speed (in MB/s): This number indicates the speed in MB/s per download over all parallel downloads and chunks.
|
- 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.
|
||||||
- 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.
|
#### Multi Part Downloader
|
||||||
- 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.
|
|
||||||
|
This [downloader](https://github.com/bezzad/Downloader) as more options and such uses more resources (memory, CPU) to download files. Recommended more powerful systems.
|
||||||
|
|
||||||
|
It has the following options:
|
||||||
|
|
||||||
|
- Temp Download path: Set this path to where the downloader temporarily stores chunks. This path can be an internal path in Docker (i.e. `/data/temp`) but make sure you have enough disk space to complete the whole download. When all chunks are completed the completed file is copied to your download folder.
|
||||||
|
- Maximum parallel downloads: This number indicates how many completed torrents from Real-Debrid can be downloaded at the same time.
|
||||||
|
- Parallel connections per download: This number indicates how many threads/connections/parts/chunks it will use per download. This can increase speed, recommended is no more than 8.
|
||||||
|
- Download speed (in MB/s): This number indicates the speed in MB/s per download. If you set this to 10 and `Maximum parallel downloads` to 2, you can download with a maximum of 20MB/s.
|
||||||
|
|
||||||
#### Aria2c downloader
|
#### Aria2c downloader
|
||||||
|
|
||||||
|
|
@ -132,33 +119,6 @@ It has the following options:
|
||||||
- Url: The full URL to your Aria2c service. This must end in /jsonrpc. A standard path is `http://192.168.10.2:6800/jsonrpc`.
|
- Url: The full URL to your Aria2c service. This must end in /jsonrpc. A standard path is `http://192.168.10.2:6800/jsonrpc`.
|
||||||
- Secret: Optional secret to connecto to your Aria2c service.
|
- Secret: Optional secret to connecto to your Aria2c service.
|
||||||
|
|
||||||
If Aria2c is selected, none of the above options for `Internal Downloader` are used, you have to configure Aria2c manually.
|
|
||||||
|
|
||||||
#### Symlink downloader
|
|
||||||
|
|
||||||
Symlink downloader requires a rclone mount to be mounted into your filesystem. Be sure to keep the exact path to mounted files in other apps exactly
|
|
||||||
the same as used by rdt-client. Otherwise the symlinks wont resolve the file its trying to point to.
|
|
||||||
|
|
||||||
If the mount path folder cant be found the client wont start downloading anything.
|
|
||||||
|
|
||||||
Required configuration:
|
|
||||||
- Post Download Action = DO NOT SELECT REMOVE FROM PROVIDER
|
|
||||||
- Rclone mount path = /PATH_TO_YOUR_RCLONE_MOUNT/torrents/
|
|
||||||
|
|
||||||
Suggested configuration:
|
|
||||||
- Automatic retry downloads > 3
|
|
||||||
|
|
||||||
#### Synology Download Station
|
|
||||||
|
|
||||||
The Synology Download Station downloader uses an external Download Station server. You will need to set this up yourself.
|
|
||||||
|
|
||||||
It has the following options:
|
|
||||||
|
|
||||||
- Url: The URL to the Synology DownloadStation. A common URL is `http://127.0.0.1:5000`
|
|
||||||
- Username: The username to use when connecting to the Synology DownloadStation.
|
|
||||||
- Password: The password to use when connecting to the Synology DownloadStation.
|
|
||||||
- Download Path: The root path to download the file on the Synology DownloadStation host. If left empty, the default path configured on your Download Station server will be used.
|
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
- If you forgot your logins simply delete the `rdtclient.db` and restart the service.
|
- If you forgot your logins simply delete the `rdtclient.db` and restart the service.
|
||||||
|
|
@ -166,9 +126,7 @@ It has the following options:
|
||||||
|
|
||||||
### Connecting Sonarr/Radarr
|
### Connecting Sonarr/Radarr
|
||||||
|
|
||||||
RdtClient emulates the qBittorrent web protocol and allows applications to use those APIs. This way you can use Sonarr and Radarr to download directly from RealDebrid.
|
RdtClient emulates the qBittorrent web protocol and allow applications to use those APIs. This way you can use Sonarr and Radarr to download directly from RealDebrid.
|
||||||
|
|
||||||
#### Torrents
|
|
||||||
|
|
||||||
1. Login to Sonarr or Radarr and click `Settings`.
|
1. Login to Sonarr or Radarr and click `Settings`.
|
||||||
1. Go to the `Download Client` tab and click the plus to add.
|
1. Go to the `Download Client` tab and click the plus to add.
|
||||||
|
|
@ -185,28 +143,6 @@ When downloading files it will append the `category` setting in the Sonarr/Radar
|
||||||
|
|
||||||
Notice: the progress and ETA reported in Sonarr's Activity tab will not be accurate, but it will report the torrent as completed so it can be processed after it is done downloading.
|
Notice: the progress and ETA reported in Sonarr's Activity tab will not be accurate, but it will report the torrent as completed so it can be processed after it is done downloading.
|
||||||
|
|
||||||
#### Usenet/NZB
|
|
||||||
|
|
||||||
RdtClient also emulates part of the SABnzbd API so Sonarr and Radarr can add NZB downloads. This requires a provider that supports Usenet/NZB downloads, currently TorBox or Premiumize.
|
|
||||||
|
|
||||||
1. Login to Sonarr or Radarr and click `Settings`.
|
|
||||||
1. Go to the `Download Client` tab and click the plus to add.
|
|
||||||
1. Click `SABnzbd` in the list.
|
|
||||||
1. Enter the IP or hostname of RdtClient in the `Host` field.
|
|
||||||
1. Enter `6500` in the `Port` field.
|
|
||||||
1. Enable `Use SSL` only if you access RdtClient through HTTPS.
|
|
||||||
1. Leave `URL Base` empty unless RdtClient is configured with a `BasePath`, for example `/rdt`.
|
|
||||||
1. If RdtClient authentication is enabled, leave `API Key` empty and enter your RdtClient username and password. If your client only supports an API key, enter `{username}:{password}` in `API Key`.
|
|
||||||
1. If RdtClient authentication is disabled, enter any value in `API Key`, for example `rdtclient`, and leave username/password empty.
|
|
||||||
1. Set the category to `sonarr` for Sonarr or `radarr` for Radarr.
|
|
||||||
1. Hit `Test` and then `Save` if all is well.
|
|
||||||
|
|
||||||
When importing completed NZB downloads, Sonarr/Radarr must be able to access the path reported by RdtClient. In Docker setups this may require a Remote Path Mapping from the RdtClient download path to the path mounted inside Sonarr/Radarr.
|
|
||||||
|
|
||||||
### Running within a folder
|
|
||||||
|
|
||||||
By default the application runs in the root of your hosted address (i.e. https://rdt.myserver.com/), but if you want to run it as a relative folder (i.e. https://myserver.com/rdt) you will have to change the `BasePath` setting in the `appsettings.json` file. You can set the `BASE_PATH` environment variable for docker enviroments.
|
|
||||||
|
|
||||||
## Build instructions
|
## Build instructions
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
@ -214,24 +150,13 @@ By default the application runs in the root of your hosted address (i.e. https:/
|
||||||
- NodeJS
|
- NodeJS
|
||||||
- NPM
|
- NPM
|
||||||
- Angular CLI
|
- Angular CLI
|
||||||
- .NET 10
|
- .NET 6
|
||||||
- Visual Studio 2025
|
- Visual Studio 2022
|
||||||
- (optional) Resharper
|
- (optional) Resharper
|
||||||
|
|
||||||
### Dev Container
|
|
||||||
|
|
||||||
The repository includes a dev container under `.devcontainer/` for the split development workflow used by this project.
|
|
||||||
|
|
||||||
It installs .NET 10 and Node 22, forwards ports `4200` and `6500`, and persists `/data/db` and `/data/downloads` in named volumes so the local SQLite database, logs, and downloads survive container rebuilds.
|
|
||||||
|
|
||||||
1. Open the repository in the dev container.
|
|
||||||
1. In one terminal run `dotnet watch run --project server/RdtClient.Web`.
|
|
||||||
1. In another terminal run `cd client && npm start`.
|
|
||||||
1. Open `http://localhost:4200`. The Angular dev server proxies `/Api` and `/hub` to the backend running on `6500`.
|
|
||||||
|
|
||||||
1. Open the client folder project in VS Code and run `npm install`.
|
1. Open the client folder project in VS Code and run `npm install`.
|
||||||
1. To debug run `ng serve`, to build run `ng build -c production`.
|
1. To debug run `ng serve`, to build run `ng build --prod`.
|
||||||
1. Open the Visual Studio 2025 project `RdtClient.sln` and `Publish` the `RdtClient.Web` to the given `PublishFolder` target.
|
1. Open the Visual Studio 2019 project `RdtClient.sln` and `Publish` the `RdtClient.Web` to the given `PublishFolder` target.
|
||||||
1. When debugging, make sure to run `RdtClient.Web.dll` and not `IISExpress`.
|
1. When debugging, make sure to run `RdtClient.Web.dll` and not `IISExpress`.
|
||||||
1. The result is found in `Publish`.
|
1. The result is found in `Publish`.
|
||||||
|
|
||||||
|
|
@ -242,14 +167,3 @@ It installs .NET 10 and Node 22, forwards ports `4200` and `6500`, and persists
|
||||||
1. To stop: `docker stop rdtclient`
|
1. To stop: `docker stop rdtclient`
|
||||||
1. To remove: `docker rm rdtclient`
|
1. To remove: `docker rm rdtclient`
|
||||||
1. Or use `docker-build.bat`
|
1. Or use `docker-build.bat`
|
||||||
|
|
||||||
## Misc Install Notes
|
|
||||||
|
|
||||||
### Rootless Podman, Linux Host, and CIFS Connections
|
|
||||||
|
|
||||||
RDT Client read and write permission tests fail if the CIFS connection is not setup properly, despite permissions working inspection. In the Web GUI, it will report access denied, and in the log file you will see exceptions like this ([dotnet issue](https://github.com/dotnet/runtime/issues/42790)):
|
|
||||||
```
|
|
||||||
System.IO.IOException: Permission denied
|
|
||||||
```
|
|
||||||
The **nobrl** has to be specified in your CIFS connection - [man page](https://linux.die.net/man/8/mount.cifs).
|
|
||||||
Example: ```Options=_netdev,credentials=/etc/samba/credentials/600file,rw,uid=SUBUID,gid=SBUGID,nobrl,file_mode=0770,dir_mode=0770,noperm```
|
|
||||||
|
|
|
||||||
16
client/.browserslistrc
Normal file
16
client/.browserslistrc
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# For the full list of supported browsers by the Angular framework, please see:
|
||||||
|
# https://angular.io/guide/browser-support
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
last 1 Chrome version
|
||||||
|
last 1 Firefox version
|
||||||
|
last 2 Edge major versions
|
||||||
|
last 2 Safari major versions
|
||||||
|
last 2 iOS major versions
|
||||||
|
Firefox ESR
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
{
|
|
||||||
"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": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
const minorOnly = ["eslint", "typescript"];
|
|
||||||
const patchOnly = [];
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
target: (name) => {
|
|
||||||
if (minorOnly.indexOf(name) > -1) {
|
|
||||||
return "minor";
|
|
||||||
}
|
|
||||||
if (patchOnly.indexOf(name) > -1) {
|
|
||||||
return "patch";
|
|
||||||
}
|
|
||||||
return "latest";
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
.angular
|
|
||||||
.vscode
|
|
||||||
|
|
@ -37,33 +37,25 @@
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular/build:application",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": {
|
"outputPath": "../server/RdtClient.Web/wwwroot",
|
||||||
"base": "../server/RdtClient.Web/wwwroot",
|
|
||||||
"browser": ""
|
|
||||||
},
|
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"polyfills": ["src/polyfills.ts"],
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": ["src/favicon.ico", "src/assets"],
|
"assets": ["src/favicon.ico", "src/assets"],
|
||||||
"styles": ["src/styles.scss"],
|
"styles": ["src/styles.scss"],
|
||||||
"scripts": [],
|
"scripts": []
|
||||||
"browser": "src/main.ts",
|
|
||||||
"stylePreprocessorOptions": {
|
|
||||||
"sass": {
|
|
||||||
"silenceDeprecations": ["if-function"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "2mb",
|
"maximumWarning": "500kb",
|
||||||
"maximumError": "2mb"
|
"maximumError": "1mb"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
|
|
@ -80,7 +72,9 @@
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
|
"buildOptimizer": false,
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"namedChunks": true
|
"namedChunks": true
|
||||||
|
|
@ -89,63 +83,28 @@
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular/build:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"proxyConfig": "angular-proxy.json"
|
"proxyConfig": "angular-proxy.json"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "client:build:production"
|
"browserTarget": "client:build:production"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"buildTarget": "client:build:development"
|
"browserTarget": "client:build:development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular/build:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "client:build"
|
"browserTarget": "client:build"
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"builder": "@angular-eslint/builder:lint",
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cli": {
|
"defaultProject": "client"
|
||||||
"analytics": false,
|
|
||||||
"schematicCollections": ["@angular-eslint/schematics"]
|
|
||||||
},
|
|
||||||
"schematics": {
|
|
||||||
"@schematics/angular:component": {
|
|
||||||
"type": "component"
|
|
||||||
},
|
|
||||||
"@schematics/angular:directive": {
|
|
||||||
"type": "directive"
|
|
||||||
},
|
|
||||||
"@schematics/angular:service": {
|
|
||||||
"type": "service"
|
|
||||||
},
|
|
||||||
"@schematics/angular:guard": {
|
|
||||||
"typeSeparator": "."
|
|
||||||
},
|
|
||||||
"@schematics/angular:interceptor": {
|
|
||||||
"typeSeparator": "."
|
|
||||||
},
|
|
||||||
"@schematics/angular:module": {
|
|
||||||
"typeSeparator": "."
|
|
||||||
},
|
|
||||||
"@schematics/angular:pipe": {
|
|
||||||
"typeSeparator": "."
|
|
||||||
},
|
|
||||||
"@schematics/angular:resolver": {
|
|
||||||
"typeSeparator": "."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26603
client/package-lock.json
generated
26603
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,49 +6,37 @@
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"update": "ng update --force --allow-dirty @angular/cli @angular/core @angular/cdk",
|
"update": "ng update --force --allow-dirty @angular/cli @angular/core"
|
||||||
"lint": "ng lint",
|
|
||||||
"prettier": "prettier --write .",
|
|
||||||
"precommit": "npm run prettier && npm run lint"
|
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^21.2.10",
|
"@angular/animations": "~13.1.1",
|
||||||
"@angular/cdk": "^21.2.8",
|
"@angular/cdk": "^13.0.2",
|
||||||
"@angular/common": "^21.2.10",
|
"@angular/common": "~13.1.1",
|
||||||
"@angular/compiler": "^21.2.10",
|
"@angular/compiler": "~13.1.1",
|
||||||
"@angular/core": "^21.2.10",
|
"@angular/core": "~13.1.1",
|
||||||
"@angular/forms": "^21.2.10",
|
"@angular/flex-layout": "^12.0.0-beta.35",
|
||||||
"@angular/platform-browser": "^21.2.10",
|
"@angular/forms": "~13.1.1",
|
||||||
"@angular/platform-browser-dynamic": "^21.2.10",
|
"@angular/platform-browser": "~13.1.1",
|
||||||
"@angular/router": "^21.2.10",
|
"@angular/platform-browser-dynamic": "~13.1.1",
|
||||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
"@angular/router": "~13.1.1",
|
||||||
"@microsoft/signalr": "^10.0.0",
|
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||||
"bulma": "^1.0.4",
|
"@microsoft/signalr": "^6.0.0",
|
||||||
"curray": "^1.0.12",
|
"bulma": "^0.9.3",
|
||||||
"file-saver-es": "^2.0.5",
|
"curray": "^1.0.8",
|
||||||
"filesize": "^11.0.17",
|
"file-saver": "^2.0.5",
|
||||||
"rxjs": "^7.8.2",
|
"ngx-filesize": "^2.0.16",
|
||||||
"tslib": "^2.8.1",
|
"rxjs": "~7.4.0",
|
||||||
"zone.js": "^0.16.1"
|
"tslib": "^2.3.1",
|
||||||
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-eslint/builder": "21.3.1",
|
"@angular-devkit/build-angular": "~13.1.2",
|
||||||
"@angular-eslint/eslint-plugin": "21.3.1",
|
"@angular/cli": "~13.1.2",
|
||||||
"@angular-eslint/eslint-plugin-template": "21.3.1",
|
"@angular/compiler-cli": "~13.1.1",
|
||||||
"@angular-eslint/schematics": "21.3.1",
|
"@angular/language-service": "~13.1.1",
|
||||||
"@angular-eslint/template-parser": "21.3.1",
|
"@types/file-saver": "^2.0.4",
|
||||||
"@angular/build": "^21.2.8",
|
"@types/node": "^16.11.9",
|
||||||
"@angular/cli": "^21.2.8",
|
"typescript": "~4.4.4"
|
||||||
"@angular/compiler-cli": "^21.2.10",
|
|
||||||
"@angular/language-service": "^21.2.10",
|
|
||||||
"@types/file-saver": "^2.0.7",
|
|
||||||
"@types/file-saver-es": "^2.0.3",
|
|
||||||
"@types/node": "^25.6.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
|
||||||
"@typescript-eslint/parser": "^8.59.1",
|
|
||||||
"eslint": "^9.39.4",
|
|
||||||
"prettier": "^3.8.3",
|
|
||||||
"typescript": "5.9.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,24 +1,5 @@
|
||||||
<div class="field">
|
<div fxLayout.lt-lg="column" fxLayout.gt-sm="row" fxLayoutGap="20px" class="field">
|
||||||
<div class="tabs is-toggle is-fullwidth">
|
<div fxFlex>
|
||||||
<ul>
|
|
||||||
<li [class.is-active]="type === 'torrent'">
|
|
||||||
<a (click)="changeType('torrent')">
|
|
||||||
<span class="icon is-small"><i class="fas fa-magnet" aria-hidden="true"></i></span>
|
|
||||||
<span>Torrent</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li [class.is-active]="type === 'nzb'">
|
|
||||||
<a (click)="changeType('nzb')">
|
|
||||||
<span class="icon is-small"><i class="fas fa-file-archive" aria-hidden="true"></i></span>
|
|
||||||
<span>NZB</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<div [hidden]="type !== 'torrent'">
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Torrent file</label>
|
<label class="label">Torrent file</label>
|
||||||
<div class="file has-name">
|
<div class="file has-name">
|
||||||
|
|
@ -37,6 +18,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Magnet Link</label>
|
<label class="label">Magnet Link</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
@ -50,82 +33,37 @@
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div [hidden]="type !== 'nzb'">
|
<hr />
|
||||||
<div class="field">
|
|
||||||
<label class="label">NZB 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 an NZB file... </span>
|
|
||||||
</span>
|
|
||||||
<span class="file-name">
|
|
||||||
{{ fileName }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">NZB Link</label>
|
<label class="label">Download action</label>
|
||||||
<div class="control">
|
|
||||||
<textarea
|
|
||||||
class="textarea"
|
|
||||||
placeholder="Paste your NZB link here"
|
|
||||||
[(ngModel)]="nzbLink"
|
|
||||||
[disabled]="saving"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field" style="margin-top: 10px">
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-success" [disabled]="saving" [ngClass]="{ 'is-loading': saving }" (click)="ok()">
|
|
||||||
@if (type === "torrent") {
|
|
||||||
<span>Add Torrent</span>
|
|
||||||
} @else {
|
|
||||||
<span>Add NZB</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="separator">Advanced settings</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Downloader</label>
|
|
||||||
<div class="control select is-fullwidth">
|
<div class="control select is-fullwidth">
|
||||||
<select [(ngModel)]="downloadClient" (ngModelChange)="setFinishAction()">
|
<select [(ngModel)]="downloadAction" [disabled]="provider === 'AllDebrid'">
|
||||||
<option [ngValue]="0">Bezzad</option>
|
<option [ngValue]="0">Download all files</option>
|
||||||
<option [ngValue]="1">Aria2c</option>
|
<option [ngValue]="1">Download all available files</option>
|
||||||
<option [ngValue]="2">Symlink Downloader</option>
|
<option [ngValue]="2">Manually pick files</option>
|
||||||
<option [ngValue]="3">Synology DownloadStation</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<p class="help">Select which downloader is used to download this from the debrid provider to your host.</p>
|
<p class="help" *ngIf="provider === 'AllDebrid'">
|
||||||
</div>
|
This option is only available for RealDebrid. AllDebrid will always download the full torrent.
|
||||||
|
|
||||||
<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 download is finished on the provider, perform this action. Use this setting if you only want to add files
|
|
||||||
to your debrid provider but not download them to the host.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div class="field">
|
||||||
<label class="label">Minimum file size to download</label>
|
<label class="label">Minimum file size to download</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
@ -138,57 +76,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (provider === "AllDebrid") {
|
<p class="help" *ngIf="provider === 'AllDebrid'">
|
||||||
<p class="help">
|
When downloading with AllDebrid it cannot be guaranteed that only files above this limit will be download as
|
||||||
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.
|
||||||
some files are grouped together in 1 large archive.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
@if (downloadAction === 2) {
|
|
||||||
<p class="help">This setting does not apply to manually selected files.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Include files</label>
|
|
||||||
<div class="control">
|
|
||||||
<div class="field" style="margin-bottom: 0">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" type="text" [(ngModel)]="includeRegex" (blur)="verifyRegex()" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="help">
|
|
||||||
Select only the files that are matching this regular expression. Only use this setting OR the Exclude files
|
|
||||||
setting, not both.
|
|
||||||
</p>
|
</p>
|
||||||
@if (downloadAction === 2) {
|
<p class="help" *ngIf="downloadAction === 2">This setting does not apply to manually selected files.</p>
|
||||||
<p class="help">This setting does not apply to manually selected files.</p>
|
|
||||||
}
|
|
||||||
@if (includeRegexError) {
|
|
||||||
<p class="help is-danger">{{ includeRegexError }}</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Exclude files</label>
|
|
||||||
<div class="control">
|
|
||||||
<div class="field" style="margin-bottom: 0">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" type="text" [(ngModel)]="excludeRegex" (blur)="verifyRegex()" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="help">
|
|
||||||
Ignore files that are matching this regular expression. Only use this setting OR the Include files setting, not
|
|
||||||
both.
|
|
||||||
</p>
|
|
||||||
@if (downloadAction === 2) {
|
|
||||||
<p class="help">This setting does not apply to manually selected files.</p>
|
|
||||||
}
|
|
||||||
@if (excludeRegexError) {
|
|
||||||
<p class="help is-danger">{{ excludeRegexError }}</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
@ -196,46 +88,16 @@
|
||||||
<div class="control select is-fullwidth">
|
<div class="control select is-fullwidth">
|
||||||
<select [(ngModel)]="finishedAction">
|
<select [(ngModel)]="finishedAction">
|
||||||
<option [ngValue]="0">Do nothing</option>
|
<option [ngValue]="0">Do nothing</option>
|
||||||
@if (downloadClient !== 2) {
|
<option [ngValue]="1">Remove torrent from provider and client</option>
|
||||||
<option [ngValue]="1">Remove from provider and client</option>
|
<option [ngValue]="2">Remove torrent from provider</option>
|
||||||
<option [ngValue]="2">Remove from provider</option>
|
|
||||||
}
|
|
||||||
<option [ngValue]="3">Remove from client</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Finished Action Delay (minutes)</label>
|
|
||||||
<div class="control is-fullwidth">
|
|
||||||
<input class="input" type="number" [(ngModel)]="finishedActionDelay" />
|
|
||||||
</div>
|
|
||||||
<p class="help">
|
|
||||||
When a download is finished on the provider, perform this action. Use this setting if you only want to add files
|
|
||||||
to your debrid provider but not download them to the host.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Category</label>
|
<label class="label">Category</label>
|
||||||
<div class="control category-combo">
|
<div class="control">
|
||||||
<input
|
<input class="input" type="text" maxlength="100" [(ngModel)]="category" />
|
||||||
class="input"
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="category"
|
|
||||||
(focus)="categoryDropdownOpen = true"
|
|
||||||
(input)="categoryDropdownOpen = true"
|
|
||||||
(blur)="onCategoryBlur()"
|
|
||||||
placeholder="Type or select a category"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
@if (categoryDropdownOpen && filteredCategories.length > 0) {
|
|
||||||
<div class="category-dropdown">
|
|
||||||
@for (cat of filteredCategories; track $index) {
|
|
||||||
<a class="category-option" (mousedown)="selectCategory(cat)">{{ cat }}</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<p class="help">The category becomes a sub-folder in your main download path.</p>
|
<p class="help">The category becomes a sub-folder in your main download path.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -245,13 +107,10 @@
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="number" step="1" [(ngModel)]="priority" />
|
<input class="input" type="number" step="1" [(ngModel)]="priority" />
|
||||||
</div>
|
</div>
|
||||||
<p class="help">Set the priority where 1 is the highest. When empty it will be assigned the lowest priority.</p>
|
<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>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Automatic retry downloads</label>
|
<label class="label">Automatic retry downloads</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
@ -260,42 +119,61 @@
|
||||||
<p class="help">When a single download fails it will retry it this many times before marking it as failed.</p>
|
<p class="help">When a single download fails it will retry it this many times before marking it as failed.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Automatic retry download</label>
|
<label class="label">Automatic retry torrent</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="torrentRetryAttempts" />
|
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="torrentRetryAttempts" />
|
||||||
</div>
|
</div>
|
||||||
<p class="help">
|
<p class="help">
|
||||||
When a single download has failed multiple times (see setting above) or when the download itself received an
|
When a single download has failed multiple times (see setting above) or when the torrent itself received an
|
||||||
error it will retry the full download this many times before marking it failed.
|
error it will retry the full torrent this many times before marking it failed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div fxFlex *ngIf="provider === 'RealDebrid'">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Delete download when in error</label>
|
<label class="label">Available files</label>
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentDeleteOnError" />
|
|
||||||
</div>
|
|
||||||
<p class="help">
|
<p class="help">
|
||||||
When a download has been in error for this many minutes, delete it from the provider and the client. 0 to
|
These files are available for immediate download from Real-Debrid. <br />
|
||||||
disable.
|
It is possible that there are more files in the torrent, which are not shown here.<br />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<div class="scroll-container">
|
||||||
<div class="field">
|
<div class="field" *ngIf="downloadAction === 2">
|
||||||
<label class="label">Maximum lifetime</label>
|
<label class="checkbox is-fullwidth-label">
|
||||||
<div class="control">
|
<input type="checkbox" [checked]="allSelected" (change)="downloadFileCheckedAll()" />
|
||||||
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentLifetime" />
|
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>
|
||||||
<p class="help">
|
|
||||||
The maximum lifetime of a download in minutes. When this time has passed, mark it as error. If the download is
|
|
||||||
completed, the lifetime setting will not apply. 0 to disable.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
@if (error) {
|
<div class="notification is-danger is-light" *ngIf="error">{{ error }}</div>
|
||||||
<div class="notification is-danger is-light">{{ 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,53 +3,17 @@
|
||||||
width: 24em;
|
width: 24em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
.scroll-container {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
color: darkgray;
|
|
||||||
font-size: 10px;
|
|
||||||
margin-top: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator::before,
|
|
||||||
.separator::after {
|
|
||||||
content: "";
|
|
||||||
flex: 1;
|
|
||||||
border-bottom: 1px solid darkgray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator:not(:empty)::before {
|
|
||||||
margin-right: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator:not(:empty)::after {
|
|
||||||
margin-left: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-combo {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: white;
|
overflow-x: auto;
|
||||||
border: 1px solid #dbdbdb;
|
|
||||||
border-top: none;
|
|
||||||
border-radius: 0 0 4px 4px;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
max-height: 603px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-option {
|
.is-fullwidth-label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-block {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
color: #363636;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,31 @@
|
||||||
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { DownloadType, Torrent, TorrentFileAvailability } from '../models/torrent.model';
|
import { TorrentService } from 'src/app/torrent.service';
|
||||||
|
import { Torrent, TorrentFileAvailability } from '../models/torrent.model';
|
||||||
import { SettingsService } from '../settings.service';
|
import { SettingsService } from '../settings.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { NgClass } from '@angular/common';
|
|
||||||
import { TorrentService } from '../torrent.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-add-new-torrent',
|
selector: 'app-add-new-torrent',
|
||||||
templateUrl: './add-new-torrent.component.html',
|
templateUrl: './add-new-torrent.component.html',
|
||||||
styleUrls: ['./add-new-torrent.component.scss'],
|
styleUrls: ['./add-new-torrent.component.scss'],
|
||||||
imports: [FormsModule, NgClass],
|
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
export class AddNewTorrentComponent implements OnInit {
|
export class AddNewTorrentComponent implements OnInit {
|
||||||
private destroyRef = inject(DestroyRef);
|
|
||||||
private router = inject(Router);
|
|
||||||
private torrentService = inject(TorrentService);
|
|
||||||
private settingsService = inject(SettingsService);
|
|
||||||
private activatedRoute = inject(ActivatedRoute);
|
|
||||||
|
|
||||||
public type: 'torrent' | 'nzb' = 'torrent';
|
|
||||||
public fileName: string;
|
public fileName: string;
|
||||||
public magnetLink: string;
|
public magnetLink: string;
|
||||||
public nzbLink: string;
|
|
||||||
private currentTorrentFile: string;
|
private currentTorrentFile: string;
|
||||||
|
|
||||||
public provider: string;
|
public provider: string;
|
||||||
public downloadClient: number;
|
|
||||||
|
|
||||||
private _category = '';
|
public category: string;
|
||||||
public categories: string[] = [];
|
public priority: number;
|
||||||
public filteredCategories: string[] = [];
|
|
||||||
public categoryDropdownOpen = false;
|
|
||||||
public hostDownloadAction: number = 0;
|
|
||||||
public downloadAction: number = 0;
|
public downloadAction: number = 0;
|
||||||
public finishedAction: number = 0;
|
public finishedAction: number = 0;
|
||||||
public finishedActionDelay: number = 0;
|
|
||||||
public downloadMinSize: number = 0;
|
public downloadMinSize: number = 0;
|
||||||
public includeRegex: string = '';
|
|
||||||
public excludeRegex: string = '';
|
|
||||||
public torrentRetryAttempts: number = 1;
|
|
||||||
public downloadRetryAttempts: number = 3;
|
public downloadRetryAttempts: number = 3;
|
||||||
public torrentDeleteOnError: number = 0;
|
public torrentRetryAttempts: number = 1;
|
||||||
public torrentLifetime: number = 0;
|
|
||||||
public priority: number;
|
|
||||||
|
|
||||||
public availableFiles: TorrentFileAvailability[];
|
public availableFiles: TorrentFileAvailability[];
|
||||||
public downloadFiles: { [key: string]: boolean } = {};
|
public downloadFiles: { [key: string]: boolean } = {};
|
||||||
|
|
@ -55,119 +34,20 @@ export class AddNewTorrentComponent implements OnInit {
|
||||||
public saving = false;
|
public saving = false;
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
public includeRegexError: string;
|
private selectedFile: File;
|
||||||
public excludeRegexError: string;
|
|
||||||
public regexSelected: TorrentFileAvailability[];
|
|
||||||
|
|
||||||
private selectedFile: File | null = null;
|
constructor(private router: Router, private torrentService: TorrentService, private settingsService: SettingsService) {
|
||||||
|
this.settingsService.get().subscribe(settings => {
|
||||||
public get category(): string {
|
this.provider = settings.firstOrDefault(m => m.settingId === 'Provider')?.value;
|
||||||
return this._category;
|
|
||||||
}
|
|
||||||
|
|
||||||
public set category(value: string) {
|
|
||||||
this._category = value;
|
|
||||||
this.updateFilteredCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.activatedRoute.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
|
|
||||||
if (params['type'] === 'nzb') {
|
|
||||||
this.type = 'nzb';
|
|
||||||
} else if (params['type'] === 'torrent') {
|
|
||||||
this.type = 'torrent';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params['magnet']) {
|
|
||||||
this.magnetLink = decodeURIComponent(params['magnet']);
|
|
||||||
this.type = 'torrent';
|
|
||||||
}
|
|
||||||
if (params['nzb']) {
|
|
||||||
this.nzbLink = decodeURIComponent(params['nzb']);
|
|
||||||
this.type = 'nzb';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.settingsService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe((settings) => {
|
|
||||||
const providerSetting = settings.find((m) => m.key === 'Provider:Provider');
|
|
||||||
this.provider = providerSetting.enumValues[providerSetting.value as number];
|
|
||||||
this.downloadClient = settings.find((m) => m.key === 'DownloadClient:Client')?.value as number;
|
|
||||||
|
|
||||||
this.category = settings.find((m) => m.key === 'Gui:Default:Category')?.value as string;
|
|
||||||
const categoriesSetting = settings.find((m) => m.key === 'General:Categories')?.value as string;
|
|
||||||
this.categories = (categoriesSetting ?? '')
|
|
||||||
.split(',')
|
|
||||||
.map((c) => c.trim())
|
|
||||||
.filter((c) => c.length > 0)
|
|
||||||
.filter((c, i, arr) => arr.findIndex((a) => a.toLowerCase() === c.toLowerCase()) === i);
|
|
||||||
const matchedCategory = this.categories.find((c) => c.toLowerCase() === (this.category ?? '').toLowerCase());
|
|
||||||
if (matchedCategory) {
|
|
||||||
this.category = matchedCategory;
|
|
||||||
} else {
|
|
||||||
this.updateFilteredCategories();
|
|
||||||
}
|
|
||||||
this.hostDownloadAction = this.downloadAction = settings.find((m) => m.key === 'Gui:Default:HostDownloadAction')
|
|
||||||
?.value as number;
|
|
||||||
this.downloadAction =
|
|
||||||
settings.find((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0;
|
|
||||||
this.finishedAction = settings.find((m) => m.key === 'Gui:Default:FinishedAction')?.value as number;
|
|
||||||
this.finishedActionDelay = settings.find((m) => m.key == 'Gui:Default:FinishedActionDelay')?.value as number;
|
|
||||||
this.downloadMinSize = settings.find((m) => m.key === 'Gui:Default:MinFileSize')?.value as number;
|
|
||||||
this.includeRegex = settings.find((m) => m.key === 'Gui:Default:IncludeRegex')?.value as string;
|
|
||||||
this.excludeRegex = settings.find((m) => m.key === 'Gui:Default:ExcludeRegex')?.value as string;
|
|
||||||
this.torrentRetryAttempts = settings.find((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number;
|
|
||||||
this.downloadRetryAttempts = settings.find((m) => m.key === 'Gui:Default:DownloadRetryAttempts')
|
|
||||||
?.value as number;
|
|
||||||
this.torrentDeleteOnError = settings.find((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number;
|
|
||||||
this.torrentLifetime = settings.find((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number;
|
|
||||||
this.priority = settings.find((m) => m.key === 'Gui:Default:Priority')?.value as number;
|
|
||||||
|
|
||||||
this.setFinishAction();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateFilteredCategories(): void {
|
ngOnInit(): void {}
|
||||||
if (!this.category) {
|
|
||||||
this.filteredCategories = this.categories;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = this.category.toLowerCase();
|
|
||||||
this.filteredCategories = this.categories.filter((value) => value.toLowerCase().includes(search));
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectCategory(cat: string): void {
|
|
||||||
this.category = cat;
|
|
||||||
this.categoryDropdownOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public onCategoryBlur(): void {
|
|
||||||
setTimeout(() => (this.categoryDropdownOpen = false), 150);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setFinishAction() {
|
|
||||||
if (this.downloadClient === 2) {
|
|
||||||
if (this.finishedAction === 1) {
|
|
||||||
this.finishedAction = 3;
|
|
||||||
} else if (this.finishedAction === 2) {
|
|
||||||
this.finishedAction = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public changeType(type: 'torrent' | 'nzb'): void {
|
|
||||||
this.type = type;
|
|
||||||
this.fileName = null;
|
|
||||||
this.selectedFile = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public pickFile(evt: Event): void {
|
public pickFile(evt: Event): void {
|
||||||
const files = (evt.target as HTMLInputElement).files;
|
const files = (evt.target as HTMLInputElement).files;
|
||||||
|
|
||||||
if (files == null || files.length === 0) {
|
if (files.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,6 +60,25 @@ export class AddNewTorrentComponent implements OnInit {
|
||||||
this.checkFiles();
|
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 {
|
public ok(): void {
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
@ -188,7 +87,7 @@ export class AddNewTorrentComponent implements OnInit {
|
||||||
|
|
||||||
if (this.downloadAction === 2) {
|
if (this.downloadAction === 2) {
|
||||||
const selectedFiles = [];
|
const selectedFiles = [];
|
||||||
for (const filePath in this.downloadFiles) {
|
for (let filePath in this.downloadFiles) {
|
||||||
if (this.downloadFiles[filePath] === true) {
|
if (this.downloadFiles[filePath] === true) {
|
||||||
selectedFiles.push(filePath);
|
selectedFiles.push(filePath);
|
||||||
}
|
}
|
||||||
|
|
@ -204,64 +103,37 @@ export class AddNewTorrentComponent implements OnInit {
|
||||||
|
|
||||||
const torrent = new Torrent();
|
const torrent = new Torrent();
|
||||||
torrent.category = this.category;
|
torrent.category = this.category;
|
||||||
torrent.hostDownloadAction = this.hostDownloadAction;
|
|
||||||
torrent.downloadAction = this.downloadAction;
|
torrent.downloadAction = this.downloadAction;
|
||||||
torrent.finishedAction = this.finishedAction;
|
torrent.finishedAction = this.finishedAction;
|
||||||
torrent.finishedActionDelay = this.finishedActionDelay;
|
|
||||||
torrent.downloadMinSize = this.downloadMinSize;
|
torrent.downloadMinSize = this.downloadMinSize;
|
||||||
torrent.includeRegex = this.includeRegex;
|
|
||||||
torrent.excludeRegex = this.excludeRegex;
|
|
||||||
torrent.downloadManualFiles = downloadManualFiles;
|
torrent.downloadManualFiles = downloadManualFiles;
|
||||||
torrent.priority = this.priority;
|
torrent.priority = this.priority;
|
||||||
torrent.torrentRetryAttempts = this.torrentRetryAttempts;
|
torrent.torrentRetryAttempts = this.torrentRetryAttempts;
|
||||||
torrent.downloadRetryAttempts = this.downloadRetryAttempts;
|
torrent.downloadRetryAttempts = this.downloadRetryAttempts;
|
||||||
torrent.deleteOnError = this.torrentDeleteOnError;
|
|
||||||
torrent.lifetime = this.torrentLifetime;
|
|
||||||
torrent.downloadClient = this.downloadClient;
|
|
||||||
|
|
||||||
if (this.type === 'torrent') {
|
if (this.magnetLink) {
|
||||||
torrent.type = DownloadType.Torrent;
|
this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe(
|
||||||
if (this.magnetLink) {
|
() => {
|
||||||
this.torrentService.uploadMagnet(this.magnetLink, torrent).subscribe({
|
this.router.navigate(['/']);
|
||||||
next: () => this.router.navigate(['/']),
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.error = err.error;
|
this.error = err.error;
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
} else if (this.selectedFile) {
|
} else if (this.selectedFile) {
|
||||||
this.torrentService.uploadFile(this.selectedFile, torrent).subscribe({
|
this.torrentService.uploadFile(this.selectedFile, torrent).subscribe(
|
||||||
next: () => this.router.navigate(['/']),
|
() => {
|
||||||
error: (err) => {
|
this.router.navigate(['/']);
|
||||||
this.error = err.error;
|
},
|
||||||
this.saving = false;
|
(err) => {
|
||||||
},
|
this.error = err.error;
|
||||||
});
|
this.saving = false;
|
||||||
} else {
|
}
|
||||||
this.error = 'No magnet or file uploaded';
|
);
|
||||||
this.saving = false;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (this.nzbLink) {
|
this.error = 'No magnet or file uploaded';
|
||||||
this.torrentService.uploadNzbLink(this.nzbLink, torrent).subscribe({
|
this.saving = false;
|
||||||
next: () => this.router.navigate(['/']),
|
|
||||||
error: (err) => {
|
|
||||||
this.error = err.error;
|
|
||||||
this.saving = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (this.selectedFile) {
|
|
||||||
this.torrentService.uploadNzbFile(this.selectedFile, torrent).subscribe({
|
|
||||||
next: () => this.router.navigate(['/']),
|
|
||||||
error: (err) => {
|
|
||||||
this.error = err.error;
|
|
||||||
this.saving = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.error = 'No NZB link or file uploaded';
|
|
||||||
this.saving = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,9 +144,6 @@ export class AddNewTorrentComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkFiles(): void {
|
public checkFiles(): void {
|
||||||
if (this.type === 'nzb') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.magnetLink && this.magnetLink === this.currentTorrentFile) {
|
if (this.magnetLink && this.magnetLink === this.currentTorrentFile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -286,8 +155,8 @@ export class AddNewTorrentComponent implements OnInit {
|
||||||
this.allSelected = true;
|
this.allSelected = true;
|
||||||
|
|
||||||
if (this.magnetLink) {
|
if (this.magnetLink) {
|
||||||
this.torrentService.checkFilesMagnet(this.magnetLink).subscribe({
|
this.torrentService.checkFilesMagnet(this.magnetLink).subscribe(
|
||||||
next: (result) => {
|
(result) => {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
this.availableFiles = result;
|
this.availableFiles = result;
|
||||||
this.currentTorrentFile = this.magnetLink;
|
this.currentTorrentFile = this.magnetLink;
|
||||||
|
|
@ -295,39 +164,27 @@ export class AddNewTorrentComponent implements OnInit {
|
||||||
this.downloadFiles[file.filename] = true;
|
this.downloadFiles[file.filename] = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.error = err.error;
|
this.error = err.error;
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
} else if (this.selectedFile) {
|
} else if (this.selectedFile) {
|
||||||
this.torrentService.checkFiles(this.selectedFile).subscribe({
|
this.torrentService.checkFiles(this.selectedFile).subscribe(
|
||||||
next: (result) => {
|
(result) => {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
this.availableFiles = result;
|
this.availableFiles = result;
|
||||||
result.forEach((file) => {
|
result.forEach((file) => {
|
||||||
this.downloadFiles[file.filename] = true;
|
this.downloadFiles[file.filename] = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.error = err.error;
|
this.error = err.error;
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
} else {
|
} else {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public verifyRegex(): void {
|
|
||||||
this.includeRegexError = null;
|
|
||||||
this.excludeRegexError = null;
|
|
||||||
this.regexSelected = null;
|
|
||||||
|
|
||||||
this.torrentService.verifyRegex(this.includeRegex, this.excludeRegex, this.magnetLink).subscribe((result) => {
|
|
||||||
this.includeRegexError = result.includeError;
|
|
||||||
this.excludeRegexError = result.excludeError;
|
|
||||||
this.regexSelected = result.selectedFiles;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
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 { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
|
||||||
import { authResolver } 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 { 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 { TorrentTableComponent } from './torrent-table/torrent-table.component';
|
||||||
|
|
@ -23,7 +22,7 @@ const routes: Routes = [
|
||||||
path: '',
|
path: '',
|
||||||
component: MainLayoutComponent,
|
component: MainLayoutComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
isLoggedIn: authResolver,
|
isLoggedIn: AuthResolverService,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|
@ -42,10 +41,6 @@ const routes: Routes = [
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'profile',
|
|
||||||
component: ProfileComponent,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
template: '<router-outlet></router-outlet>',
|
template: '<router-outlet></router-outlet>',
|
||||||
styles: [],
|
styles: [],
|
||||||
imports: [RouterOutlet],
|
|
||||||
})
|
})
|
||||||
export class AppComponent {}
|
export class AppComponent {}
|
||||||
|
|
|
||||||
53
client/src/app/app.module.ts
Normal file
53
client/src/app/app.module.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { curray } from 'curray';
|
||||||
|
import { FileSizePipe, NgxFilesizeModule } from 'ngx-filesize';
|
||||||
|
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
|
||||||
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { AuthInterceptor } from './auth.interceptor';
|
||||||
|
import { DownloadStatusPipe } from './download-status.pipe';
|
||||||
|
import { LoginComponent } from './login/login.component';
|
||||||
|
import { MainLayoutComponent } from './main-layout/main-layout.component';
|
||||||
|
import { NavbarComponent } from './navbar/navbar.component';
|
||||||
|
import { SettingsComponent } from './settings/settings.component';
|
||||||
|
import { SetupComponent } from './setup/setup.component';
|
||||||
|
import { TorrentStatusPipe } from './torrent-status.pipe';
|
||||||
|
import { TorrentTableComponent } from './torrent-table/torrent-table.component';
|
||||||
|
import { TorrentComponent } from './torrent/torrent.component';
|
||||||
|
import { DecodeURIPipe } from './decode-uri.pipe';
|
||||||
|
|
||||||
|
curray();
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
MainLayoutComponent,
|
||||||
|
NavbarComponent,
|
||||||
|
AddNewTorrentComponent,
|
||||||
|
TorrentTableComponent,
|
||||||
|
SettingsComponent,
|
||||||
|
TorrentStatusPipe,
|
||||||
|
DownloadStatusPipe,
|
||||||
|
LoginComponent,
|
||||||
|
SetupComponent,
|
||||||
|
TorrentComponent,
|
||||||
|
DecodeURIPipe,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
FormsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
NgxFilesizeModule,
|
||||||
|
FlexLayoutModule,
|
||||||
|
ClipboardModule,
|
||||||
|
],
|
||||||
|
providers: [FileSizePipe, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
import { inject } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Resolve } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { ResolveFn } from '@angular/router';
|
|
||||||
|
|
||||||
export const authResolver: ResolveFn<boolean> = () => {
|
@Injectable({
|
||||||
return inject(AuthService).isLoggedIn();
|
providedIn: 'root',
|
||||||
};
|
})
|
||||||
|
export class AuthResolverService implements Resolve<Observable<any>> {
|
||||||
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
|
resolve() {
|
||||||
|
return this.authService.isLoggedIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Observable, throwError } from 'rxjs';
|
import { Observable, throwError } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthInterceptor implements HttpInterceptor {
|
export class AuthInterceptor implements HttpInterceptor {
|
||||||
private router = inject(Router);
|
constructor(private router: Router) {}
|
||||||
|
|
||||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
return next.handle(req).pipe(
|
return next.handle(req).pipe(
|
||||||
|
|
@ -16,8 +16,8 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||||
} else if (error && (error.status === 401 || error.status === 403)) {
|
} else if (error && (error.status === 401 || error.status === 403)) {
|
||||||
this.router.navigate(['/login']);
|
this.router.navigate(['/login']);
|
||||||
}
|
}
|
||||||
return throwError(() => error);
|
return throwError(error);
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,32 @@
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private http = inject(HttpClient);
|
constructor(private http: HttpClient) {}
|
||||||
private baseHref = inject(APP_BASE_HREF);
|
|
||||||
|
|
||||||
public isLoggedIn(): Observable<boolean> {
|
public isLoggedIn(): Observable<void> {
|
||||||
return this.http.get<boolean>(`${this.baseHref}Api/Authentication/IsLoggedIn`);
|
return this.http.get<void>(`/Api/Authentication/IsLoggedIn`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public create(userName: string, password: string): Observable<void> {
|
public create(userName: string, password: string): Observable<void> {
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Authentication/Create`, {
|
return this.http.post<void>(`/Api/Authentication/Create`, {
|
||||||
userName,
|
userName,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setupProvider(provider: number, token: string): Observable<void> {
|
|
||||||
return this.http.post<void>(`${this.baseHref}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>(`${this.baseHref}Api/Authentication/Login`, {
|
return this.http.post<void>(`/Api/Authentication/Login`, {
|
||||||
userName,
|
userName,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public logout() {
|
public logout() {
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Authentication/Logout`, {});
|
return this.http.post<void>(`/Api/Authentication/Logout`, {});
|
||||||
}
|
|
||||||
|
|
||||||
public update(userName: string, password: string): Observable<void> {
|
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Authentication/Update`, {
|
|
||||||
userName,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
@Pipe({ name: 'decodeURI' })
|
@Pipe({
|
||||||
|
name: 'decodeURI',
|
||||||
|
})
|
||||||
export class DecodeURIPipe implements PipeTransform {
|
export class DecodeURIPipe implements PipeTransform {
|
||||||
transform(input: any) {
|
transform(input: any) {
|
||||||
return decodeURI(input);
|
return decodeURI(input);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { Pipe, PipeTransform, inject } from '@angular/core';
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { FileSizePipe } from 'ngx-filesize';
|
||||||
import { Download } from './models/download.model';
|
import { Download } from './models/download.model';
|
||||||
import { FileSizePipe } from './filesize.pipe';
|
|
||||||
|
|
||||||
@Pipe({ name: 'downloadStatus' })
|
@Pipe({
|
||||||
|
name: 'downloadStatus',
|
||||||
|
})
|
||||||
export class DownloadStatusPipe implements PipeTransform {
|
export class DownloadStatusPipe implements PipeTransform {
|
||||||
private pipe = inject(FileSizePipe);
|
constructor(private pipe: FileSizePipe) {}
|
||||||
|
|
||||||
transform(value: Download): string {
|
transform(value: Download): string {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
|
@ -24,7 +26,11 @@ export class DownloadStatusPipe implements PipeTransform {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.unpackingStarted) {
|
if (value.unpackingStarted) {
|
||||||
const progress = (value.bytesDone / value.bytesTotal || 0) * 100;
|
let progress = (value.bytesDone / value.bytesTotal) * 100;
|
||||||
|
|
||||||
|
if (isNaN(progress)) {
|
||||||
|
progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
return `Unpacking ${progress.toFixed(2)}%`;
|
return `Unpacking ${progress.toFixed(2)}%`;
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +44,11 @@ export class DownloadStatusPipe implements PipeTransform {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.downloadStarted) {
|
if (value.downloadStarted) {
|
||||||
const progress = (value.bytesDone / value.bytesTotal || 0) * 100;
|
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');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
|
||||||
import { filesize } from 'filesize';
|
|
||||||
|
|
||||||
@Pipe({ name: 'filesize' })
|
|
||||||
export class FileSizePipe implements PipeTransform {
|
|
||||||
private static transformOne(value: number, options?: any): string {
|
|
||||||
return filesize(value, options).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
transform(value: number | number[], options?: any) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.map((val) => FileSizePipe.transformOne(val, options));
|
|
||||||
}
|
|
||||||
|
|
||||||
return FileSizePipe.transformOne(value, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,37 +3,22 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<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">
|
||||||
<form (ngSubmit)="login()">
|
<form (ngSubmit)="login()">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="userName" class="label">Username</label>
|
<label for="" class="label">Username</label>
|
||||||
<div class="control has-icons-left">
|
<div class="control has-icons-left">
|
||||||
<input
|
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required />
|
||||||
type="text"
|
|
||||||
placeholder=""
|
|
||||||
class="input"
|
|
||||||
name="userName"
|
|
||||||
id="userName"
|
|
||||||
(change)="setUserName($event)"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<span class="icon is-small is-left">
|
<span class="icon is-small is-left">
|
||||||
<i class="fa fa-envelope"></i>
|
<i class="fa fa-envelope"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="password" class="label">Password</label>
|
<label for="" class="label" name="password">Password</label>
|
||||||
<div class="control has-icons-left">
|
<div class="control has-icons-left">
|
||||||
<input
|
<input type="password" class="input" required name="password" [(ngModel)]="password" />
|
||||||
type="password"
|
|
||||||
class="input"
|
|
||||||
required
|
|
||||||
name="password"
|
|
||||||
id="password"
|
|
||||||
(change)="setPassword($event)"
|
|
||||||
/>
|
|
||||||
<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>
|
||||||
|
|
@ -49,11 +34,9 @@
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@if (error?.length > 0) {
|
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
||||||
<div class="notification is-danger is-light">
|
{{ error }}
|
||||||
{{ error }}
|
</div>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,33 @@
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { Router } from '@angular/router';
|
||||||
import { NgClass } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrls: ['./login.component.scss'],
|
styleUrls: ['./login.component.scss'],
|
||||||
imports: [FormsModule, NgClass],
|
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
export class LoginComponent {
|
export class LoginComponent implements OnInit {
|
||||||
private authService = inject(AuthService);
|
|
||||||
private router = inject(Router);
|
|
||||||
|
|
||||||
public userName: string;
|
public userName: string;
|
||||||
public password: string;
|
public password: string;
|
||||||
public error: string;
|
public error: string;
|
||||||
public loggingIn: boolean;
|
public loggingIn: boolean;
|
||||||
|
|
||||||
public setUserName(event: Event): void {
|
constructor(private authService: AuthService, private router: Router) {}
|
||||||
this.userName = (event.target as any).value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setPassword(event: Event): void {
|
ngOnInit(): void {}
|
||||||
this.password = (event.target as any).value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public login(): void {
|
public login(): void {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.loggingIn = true;
|
this.loggingIn = true;
|
||||||
this.authService.login(this.userName, this.password).subscribe({
|
this.authService.login(this.userName, this.password).subscribe(
|
||||||
next: () => this.router.navigate(['/']),
|
() => {
|
||||||
error: (err) => {
|
this.router.navigate(['/']);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
this.loggingIn = false;
|
this.loggingIn = false;
|
||||||
this.error = err.error;
|
this.error = err.error;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { NavbarComponent } from '../navbar/navbar.component';
|
|
||||||
import { RouterOutlet } from '@angular/router';
|
|
||||||
|
|
||||||
@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'],
|
||||||
imports: [NavbarComponent, RouterOutlet],
|
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
export class MainLayoutComponent {
|
export class MainLayoutComponent implements OnInit {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
ngOnInit(): void {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export interface DiskSpaceStatus {
|
|
||||||
isPaused: boolean;
|
|
||||||
availableSpaceGB: number;
|
|
||||||
thresholdGB: number;
|
|
||||||
lastCheckTime: Date;
|
|
||||||
}
|
|
||||||
|
|
@ -4,6 +4,4 @@ export class Profile {
|
||||||
public expiration: Date;
|
public expiration: Date;
|
||||||
public currentVersion: string;
|
public currentVersion: string;
|
||||||
public latestVersion: string;
|
public latestVersion: string;
|
||||||
public isInsecure: boolean;
|
|
||||||
public disableUpdateNotification: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export interface RateLimitStatus {
|
|
||||||
nextDequeueTime: Date | null;
|
|
||||||
secondsRemaining: number;
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
export class Setting {
|
export class Setting {
|
||||||
key: string;
|
public settingId: string;
|
||||||
value: boolean | number | null | string;
|
public value: string;
|
||||||
displayName: null | string;
|
|
||||||
description: null | string;
|
|
||||||
type: string;
|
|
||||||
settings: Setting[];
|
|
||||||
enumValues: { [key: string]: string };
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,9 @@ export class Torrent {
|
||||||
public torrentId: string;
|
public torrentId: string;
|
||||||
public hash: string;
|
public hash: string;
|
||||||
public category: string;
|
public category: string;
|
||||||
public downloadClient: number;
|
|
||||||
public hostDownloadAction: number;
|
|
||||||
public downloadAction: number;
|
public downloadAction: number;
|
||||||
public finishedAction: number;
|
public finishedAction: number;
|
||||||
public finishedActionDelay: number;
|
|
||||||
public downloadMinSize: number;
|
public downloadMinSize: number;
|
||||||
public includeRegex: string;
|
|
||||||
public excludeRegex: string;
|
|
||||||
public downloadManualFiles: string;
|
public downloadManualFiles: string;
|
||||||
|
|
||||||
public added: Date;
|
public added: Date;
|
||||||
|
|
@ -24,11 +19,8 @@ export class Torrent {
|
||||||
public retryCount: number;
|
public retryCount: number;
|
||||||
public downloadRetryAttempts: number;
|
public downloadRetryAttempts: number;
|
||||||
public torrentRetryAttempts: number;
|
public torrentRetryAttempts: number;
|
||||||
public deleteOnError: number;
|
|
||||||
public lifetime: number;
|
|
||||||
|
|
||||||
public priority: number;
|
public priority: number;
|
||||||
public type: DownloadType;
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
public rdId: string;
|
public rdId: string;
|
||||||
|
|
@ -44,12 +36,9 @@ export class Torrent {
|
||||||
public rdSpeed: number;
|
public rdSpeed: number;
|
||||||
public rdSeeders: number;
|
public rdSeeders: number;
|
||||||
public rdFiles: string;
|
public rdFiles: string;
|
||||||
public statusText?: string;
|
|
||||||
public filesCount?: number;
|
|
||||||
public downloadsCount?: number;
|
|
||||||
|
|
||||||
public files?: TorrentFile[];
|
public files: TorrentFile[];
|
||||||
public downloads?: Download[];
|
public downloads: Download[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TorrentFile {
|
export class TorrentFile {
|
||||||
|
|
@ -67,18 +56,11 @@ export class TorrentFileAvailability {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RealDebridStatus {
|
export enum RealDebridStatus {
|
||||||
Queued = 0,
|
Processing = 0,
|
||||||
|
WaitingForFileSelection = 1,
|
||||||
Processing = 1,
|
Downloading = 2,
|
||||||
WaitingForFileSelection = 2,
|
Finished = 3,
|
||||||
Downloading = 3,
|
Uploading = 4,
|
||||||
Finished = 4,
|
|
||||||
Uploading = 5,
|
|
||||||
|
|
||||||
Error = 99,
|
Error = 99,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DownloadType {
|
|
||||||
Torrent = 0,
|
|
||||||
Nzb = 1,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export class Version {
|
|
||||||
public version: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<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" routerLink="/">
|
<a class="navbar-item" routerLink="/">
|
||||||
<img src="assets/logo.png" />
|
<img src="../../assets/logo.png" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
|
@ -24,13 +24,13 @@
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-table" aria-hidden="true"></i>
|
<i class="fas fa-table" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>Torrents/NZBs</span>
|
<span>Torrents</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="navbar-item" routerLink="/add" [queryParams]="{ type: 'torrent' }">
|
<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 Torrent/NZB</span>
|
<span>Add New Torrent</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="navbar-item" routerLink="/settings">
|
<a class="navbar-item" routerLink="/settings">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
|
|
@ -38,18 +38,13 @@
|
||||||
</span>
|
</span>
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</a>
|
</a>
|
||||||
@if (profile) {
|
<a class="navbar-item" *ngIf="profile" href="{{ providerLink }}" target="_blank" rel="noopener">
|
||||||
<a class="navbar-item" 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>
|
<span *ngIf="profile.expiration">Premium Status: {{ profile.expiration | date }}</span>
|
||||||
@if (profile.expiration) {
|
<span *ngIf="!profile.expiration" class="no-premium">Not premium</span>
|
||||||
<span>Premium Status: {{ profile.expiration | date }}</span>
|
</a>
|
||||||
} @else {
|
|
||||||
<span class="no-premium">Not premium</span>
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
|
|
@ -57,29 +52,16 @@
|
||||||
<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" routerLink="profile"> Profile </a>
|
<a class="navbar-item"> 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" />
|
||||||
<a href="https://github.com/rogerfar/rdt-client" target="_blank" class="navbar-item">Version {{ version }}</a>
|
<div class="navbar-item">Version 2.0.5</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="notification is-warning" *ngIf="profile && profile.latestVersion && profile.currentVersion !== profile.latestVersion">
|
||||||
@if (
|
Version {{ profile.latestVersion }} of RealDebrid Client was found. You are currently on version
|
||||||
profile &&
|
{{ profile.currentVersion }}.
|
||||||
(!profile.disableUpdateNotification || profile.isInsecure) &&
|
</div>
|
||||||
profile.latestVersion &&
|
|
||||||
profile.currentVersion !== profile.latestVersion
|
|
||||||
) {
|
|
||||||
<div class="notification" [ngClass]="profile.isInsecure ? 'is-danger' : 'is-warning'">
|
|
||||||
@if (profile.isInsecure) {
|
|
||||||
<span> Your current version is insecure. </span>
|
|
||||||
}
|
|
||||||
<span>
|
|
||||||
Version {{ profile.latestVersion }} of RealDebrid Client was found. You are currently on version
|
|
||||||
{{ profile.currentVersion }}.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,4 @@
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
margin-top: 52px;
|
margin-top: 52px;
|
||||||
}
|
}
|
||||||
|
|
@ -1,77 +1,43 @@
|
||||||
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { Router } from '@angular/router';
|
||||||
import { NavigationEnd, Router, RouterLink } from '@angular/router';
|
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
import { Profile } from '../models/profile.model';
|
import { Profile } from '../models/profile.model';
|
||||||
import { SettingsService } from '../settings.service';
|
import { SettingsService } from '../settings.service';
|
||||||
import { NgClass, DatePipe } from '@angular/common';
|
|
||||||
import { filter } from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-navbar',
|
||||||
templateUrl: './navbar.component.html',
|
templateUrl: './navbar.component.html',
|
||||||
styleUrls: ['./navbar.component.scss'],
|
styleUrls: ['./navbar.component.scss'],
|
||||||
imports: [RouterLink, NgClass, DatePipe],
|
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
export class NavbarComponent implements OnInit {
|
export class NavbarComponent implements OnInit {
|
||||||
private destroyRef = inject(DestroyRef);
|
|
||||||
private settingsService = inject(SettingsService);
|
|
||||||
private authService = inject(AuthService);
|
|
||||||
private router = inject(Router);
|
|
||||||
|
|
||||||
public showMobileMenu = false;
|
public showMobileMenu = false;
|
||||||
|
|
||||||
public profile: Profile;
|
public profile: Profile;
|
||||||
public providerLink: string;
|
public providerLink: string;
|
||||||
public version: string;
|
|
||||||
|
|
||||||
constructor() {
|
constructor(private settingsService: SettingsService, private authService: AuthService, private router: Router) {}
|
||||||
this.router.events
|
|
||||||
.pipe(
|
|
||||||
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
|
||||||
takeUntilDestroyed(this.destroyRef),
|
|
||||||
)
|
|
||||||
.subscribe(() => {
|
|
||||||
this.showMobileMenu = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.settingsService
|
this.settingsService.getProfile().subscribe((result) => {
|
||||||
.getProfile()
|
this.profile = result;
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe((result) => {
|
|
||||||
this.profile = result;
|
|
||||||
|
|
||||||
switch (result.provider) {
|
switch (result.provider) {
|
||||||
case 'RealDebrid':
|
case 'RealDebrid':
|
||||||
this.providerLink = 'https://real-debrid.com/?id=1348683';
|
this.providerLink = 'https://real-debrid.com/?id=1348683';
|
||||||
break;
|
break;
|
||||||
case 'AllDebrid':
|
case 'AllDebrid':
|
||||||
this.providerLink = 'https://alldebrid.com/?uid=2v91l&lang=en';
|
this.providerLink = 'https://alldebrid.com/?uid=2v91l&lang=en';
|
||||||
break;
|
break;
|
||||||
case 'Premiumize':
|
}
|
||||||
this.providerLink = 'https://www.premiumize.me/';
|
});
|
||||||
break;
|
|
||||||
case 'TorBox':
|
|
||||||
this.providerLink = 'https://torbox.app/';
|
|
||||||
break;
|
|
||||||
case 'DebridLink':
|
|
||||||
this.providerLink = 'https://debrid-link.com/';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.settingsService
|
|
||||||
.getVersion()
|
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe((result) => {
|
|
||||||
this.version = result.version;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public logout(): void {
|
public logout(): void {
|
||||||
this.authService.logout().subscribe({ next: () => this.router.navigate(['/login']), error: console.error });
|
this.authService.logout().subscribe(
|
||||||
|
() => {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
},
|
||||||
|
(err) => {}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { Pipe, PipeTransform, SecurityContext, VERSION, inject } from '@angular/core';
|
|
||||||
import { DomSanitizer } from '@angular/platform-browser';
|
|
||||||
|
|
||||||
@Pipe({ name: 'nl2br' })
|
|
||||||
export class Nl2BrPipe implements PipeTransform {
|
|
||||||
private sanitizer = inject(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<div class="field">
|
|
||||||
<label class="label">Login username</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="text" minlength="1" [(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" [(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">
|
|
||||||
@if (error !== null) {
|
|
||||||
<div class="notification is-danger is-light">Error saving: {{ error }}</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
@if (success) {
|
|
||||||
<div class="notification is-success is-light">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>
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import { Component, inject } from '@angular/core';
|
|
||||||
import { AuthService } from '../auth.service';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { NgClass } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-profile',
|
|
||||||
templateUrl: './profile.component.html',
|
|
||||||
styleUrls: ['./profile.component.scss'],
|
|
||||||
imports: [FormsModule, NgClass],
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
export class ProfileComponent {
|
|
||||||
private authService = inject(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({
|
|
||||||
next: () => {
|
|
||||||
this.success = true;
|
|
||||||
this.saving = false;
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
this.error = err.error;
|
|
||||||
this.success = false;
|
|
||||||
this.saving = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +1,41 @@
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Profile } from './models/profile.model';
|
import { Profile } from './models/profile.model';
|
||||||
import { Setting } from './models/setting.model';
|
import { Setting } from './models/setting.model';
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
import { Version } from './models/version.model';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class SettingsService {
|
export class SettingsService {
|
||||||
private http = inject(HttpClient);
|
constructor(private http: HttpClient) {}
|
||||||
private baseHref = inject(APP_BASE_HREF);
|
|
||||||
|
|
||||||
public get(): Observable<Setting[]> {
|
public get(): Observable<Setting[]> {
|
||||||
return this.http.get<Setting[]>(`${this.baseHref}Api/Settings`);
|
return this.http.get<Setting[]>(`/Api/Settings`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(settings: Setting[]): Observable<void> {
|
public update(settings: Setting[]): Observable<void> {
|
||||||
return this.http.put<void>(`${this.baseHref}Api/Settings`, settings);
|
return this.http.put<void>(`/Api/Settings`, { settings });
|
||||||
}
|
}
|
||||||
|
|
||||||
public getProfile(): Observable<Profile> {
|
public getProfile(): Observable<Profile> {
|
||||||
return this.http.get<Profile>(`${this.baseHref}Api/Settings/Profile`);
|
return this.http.get<Profile>(`/Api/Settings/Profile`);
|
||||||
}
|
|
||||||
|
|
||||||
public getVersion(): Observable<Version> {
|
|
||||||
return this.http.get<Version>(`${this.baseHref}Api/Settings/Version`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public testPath(path: string): Observable<void> {
|
public testPath(path: string): Observable<void> {
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Settings/TestPath`, { path });
|
return this.http.post<void>(`/Api/Settings/TestPath`, { path });
|
||||||
}
|
}
|
||||||
|
|
||||||
public testDownloadSpeed(): Observable<number> {
|
public testDownloadSpeed(): Observable<number> {
|
||||||
return this.http.get<number>(`${this.baseHref}Api/Settings/TestDownloadSpeed`);
|
return this.http.get<number>(`/Api/Settings/TestDownloadSpeed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public testWriteSpeed(): Observable<number> {
|
public testWriteSpeed(): Observable<number> {
|
||||||
return this.http.get<number>(`${this.baseHref}Api/Settings/TestWriteSpeed`);
|
return this.http.get<number>(`/Api/Settings/TestWriteSpeed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public testAria2cConnection(url: string, secret: string): Observable<{ version: string }> {
|
public testAria2cConnection(url: string, secret: string): Observable<{ version: string }> {
|
||||||
return this.http.post<{ version: string }>(`${this.baseHref}Api/Settings/TestAria2cConnection`, {
|
return this.http.post<{ version: string }>(`/Api/Settings/TestAria2cConnection`, {
|
||||||
url,
|
url,
|
||||||
secret,
|
secret,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,219 +1,370 @@
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
@for (tab of tabs; track tab.key; let i = $index) {
|
<li [ngClass]="{ 'is-active': activeTab === 0 }" (click)="activeTab = 0">
|
||||||
<li [ngClass]="{ 'is-active': activeTab === i }" (click)="activeTab = i">
|
<a>General</a>
|
||||||
<a>{{ tab.displayName }}</a>
|
</li>
|
||||||
</li>
|
<li [ngClass]="{ 'is-active': activeTab === 4 }" (click)="activeTab = 4">
|
||||||
}
|
<a>Provider</a>
|
||||||
<li [ngClass]="{ 'is-active': activeTab === 99 }" (click)="activeTab = 99">
|
</li>
|
||||||
<a>Speed Tests</a>
|
<li [ngClass]="{ 'is-active': activeTab === 1 }" (click)="activeTab = 1">
|
||||||
|
<a>Download Client</a>
|
||||||
|
</li>
|
||||||
|
<li [ngClass]="{ 'is-active': activeTab === 2 }" (click)="activeTab = 2">
|
||||||
|
<a>Radarr/Sonarr</a>
|
||||||
|
</li>
|
||||||
|
<li [ngClass]="{ 'is-active': activeTab === 3 }" (click)="activeTab = 3">
|
||||||
|
<a>Tests</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@for (tab of tabs; track tab.key; let i = $index) {
|
<div *ngIf="activeTab === 4">
|
||||||
<div [hidden]="activeTab !== i">
|
<div class="field">
|
||||||
<h3>{{ tab.description }}</h3>
|
<label class="label">Provider</label>
|
||||||
@for (setting of tab.settings; track setting.key) {
|
<div class="control select is-fullwidth">
|
||||||
@if (setting.type === "Object") {
|
<select [(ngModel)]="settingProvider">
|
||||||
<h3 class="title is-3" style="margin-top: 1.2rem">{{ setting.displayName }}</h3>
|
<option value="RealDebrid">Real-Debrid</option>
|
||||||
}
|
<option value="AllDebrid">AllDebrid</option>
|
||||||
<div class="field">
|
</select>
|
||||||
@if (setting.type !== "Boolean" && setting.type !== "Object") {
|
|
||||||
<label class="label">{{ setting.displayName }}</label>
|
|
||||||
}
|
|
||||||
@switch (setting.type) {
|
|
||||||
@case ("String") {
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="text" [(ngModel)]="setting.value" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@case ("Int32") {
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="number" [(ngModel)]="setting.value" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@case ("Boolean") {
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" [(ngModel)]="setting.value" />
|
|
||||||
{{ setting.displayName }}
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
@case ("Enum") {
|
|
||||||
<div class="control select is-fullwidth">
|
|
||||||
<select [(ngModel)]="setting.value">
|
|
||||||
@for (kvp of setting.enumValues | keyvalue; track kvp) {
|
|
||||||
<option [value]="kvp.key">{{ kvp.value }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@case ("Object") {}
|
|
||||||
@default {
|
|
||||||
<div class="control">Invalid setting type {{ setting.type }}</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<p class="help" [innerHtml]="setting.description | nl2br"></p>
|
|
||||||
@if (setting.key === "DownloadClient:Aria2cSecret") {
|
|
||||||
<button
|
|
||||||
class="button is-warning"
|
|
||||||
(click)="testAria2cConnection()"
|
|
||||||
[disabled]="saving"
|
|
||||||
[ngClass]="{ 'is-loading': saving }"
|
|
||||||
>
|
|
||||||
Test aria2 connection
|
|
||||||
</button>
|
|
||||||
@if (testAria2cConnectionError) {
|
|
||||||
<div class="notification is-danger is-light" style="margin-top: 1rem">
|
|
||||||
Could connect to Aria2 client<br />
|
|
||||||
{{ testAria2cConnectionError }}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (testAria2cConnectionSuccess) {
|
|
||||||
<div class="notification is-success is-light" style="margin-top: 1rem">
|
|
||||||
Found Aria2 client version {{ testAria2cConnectionSuccess }}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (activeTab === 99) {
|
|
||||||
<div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Test download path permissions</label>
|
|
||||||
<div class="control">
|
|
||||||
@if (!testPathError && !testPathSuccess) {
|
|
||||||
<button
|
|
||||||
class="button is-warning"
|
|
||||||
(click)="testDownloadPath()"
|
|
||||||
[disabled]="saving"
|
|
||||||
[ngClass]="{ 'is-loading': saving }"
|
|
||||||
>
|
|
||||||
Test permissions
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
@if (testPathError) {
|
|
||||||
<div class="notification is-danger is-light">
|
|
||||||
Could not test your download path<br />
|
|
||||||
{{ testPathError }}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (testPathSuccess) {
|
|
||||||
<div class="notification is-success is-light">Your download path looks good!</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="help">This will check if the download folder has write permissions.</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<p class="help">
|
||||||
<label class="label">Test Real-Debrid download speed</label>
|
The following 2 providers are supported:
|
||||||
<div class="control">
|
<br />
|
||||||
@if (testDownloadSpeedError) {
|
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener">https://real-debrid.com</a>
|
||||||
<div class="notification is-danger is-light">
|
<br />
|
||||||
Could not test your download speed<br />
|
<a href="https://alldebrid.com/?uid=2v91l&lang=en" target="_blank" rel="noopener">https://alldebrid.com</a>
|
||||||
{{ testDownloadSpeedError }}
|
<br />
|
||||||
</div>
|
At this point only 1 provider can be used at the time.
|
||||||
} @else if (testDownloadSpeedSuccess) {
|
</p>
|
||||||
<div class="notification is-success is-light">Download speed {{ testDownloadSpeedSuccess | filesize }}/s</div>
|
</div>
|
||||||
} @else {
|
<div class="field" *ngIf="settingProvider === 'RealDebrid'">
|
||||||
<button
|
<label class="label">Real-Debrid API Key</label>
|
||||||
class="button is-warning"
|
<div class="control">
|
||||||
(click)="testDownloadSpeed()"
|
<input class="input" type="text" maxlength="100" [(ngModel)]="settingRealDebridApiKey" />
|
||||||
[disabled]="saving"
|
</div>
|
||||||
[ngClass]="{ 'is-loading': saving }"
|
<p class="help">
|
||||||
>
|
You can find your API key here:
|
||||||
Test download speed
|
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener">https://real-debrid.com/apitoken</a>.
|
||||||
</button>
|
</p>
|
||||||
}
|
</div>
|
||||||
</div>
|
<div class="field" *ngIf="settingProvider === 'AllDebrid'">
|
||||||
|
<label class="label">AllDebrid 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://alldebrid.com/apikeys/" target="_blank" rel="noopener">https://alldebrid.com/apikeys/</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" [(ngModel)]="settingProviderAutoImport" />
|
||||||
|
Automatically import and process torrents added to provider.
|
||||||
|
</label>
|
||||||
<div class="help">
|
<div class="help">
|
||||||
This will attempt to download a 10GB file from Real-Debrid. When 50MB has been downloaded the test will stop.
|
When selected, import downloads that are not added through RealDebridClient but have been directly added to
|
||||||
|
Real-Debrid or AllDebrid.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label class="label">Test download folder write speed</label>
|
|
||||||
<div class="control">
|
|
||||||
@if (testWriteSpeedError) {
|
|
||||||
<div class="notification is-danger is-light">
|
|
||||||
Could not test your download speed<br />
|
|
||||||
{{ testWriteSpeedError }}
|
|
||||||
</div>
|
|
||||||
} @else if (testWriteSpeedSuccess) {
|
|
||||||
<div class="notification is-success is-light">Write speed {{ testWriteSpeedSuccess | filesize }}/s</div>
|
|
||||||
} @else {
|
|
||||||
<button
|
|
||||||
class="button is-warning"
|
|
||||||
(click)="testWriteSpeed()"
|
|
||||||
[disabled]="saving"
|
|
||||||
[ngClass]="{ 'is-loading': saving }"
|
|
||||||
>
|
|
||||||
Test write speed
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</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>
|
||||||
}
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
<div class="field">
|
<label class="checkbox">
|
||||||
<div class="control">
|
<input type="checkbox" [(ngModel)]="settingProviderAutoDelete" />
|
||||||
@if (error?.length > 0) {
|
Automatically delete downloads removed from provider.
|
||||||
<div class="notification is-danger is-light">Error saving settings: {{ error }}</div>
|
</label>
|
||||||
}
|
<div class="help">
|
||||||
|
When selected, cancel and delete downloads that have been removed from Real-Debrid or AllDebrid.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (activeTab === 0) {
|
<div *ngIf="activeTab === 0">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Register client as magnet link handler</label>
|
<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">
|
<div class="control">
|
||||||
<button
|
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingDownloadPath" />
|
||||||
class="button is-info"
|
</div>
|
||||||
type="button"
|
<p class="help">Path in the docker container to download files to (i.e. /data/downloads).</p>
|
||||||
(click)="registerMagnetHandler()"
|
</div>
|
||||||
[disabled]="!canRegisterMagnetHandler"
|
<div class="field">
|
||||||
>
|
<label class="label">Mapped path</label>
|
||||||
<span>
|
<div class="control">
|
||||||
{{ canRegisterMagnetHandler ? "Register Handler" : "Unavailable in This Browser" }}
|
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingMappedPath" />
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="help">
|
<p class="help">
|
||||||
@if (canRegisterMagnetHandler) {
|
Path where files are downloaded to on your host (i.e. D:\Downloads). This path is used for Radarr and Sonarr to
|
||||||
This will attempt to register the client as your browser's default handler for magnet links and automatically
|
find your downloads.
|
||||||
open them in the new torrent screen for downloading.
|
|
||||||
} @else {
|
|
||||||
Magnet link registration is unavailable. Your
|
|
||||||
<a
|
|
||||||
href="https://caniuse.com/mdn-api_navigator_registerprotocolhandler"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
browser may not support this feature
|
|
||||||
</a>
|
|
||||||
, or the client is not being served in a
|
|
||||||
<a
|
|
||||||
href="https://developer.mozilla.org/en-US/docs/Glossary/Secure_Context/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
secure context
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
@if (activeTab < 99) {
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
<label class="label">Maximum unpack processes</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button is-success" (click)="ok()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">
|
<input class="input" type="number" max="100" min="1" step="1" [(ngModel)]="settingUnpackLimit" />
|
||||||
Save Settings
|
</div>
|
||||||
</button>
|
<p class="help">Maximum amount of downloads that get unpacked on your host at the same time.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="activeTab === 1">
|
||||||
|
<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>
|
||||||
|
<option value="Aria2c">Aria2c</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" step="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" step="1" [(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' || settingDownloadClient === 'Simple'">
|
||||||
|
<label class="label">Download speed (in MB/s)</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="number" max="1000" min="0" step="1" [(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" *ngIf="settingDownloadClient === 'Aria2c'">
|
||||||
|
<label class="label">Aria2c URL</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" maxlength="1000" [(ngModel)]="aria2cUrl" />
|
||||||
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
This is the URL to your Aria2c instance. It must end in /jsonrpc. A common URL is
|
||||||
|
http://192.168.10.2:6800/jsonrpc.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field" *ngIf="settingDownloadClient === 'Aria2c'">
|
||||||
|
<label class="label">Aria2c Secret</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" maxlength="1000" [(ngModel)]="aria2cSecret" />
|
||||||
|
</div>
|
||||||
|
<p class="help">The secret of your Aria2c instance. Optional.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="button is-warning"
|
||||||
|
(click)="testAria2cConnection()"
|
||||||
|
[disabled]="saving"
|
||||||
|
[ngClass]="{ 'is-loading': saving }"
|
||||||
|
*ngIf="settingDownloadClient === 'Aria2c'"
|
||||||
|
>
|
||||||
|
Test aria2 connection
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="notification is-danger is-light" *ngIf="testAria2cConnectionError">
|
||||||
|
Could connect to Aria2 client<br />
|
||||||
|
{{ testAria2cConnectionError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notification is-success is-light" *ngIf="testAria2cConnectionSuccess">
|
||||||
|
Found Aria2 client version {{ testAria2cConnectionSuccess }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="activeTab === 2">
|
||||||
|
<h3 class="subtitle">
|
||||||
|
The following settings only apply when a torrent gets added through the qbittorrent API, usually Radarr or Sonarr.
|
||||||
|
</h3>
|
||||||
|
<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)]="settingMinFileSize" />
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-static">MB</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="help">
|
||||||
|
Files that are smaller than this setting are skipped and not downloaded. When set to 0 all files are downloaded.
|
||||||
|
When downloading from Radarr or Sonarr it's recommended to keep this setting at atleast a few MB to avoid
|
||||||
|
Real-Debrid having to re-download the torrent.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" [(ngModel)]="settingOnlyDownloadAvailableFiles" />
|
||||||
|
Only download available files on Real-Debrid
|
||||||
|
</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">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>
|
||||||
|
|
||||||
|
<div *ngIf="activeTab === 3">
|
||||||
|
<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 != 3">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-success" (click)="ok()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,15 @@
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Setting } from '../models/setting.model';
|
import { Setting } from 'src/app/models/setting.model';
|
||||||
import { NgClass, KeyValuePipe } from '@angular/common';
|
import { SettingsService } from 'src/app/settings.service';
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { Nl2BrPipe } from '../nl2br.pipe';
|
|
||||||
import { FileSizePipe } from '../filesize.pipe';
|
|
||||||
import { SettingsService } from '../settings.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
templateUrl: './settings.component.html',
|
templateUrl: './settings.component.html',
|
||||||
styleUrls: ['./settings.component.scss'],
|
styleUrls: ['./settings.component.scss'],
|
||||||
imports: [NgClass, FormsModule, KeyValuePipe, Nl2BrPipe, FileSizePipe],
|
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
export class SettingsComponent implements OnInit {
|
export class SettingsComponent implements OnInit {
|
||||||
private settingsService = inject(SettingsService);
|
|
||||||
|
|
||||||
public activeTab = 0;
|
public activeTab = 0;
|
||||||
|
|
||||||
public tabs: Setting[] = [];
|
|
||||||
|
|
||||||
public saving = false;
|
public saving = false;
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
|
|
@ -35,59 +25,183 @@ export class SettingsComponent implements OnInit {
|
||||||
public testAria2cConnectionError: string = null;
|
public testAria2cConnectionError: string = null;
|
||||||
public testAria2cConnectionSuccess: string = null;
|
public testAria2cConnectionSuccess: string = null;
|
||||||
|
|
||||||
public canRegisterMagnetHandler = false;
|
public settingLogLevel: string;
|
||||||
|
public settingProvider: string;
|
||||||
|
public settingProviderAutoImport: boolean;
|
||||||
|
public settingProviderAutoDelete: boolean;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public aria2cUrl: string;
|
||||||
|
public aria2cSecret: string;
|
||||||
|
|
||||||
|
public downloadRetryAttempts: number;
|
||||||
|
public torrentRetryAttempts: number;
|
||||||
|
|
||||||
|
constructor(private settingsService: SettingsService) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.canRegisterMagnetHandler = !!(window.isSecureContext && 'registerProtocolHandler' in navigator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset(): void {
|
public reset(): void {
|
||||||
this.settingsService.get().subscribe((settings) => {
|
this.saving = false;
|
||||||
this.tabs = settings.filter((m) => m.key.indexOf(':') === -1);
|
this.error = null;
|
||||||
|
|
||||||
for (let tab of this.tabs) {
|
this.settingsService.get().subscribe(
|
||||||
tab.settings = settings.filter((m) => m.key.indexOf(`${tab.key}:`) > -1);
|
(results) => {
|
||||||
|
this.settingProvider = this.getSetting(results, 'Provider');
|
||||||
|
this.settingProviderAutoImport = this.getSetting(results, 'ProviderAutoImport') === '1';
|
||||||
|
this.settingProviderAutoDelete = this.getSetting(results, 'ProviderAutoDelete') === '1';
|
||||||
|
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');
|
||||||
|
this.aria2cUrl = this.getSetting(results, 'Aria2cUrl');
|
||||||
|
this.aria2cSecret = this.getSetting(results, 'Aria2cSecret');
|
||||||
|
this.downloadRetryAttempts = parseInt(this.getSetting(results, 'DownloadRetryAttempts'), 10);
|
||||||
|
this.torrentRetryAttempts = parseInt(this.getSetting(results, 'TorrentRetryAttempts'), 10);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.error = err.error;
|
||||||
|
this.saving = true;
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ok(): void {
|
public ok(): void {
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
|
|
||||||
const settingsToSave = this.tabs.flatMap((m) => m.settings).filter((m) => m.type !== 'Object');
|
const settings: Setting[] = [
|
||||||
|
{
|
||||||
|
settingId: 'Provider',
|
||||||
|
value: this.settingProvider,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingId: 'ProviderAutoImport',
|
||||||
|
value: this.settingProviderAutoImport ? '1' : '0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingId: 'ProviderAutoDelete',
|
||||||
|
value: this.settingProviderAutoDelete ? '1' : '0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingId: 'Aria2cUrl',
|
||||||
|
value: this.aria2cUrl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingId: 'Aria2cSecret',
|
||||||
|
value: this.aria2cSecret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingId: 'DownloadRetryAttempts',
|
||||||
|
value: (this.downloadRetryAttempts ?? 0).toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingId: 'TorrentRetryAttempts',
|
||||||
|
value: (this.torrentRetryAttempts ?? 0).toString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
this.settingsService.update(settingsToSave).subscribe({
|
this.settingsService.update(settings).subscribe(
|
||||||
next: () =>
|
() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
}, 1000),
|
}, 1000);
|
||||||
error: (err) => {
|
},
|
||||||
|
(err) => {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
this.error = err;
|
this.error = err;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public testDownloadPath(): void {
|
public testDownloadPath(): void {
|
||||||
const settingDownloadPath = this.tabs
|
|
||||||
.find((m) => m.key === 'DownloadClient')
|
|
||||||
.settings.find((m) => m.key === 'DownloadClient:DownloadPath').value as string;
|
|
||||||
|
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
this.testPathError = null;
|
this.testPathError = null;
|
||||||
this.testPathSuccess = false;
|
this.testPathSuccess = false;
|
||||||
|
|
||||||
this.settingsService.testPath(settingDownloadPath).subscribe({
|
this.settingsService.testPath(this.settingDownloadPath).subscribe(
|
||||||
next: () => {
|
() => {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
this.testPathSuccess = true;
|
this.testPathSuccess = true;
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.testPathError = err.error;
|
this.testPathError = err.error;
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public testDownloadSpeed(): void {
|
public testDownloadSpeed(): void {
|
||||||
|
|
@ -95,66 +209,58 @@ export class SettingsComponent implements OnInit {
|
||||||
this.testDownloadSpeedError = null;
|
this.testDownloadSpeedError = null;
|
||||||
this.testDownloadSpeedSuccess = 0;
|
this.testDownloadSpeedSuccess = 0;
|
||||||
|
|
||||||
this.settingsService.testDownloadSpeed().subscribe({
|
this.settingsService.testDownloadSpeed().subscribe(
|
||||||
next: (result) => {
|
(result) => {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
this.testDownloadSpeedSuccess = result;
|
this.testDownloadSpeedSuccess = result;
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.testDownloadSpeedError = err.error;
|
this.testDownloadSpeedError = err.error;
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
public testWriteSpeed(): void {
|
public testWriteSpeed(): void {
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
this.testWriteSpeedError = null;
|
this.testWriteSpeedError = null;
|
||||||
this.testWriteSpeedSuccess = 0;
|
this.testWriteSpeedSuccess = 0;
|
||||||
|
|
||||||
this.settingsService.testWriteSpeed().subscribe({
|
this.settingsService.testWriteSpeed().subscribe(
|
||||||
next: (result) => {
|
(result) => {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
this.testWriteSpeedSuccess = result;
|
this.testWriteSpeedSuccess = result;
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.testWriteSpeedError = err.error;
|
this.testWriteSpeedError = err.error;
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public testAria2cConnection(): void {
|
public testAria2cConnection(): void {
|
||||||
const settingAria2cUrl = this.tabs
|
|
||||||
.find((m) => m.key === 'DownloadClient')
|
|
||||||
.settings.find((m) => m.key === 'DownloadClient:Aria2cUrl').value as string;
|
|
||||||
const settingAria2cSecret = this.tabs
|
|
||||||
.find((m) => m.key === 'DownloadClient')
|
|
||||||
.settings.find((m) => m.key === 'DownloadClient:Aria2cSecret').value as string;
|
|
||||||
|
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
this.testAria2cConnectionError = null;
|
this.testAria2cConnectionError = null;
|
||||||
this.testAria2cConnectionSuccess = null;
|
this.testAria2cConnectionSuccess = null;
|
||||||
|
|
||||||
this.settingsService.testAria2cConnection(settingAria2cUrl, settingAria2cSecret).subscribe({
|
this.settingsService.testAria2cConnection(this.aria2cUrl, this.aria2cSecret).subscribe(
|
||||||
next: (result) => {
|
(result) => {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
this.testAria2cConnectionSuccess = result.version;
|
this.testAria2cConnectionSuccess = result.version;
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.testAria2cConnectionError = err.error;
|
this.testAria2cConnectionError = err.error;
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerMagnetHandler(): void {
|
private getSetting(settings: Setting[], key: string): string {
|
||||||
try {
|
const setting = settings.filter((m) => m.settingId === key);
|
||||||
navigator.registerProtocolHandler('magnet', `${window.location.origin}/add?magnet=%s`);
|
|
||||||
alert(
|
if (setting.length !== 1) {
|
||||||
'Success! Your browser will now prompt you to confirm and add the client as the default handler for magnet links.',
|
throw new Error(`Unable to find setting with key ${key}`);
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
alert('Magnet link registration failed.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return setting[0].value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,189 +3,118 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<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" />
|
||||||
@if (step === 1) {
|
<div class="box" *ngIf="step === 1">
|
||||||
<div class="box">
|
<div class="notification is-primary">
|
||||||
<div class="notification is-primary">
|
Welcome to Real-Debrid 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>
|
|
||||||
<form (ngSubmit)="setup()">
|
|
||||||
<div class="field">
|
|
||||||
<label for="userName" class="label">Username</label>
|
|
||||||
<div class="control has-icons-left">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder=""
|
|
||||||
class="input"
|
|
||||||
name="userName"
|
|
||||||
id="userName"
|
|
||||||
[(ngModel)]="userName"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<span class="icon is-small is-left">
|
|
||||||
<i class="fa fa-envelope"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="password" class="label">Password</label>
|
|
||||||
<div class="control has-icons-left">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
class="input"
|
|
||||||
required
|
|
||||||
name="password"
|
|
||||||
id="password"
|
|
||||||
[(ngModel)]="password"
|
|
||||||
/>
|
|
||||||
<span class="icon is-small is-left">
|
|
||||||
<i class="fa fa-lock"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<button class="button is-success" [ngClass]="{ 'is-loading': working }" [disabled]="working">
|
|
||||||
Setup
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@if (error?.length > 0) {
|
|
||||||
<div class="notification is-danger is-light">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
<form (ngSubmit)="setup()">
|
||||||
|
|
||||||
@if (step === 2) {
|
|
||||||
<div class="box">
|
|
||||||
<div class="notification is-primary">
|
|
||||||
To be able to use the Real-Debrid Client you need a premium subscription to download torrents.
|
|
||||||
<br /><br />
|
|
||||||
Not premium yet? You have the choice of 5 providers:
|
|
||||||
<br />
|
|
||||||
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener"
|
|
||||||
>Use this link to sign up to Real-Debrid.</a
|
|
||||||
>
|
|
||||||
<br />
|
|
||||||
<a href="https://alldebrid.com/?uid=2v91l&lang=en" target="_blank" rel="noopener"
|
|
||||||
>Use this link to sign up to AllDebrid.</a
|
|
||||||
>
|
|
||||||
<br />
|
|
||||||
<a href="https://www.premiumize.me/" target="_blank" rel="noopener"
|
|
||||||
>Use this link to sign up to Premiumize.</a
|
|
||||||
>
|
|
||||||
<br />
|
|
||||||
<a href="https://torbox.app/" target="_blank" rel="noopener">Use this link to sign up to TorBox.</a>
|
|
||||||
<br />
|
|
||||||
<a href="https://debrid-link.com/" target="_blank" rel="noopener"
|
|
||||||
>Use this link to sign up to DebridLink.</a
|
|
||||||
>
|
|
||||||
<small>(Referal links)</small>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Provider</label>
|
<label for="" class="label">Username</label>
|
||||||
<div class="control select is-fullwidth">
|
<div class="control has-icons-left">
|
||||||
<select [(ngModel)]="provider">
|
<input type="text" placeholder="" class="input" name="userName" [(ngModel)]="userName" required />
|
||||||
<option [ngValue]="0">Real-Debrid</option>
|
<span class="icon is-small is-left">
|
||||||
<option [ngValue]="1">AllDebrid</option>
|
<i class="fa fa-envelope"></i>
|
||||||
<option [ngValue]="2">Premiumize</option>
|
</span>
|
||||||
<option [ngValue]="3">TorBox</option>
|
|
||||||
<option [ngValue]="4">DebridLink</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="debrid-api-token" class="label">API Private Token</label>
|
<label for="" class="label" name="password">Password</label>
|
||||||
<div class="control">
|
<div class="control has-icons-left">
|
||||||
<input
|
<input type="password" class="input" required name="password" [(ngModel)]="password" />
|
||||||
type="text"
|
<span class="icon is-small is-left">
|
||||||
placeholder=""
|
<i class="fa fa-lock"></i>
|
||||||
class="input"
|
</span>
|
||||||
name="token"
|
|
||||||
id="debrid-api-token"
|
|
||||||
[(ngModel)]="token"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
@if (provider === 0) {
|
|
||||||
<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>
|
|
||||||
}
|
|
||||||
@if (provider === 1) {
|
|
||||||
<p class="help">
|
|
||||||
You can find your API key here:
|
|
||||||
<a href="https://alldebrid.com/apikeys/" target="_blank" rel="noopener"
|
|
||||||
>https://alldebrid.com/apikeys/</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
@if (provider === 2) {
|
|
||||||
<p class="help">
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
@if (provider === 3) {
|
|
||||||
<p class="help">
|
|
||||||
You can find your API key here:
|
|
||||||
<a href="https://torbox.app/settings" target="_blank" rel="noopener">https://torbox.app/settings</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
@if (provider === 4) {
|
|
||||||
<p class="help">
|
|
||||||
You can find your API key here:
|
|
||||||
<a href="https://debrid-link.com/webapp/apikey" target="_blank" rel="noopener"
|
|
||||||
>https://debrid-link.com/webapp/apikey</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<button
|
<button class="button is-success" [ngClass]="{ 'is-loading': working }" [disabled]="working">
|
||||||
class="button is-success"
|
|
||||||
(click)="setToken()"
|
|
||||||
[ngClass]="{ 'is-loading': working }"
|
|
||||||
[disabled]="working"
|
|
||||||
>
|
|
||||||
Setup
|
Setup
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@if (error?.length > 0) {
|
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
||||||
<div class="notification is-danger is-light">
|
{{ error }}
|
||||||
{{ error }}
|
</div>
|
||||||
</div>
|
</form>
|
||||||
}
|
</div>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (step === 3) {
|
<div class="box" *ngIf="step === 2">
|
||||||
<div class="box">
|
<div class="notification is-primary">
|
||||||
<div class="notification is-primary">
|
To be able to use the Real-Debrid Client you need a premium subscription to download torrents.
|
||||||
You are now connected to Real-Debrid! To setup Radarr or Sonarr, please following the instructions here:
|
<br /><br />
|
||||||
<a href="https://github.com/rogerfar/rdt-client/" target="_blank" rel="noopener"
|
Not premium yet? You have the choice of 2 providers:
|
||||||
>https://github.com/rogerfar/rdt-client/</a
|
<br/>
|
||||||
>.
|
<a href="https://real-debrid.com/?id=1348683" target="_blank" rel="noopener"
|
||||||
</div>
|
>Use this link to sign up to Real-Debrid.</a
|
||||||
<div class="field">
|
>
|
||||||
<button
|
<br/>
|
||||||
class="button is-success"
|
<a href="https://alldebrid.com/?uid=2v91l&lang=en" target="_blank" rel="noopener"
|
||||||
(click)="close()"
|
>Use this link to sign up to AllDebrid.</a
|
||||||
[ngClass]="{ 'is-loading': working }"
|
>
|
||||||
[disabled]="working"
|
<br/>
|
||||||
>
|
<small>(Referal links)</small>
|
||||||
OK
|
</div>
|
||||||
</button>
|
<div class="field">
|
||||||
|
<label class="label">Provider</label>
|
||||||
|
<div class="control select is-fullwidth">
|
||||||
|
<select [(ngModel)]="provider">
|
||||||
|
<option value="RealDebrid">Real-Debrid</option>
|
||||||
|
<option value="AllDebrid">AllDebrid</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<div class="field">
|
||||||
|
<label for="" class="label">API Private Token</label>
|
||||||
|
<div class="control">
|
||||||
|
<input type="text" placeholder="" class="input" name="token" [(ngModel)]="token" required />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button
|
||||||
|
class="button is-success"
|
||||||
|
(click)="setToken()"
|
||||||
|
[ngClass]="{ 'is-loading': working }"
|
||||||
|
[disabled]="working"
|
||||||
|
>
|
||||||
|
Setup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box" *ngIf="step === 3">
|
||||||
|
<div class="notification is-primary">
|
||||||
|
You are now connected to Real-Debrid! To setup Radarr or Sonarr, please following the instructions here:
|
||||||
|
<a href="https://github.com/rogerfar/rdt-client/" target="_blank" rel="noopener"
|
||||||
|
>https://github.com/rogerfar/rdt-client/</a
|
||||||
|
>.
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<button
|
||||||
|
class="button is-success"
|
||||||
|
(click)="close()"
|
||||||
|
[ngClass]="{ 'is-loading': working }"
|
||||||
|
[disabled]="working"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
import { NgClass } from '@angular/common';
|
import { Setting } from '../models/setting.model';
|
||||||
import { FormsModule } from '@angular/forms';
|
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'],
|
||||||
imports: [FormsModule, NgClass],
|
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
export class SetupComponent {
|
export class SetupComponent implements OnInit {
|
||||||
private authService = inject(AuthService);
|
|
||||||
private router = inject(Router);
|
|
||||||
|
|
||||||
public userName: string;
|
public userName: string;
|
||||||
public password: string;
|
public password: string;
|
||||||
public provider = 0;
|
public provider = 'RealDebrid';
|
||||||
public token: string;
|
public token: string;
|
||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
@ -25,33 +20,45 @@ export class SetupComponent {
|
||||||
|
|
||||||
public step: number = 1;
|
public step: number = 1;
|
||||||
|
|
||||||
|
constructor(private authService: AuthService, private settingsService: SettingsService, private router: Router) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {}
|
||||||
|
|
||||||
public setup(): void {
|
public setup(): void {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.working = true;
|
this.working = true;
|
||||||
|
|
||||||
this.authService.create(this.userName, this.password).subscribe({
|
this.authService.create(this.userName, this.password).subscribe(
|
||||||
next: () => {
|
() => {
|
||||||
this.step = 2;
|
this.step = 2;
|
||||||
this.working = false;
|
this.working = false;
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.working = false;
|
this.working = false;
|
||||||
this.error = err.error;
|
this.error = err.error;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setToken(): void {
|
public setToken(): void {
|
||||||
this.authService.setupProvider(this.provider, this.token).subscribe({
|
const settingToken = new Setting();
|
||||||
next: () => {
|
settingToken.settingId = 'RealDebridApiKey';
|
||||||
|
settingToken.value = this.token;
|
||||||
|
|
||||||
|
const settingProvider = new Setting();
|
||||||
|
settingProvider.settingId = 'Provider';
|
||||||
|
settingProvider.value = this.provider;
|
||||||
|
|
||||||
|
this.settingsService.update([settingToken, settingProvider]).subscribe(
|
||||||
|
() => {
|
||||||
this.step = 3;
|
this.step = 3;
|
||||||
this.working = false;
|
this.working = false;
|
||||||
},
|
},
|
||||||
error: (err: any) => {
|
(err) => {
|
||||||
this.working = false;
|
this.working = false;
|
||||||
this.error = err.error;
|
this.error = err.error;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public close(): void {
|
public close(): void {
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
|
||||||
|
|
||||||
export type SortDirection = 'asc' | 'desc';
|
|
||||||
|
|
||||||
export function getSortFieldValue(item: unknown, field: string): unknown {
|
|
||||||
if (item == null || !field) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return field.split('.').reduce<unknown>((value, key) => {
|
|
||||||
if (value == null || typeof value !== 'object') {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (value as Record<string, unknown>)[key];
|
|
||||||
}, item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareSortValues(left: unknown, right: unknown): number {
|
|
||||||
if (left === right) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (left == null) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (right == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof left === 'string' && typeof right === 'string') {
|
|
||||||
return left.localeCompare(right, undefined, { sensitivity: 'base' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (left instanceof Date && right instanceof Date) {
|
|
||||||
return left.getTime() - right.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof left === 'boolean' && typeof right === 'boolean') {
|
|
||||||
return Number(left) - Number(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
const comparableLeft = left as string | number | bigint;
|
|
||||||
const comparableRight = right as string | number | bigint;
|
|
||||||
|
|
||||||
if (comparableLeft < comparableRight) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comparableLeft > comparableRight) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sortItems<T>(
|
|
||||||
array: readonly T[],
|
|
||||||
field: string,
|
|
||||||
order: SortDirection = 'asc',
|
|
||||||
accessor: (item: T, field: string) => unknown = getSortFieldValue,
|
|
||||||
): T[] {
|
|
||||||
if (!Array.isArray(array)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const direction = order === 'asc' ? 1 : -1;
|
|
||||||
|
|
||||||
return [...array].sort((left, right) => compareSortValues(accessor(left, field), accessor(right, field)) * direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Pipe({ name: 'sort' })
|
|
||||||
export class SortPipe implements PipeTransform {
|
|
||||||
transform(array: unknown[], field: string, order: SortDirection = 'asc'): unknown[] {
|
|
||||||
return sortItems(array, field, order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,132 +1,113 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { FileSizePipe } from 'ngx-filesize';
|
||||||
import { RealDebridStatus, Torrent } from './models/torrent.model';
|
import { RealDebridStatus, Torrent } from './models/torrent.model';
|
||||||
import { FileSizePipe } from './filesize.pipe';
|
|
||||||
|
|
||||||
const fileSizePipe = new FileSizePipe();
|
@Pipe({
|
||||||
|
name: 'status',
|
||||||
|
})
|
||||||
|
export class TorrentStatusPipe implements PipeTransform {
|
||||||
|
constructor(private pipe: FileSizePipe) {}
|
||||||
|
|
||||||
export function getTorrentStatus(torrent: Torrent): string {
|
transform(torrent: Torrent): string {
|
||||||
if (torrent.error) {
|
|
||||||
return torrent.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloads = torrent.downloads ?? [];
|
if (torrent.error) {
|
||||||
|
return torrent.error;
|
||||||
|
}
|
||||||
|
|
||||||
if (downloads.length > 0) {
|
if (torrent.downloads.length > 0) {
|
||||||
let allFinished = true;
|
const errors = torrent.downloads.where((m) => m.error != null);
|
||||||
let downloadingCount = 0;
|
|
||||||
let downloadedCount = 0;
|
|
||||||
let downloadingBytesDone = 0;
|
|
||||||
let downloadingBytesTotal = 0;
|
|
||||||
let downloadingSpeed = 0;
|
|
||||||
let unpackingCount = 0;
|
|
||||||
let unpackedCount = 0;
|
|
||||||
let unpackingBytesDone = 0;
|
|
||||||
let unpackingBytesTotal = 0;
|
|
||||||
let queuedForUnpackingCount = 0;
|
|
||||||
let queuedForDownloadingCount = 0;
|
|
||||||
|
|
||||||
for (const download of downloads) {
|
if (errors.length > 0) {
|
||||||
if (download.completed == null) {
|
return 'Error';
|
||||||
allFinished = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download.downloadFinished != null) {
|
const allFinished = torrent.downloads.all((m) => m.completed != null);
|
||||||
downloadedCount += 1;
|
|
||||||
|
if (allFinished) {
|
||||||
|
return 'Finished';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download.downloadStarted && !download.downloadFinished && download.bytesDone > 0) {
|
const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished && m.bytesDone > 0);
|
||||||
downloadingCount += 1;
|
const downloaded = torrent.downloads.where((m) => m.downloadFinished != null);
|
||||||
downloadingBytesDone += download.bytesDone;
|
|
||||||
downloadingBytesTotal += download.bytesTotal;
|
if (downloading.length > 0) {
|
||||||
downloadingSpeed += download.speed;
|
const bytesDone = downloading.sum((m) => m.bytesDone);
|
||||||
|
const bytesTotal = downloading.sum((m) => m.bytesTotal);
|
||||||
|
let progress = (bytesDone / bytesTotal) * 100;
|
||||||
|
|
||||||
|
if (isNaN(progress)) {
|
||||||
|
progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let allSpeeds = downloading.sum((m) => m.speed);
|
||||||
|
|
||||||
|
let speed: string | string[] = '0';
|
||||||
|
|
||||||
|
speed = this.pipe.transform(allSpeeds, 'filesize');
|
||||||
|
|
||||||
|
return `Downloading file ${downloading.length + downloaded.length}/${
|
||||||
|
torrent.downloads.length
|
||||||
|
} (${progress.toFixed(2)}% - ${speed}/s)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download.unpackingFinished != null) {
|
const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished && m.bytesDone > 0);
|
||||||
unpackedCount += 1;
|
const unpacked = torrent.downloads.where((m) => m.unpackingFinished != null);
|
||||||
|
|
||||||
|
if (unpacking.length > 0) {
|
||||||
|
const bytesDone = unpacking.sum((m) => m.bytesDone);
|
||||||
|
const bytesTotal = unpacking.sum((m) => m.bytesTotal);
|
||||||
|
let progress = (bytesDone / bytesTotal) * 100;
|
||||||
|
|
||||||
|
if (isNaN(progress)) {
|
||||||
|
progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Extracting file ${unpacking.length + unpacked.length}/${torrent.downloads.length} (${progress.toFixed(
|
||||||
|
2
|
||||||
|
)}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download.unpackingStarted && !download.unpackingFinished && download.bytesDone > 0) {
|
const queuedForUnpacking = torrent.downloads.where((m) => m.unpackingQueued && !m.unpackingStarted);
|
||||||
unpackingCount += 1;
|
|
||||||
unpackingBytesDone += download.bytesDone;
|
if (queuedForUnpacking.length > 0) {
|
||||||
unpackingBytesTotal += download.bytesTotal;
|
return `Queued for unpacking`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download.unpackingQueued && !download.unpackingStarted) {
|
const queuedForDownload = torrent.downloads.where((m) => !m.downloadStarted && !m.downloadFinished);
|
||||||
queuedForUnpackingCount += 1;
|
|
||||||
|
if (queuedForDownload.length > 0) {
|
||||||
|
return `Queued for downloading`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!download.downloadStarted && !download.downloadFinished) {
|
if (unpacked.length > 0) {
|
||||||
queuedForDownloadingCount += 1;
|
return `Files unpacked`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloaded.length > 0) {
|
||||||
|
return `Files downloaded to host`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allFinished) {
|
if (torrent.completed) {
|
||||||
return 'Finished';
|
return 'Finished';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadingCount > 0) {
|
switch (torrent.rdStatus) {
|
||||||
const progress = ((downloadingBytesDone / downloadingBytesTotal) || 0) * 100;
|
case RealDebridStatus.Downloading:
|
||||||
const speed = fileSizePipe.transform(downloadingSpeed, 'filesize') as string;
|
const speed = this.pipe.transform(torrent.rdSpeed, 'filesize');
|
||||||
|
return `Torrent downloading (${torrent.rdProgress}% - ${speed}/s)`;
|
||||||
return `Downloading file ${downloadingCount + downloadedCount}/${downloads.length} (${progress.toFixed(2)}% - ${speed}/s)`;
|
case RealDebridStatus.Processing:
|
||||||
|
return `Torrent processing`;
|
||||||
|
case RealDebridStatus.WaitingForFileSelection:
|
||||||
|
return `Torrent waiting for file selection`;
|
||||||
|
case RealDebridStatus.Error:
|
||||||
|
return `Torrent error: ${torrent.rdStatusRaw}`;
|
||||||
|
case RealDebridStatus.Finished:
|
||||||
|
return `Torrent finished, waiting for download links`;
|
||||||
|
case RealDebridStatus.Uploading:
|
||||||
|
return `Torrent uploading`;
|
||||||
|
default:
|
||||||
|
return 'Unknown status';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unpackingCount > 0) {
|
|
||||||
const progress = ((unpackingBytesDone / unpackingBytesTotal) || 0) * 100;
|
|
||||||
|
|
||||||
return `Extracting file ${unpackingCount + unpackedCount}/${downloads.length} (${progress.toFixed(2)}%)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queuedForUnpackingCount > 0) {
|
|
||||||
return 'Queued for unpacking';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queuedForDownloadingCount > 0) {
|
|
||||||
return 'Queued for downloading';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unpackedCount > 0) {
|
|
||||||
return 'Files unpacked';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloadedCount > 0) {
|
|
||||||
return 'Files downloaded to host';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (torrent.completed) {
|
|
||||||
return 'Finished';
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefix = torrent.type === 1 ? 'NZB' : 'Torrent';
|
|
||||||
|
|
||||||
switch (torrent.rdStatus) {
|
|
||||||
case RealDebridStatus.Queued:
|
|
||||||
return 'Not Yet Added to Provider';
|
|
||||||
case RealDebridStatus.Downloading:
|
|
||||||
if (torrent.rdSeeders < 1 && torrent.type !== 1) {
|
|
||||||
return 'Torrent stalled';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${prefix} downloading (${torrent.rdProgress}% - ${fileSizePipe.transform(torrent.rdSpeed, 'filesize') as string}/s)`;
|
|
||||||
case RealDebridStatus.Processing:
|
|
||||||
return `${prefix} processing`;
|
|
||||||
case RealDebridStatus.WaitingForFileSelection:
|
|
||||||
return `${prefix} waiting for file selection`;
|
|
||||||
case RealDebridStatus.Error:
|
|
||||||
return `${prefix} error: ${torrent.rdStatusRaw}`;
|
|
||||||
case RealDebridStatus.Finished:
|
|
||||||
return `${prefix} finished, waiting for download links`;
|
|
||||||
case RealDebridStatus.Uploading:
|
|
||||||
return `${prefix} uploading`;
|
|
||||||
default:
|
|
||||||
return 'Unknown status';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Pipe({ name: 'status' })
|
|
||||||
export class TorrentStatusPipe implements PipeTransform {
|
|
||||||
transform(torrent: Torrent): string {
|
|
||||||
return getTorrentStatus(torrent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,362 +1,44 @@
|
||||||
@if (error && error.length > 0) {
|
<div class="notification is-danger is-light" *ngIf="error && error.length > 0">
|
||||||
<div class="notification is-danger is-light">
|
An error has occured: {{ error }}<br />
|
||||||
An error has occurred: {{ error }}<br />
|
Please refresh the screen after fixing this error.
|
||||||
Please refresh the screen after fixing this error.
|
</div>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (diskSpaceStatus?.isPaused) {
|
|
||||||
<div class="notification is-warning">
|
|
||||||
<strong>Bezzad downloads paused due to low disk space</strong>
|
|
||||||
<br />
|
|
||||||
Available: {{ diskSpaceStatus.availableSpaceGB }} GB | Resume at: {{ diskSpaceStatus.thresholdGB * 2 }} GB
|
|
||||||
<br />
|
|
||||||
<small>Last check: {{ diskSpaceStatus.lastCheckTime | date: "short" }}</small>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (rateLimitStatus?.nextDequeueTime) {
|
|
||||||
<div class="notification is-warning">
|
|
||||||
<strong>Debrid provider rate limit reached</strong>
|
|
||||||
<br />
|
|
||||||
Processing paused until {{ rateLimitStatus.nextDequeueTime | date: "medium" }}
|
|
||||||
@if (rateLimitStatus.secondsRemaining > 0) {
|
|
||||||
({{ rateLimitStatus.secondsRemaining | number: "1.0-0" }} seconds remaining)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
@if (!isMobile) {
|
<table class="table is-fullwidth is-hoverable">
|
||||||
<table class="table is-fullwidth is-hoverable">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th>Name</th>
|
||||||
<th>
|
<th>Category</th>
|
||||||
<input
|
<th>Priority</th>
|
||||||
type="checkbox"
|
<th>Files</th>
|
||||||
(click)="toggleDeleteSelectAll($event)"
|
<th>Downloads</th>
|
||||||
[checked]="selectedTorrents.length > 0 && selectedTorrents.length === torrents.length"
|
<th>Size</th>
|
||||||
/>
|
<th>Status</th>
|
||||||
</th>
|
</tr>
|
||||||
<th (click)="sort('rdName')">Name</th>
|
</thead>
|
||||||
<th (click)="sort('category')">Category</th>
|
<tbody>
|
||||||
<th (click)="sort('priority')">Priority</th>
|
<tr (click)="selectTorrent(torrent.torrentId)" *ngFor="let torrent of torrents; trackBy: trackByMethod">
|
||||||
<th (click)="sort('rdSeeders')">Seeders</th>
|
<td>
|
||||||
<th (click)="sort('files.length')">Files</th>
|
{{ torrent.rdName }}
|
||||||
<th (click)="sort('downloads.length')">Downloads</th>
|
</td>
|
||||||
<th (click)="sort('rdSize')">Size</th>
|
<td>
|
||||||
<th (click)="sort('added')">Requested</th>
|
{{ torrent.category }}
|
||||||
<th (click)="sort('status')">Status</th>
|
</td>
|
||||||
</tr>
|
<td>
|
||||||
</thead>
|
{{ torrent.priority }}
|
||||||
<tbody>
|
</td>
|
||||||
@for (torrent of sortedTorrents; track torrent.torrentId) {
|
<td>
|
||||||
<tr>
|
{{ torrent.files.length | number }}
|
||||||
<td>
|
</td>
|
||||||
<input type="checkbox" (click)="toggleSelect(torrent.torrentId)" [checked]="isSelected(torrent.torrentId)" />
|
<td>
|
||||||
</td>
|
{{ torrent.downloads.length | number }}
|
||||||
<td (click)="openTorrent(torrent.torrentId)" class="break-all">
|
</td>
|
||||||
{{ torrent.rdName }}
|
<td>
|
||||||
</td>
|
{{ torrent.rdSize | filesize }}
|
||||||
<td>
|
</td>
|
||||||
{{ torrent.category }}
|
<td>
|
||||||
</td>
|
{{ torrent | status }}
|
||||||
<td>
|
</td>
|
||||||
{{ torrent.priority }}
|
</tr>
|
||||||
</td>
|
</tbody>
|
||||||
<td>
|
</table>
|
||||||
{{ torrent.rdSeeders }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ (torrent.filesCount ?? torrent.files?.length ?? 0) | number }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ (torrent.downloadsCount ?? torrent.downloads?.length ?? 0) | number }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ torrent.rdSize | filesize }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ torrent.added | date: "medium" }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ torrent.statusText ?? (torrent | status) }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
} @else {
|
|
||||||
<div class="mobile-cards">
|
|
||||||
@for (torrent of sortedTorrents; track torrent.torrentId) {
|
|
||||||
<div class="box mobile-card" [class.is-selected]="isSelected(torrent.torrentId)">
|
|
||||||
<div class="mobile-card-checkbox">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
(click)="toggleSelect(torrent.torrentId)"
|
|
||||||
[checked]="isSelected(torrent.torrentId)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mobile-card-body" (click)="openTorrent(torrent.torrentId)">
|
|
||||||
<p class="mobile-card-name">{{ torrent.rdName }}</p>
|
|
||||||
<div class="tags mb-2">
|
|
||||||
<span class="tag is-info is-light">{{ torrent.statusText ?? (torrent | status) }}</span>
|
|
||||||
@if (torrent.category) {
|
|
||||||
<span class="tag is-light">{{ torrent.category }}</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="mobile-card-details">
|
|
||||||
<span class="mobile-card-detail"
|
|
||||||
><span class="detail-label">Size</span> {{ torrent.rdSize | filesize }}</span
|
|
||||||
>
|
|
||||||
<span class="mobile-card-detail"><span class="detail-label">Seeders</span> {{ torrent.rdSeeders }}</span>
|
|
||||||
<span class="mobile-card-detail"
|
|
||||||
><span class="detail-label">Files</span> {{ (torrent.filesCount ?? torrent.files?.length ?? 0) | number }}</span
|
|
||||||
>
|
|
||||||
<span class="mobile-card-detail"
|
|
||||||
><span class="detail-label">Downloads</span> {{ (torrent.downloadsCount ?? torrent.downloads?.length ?? 0) | number }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="flex-container">
|
|
||||||
@if (torrents.length > 0) {
|
|
||||||
<button class="button is-danger" (click)="showDeleteModal()" [disabled]="selectedTorrents.length === 0">
|
|
||||||
Delete Selected
|
|
||||||
</button>
|
|
||||||
<button class="button is-primary" (click)="showRetryModal()" [disabled]="selectedTorrents.length === 0">
|
|
||||||
Retry Selected
|
|
||||||
</button>
|
|
||||||
<button class="button is-primary" (click)="changeSettingsModal()" [disabled]="selectedTorrents.length === 0">
|
|
||||||
Change Settings
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</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 selected torrents</p>
|
|
||||||
<button class="delete" aria-label="close" (click)="deleteCancel()"></button>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body">
|
|
||||||
<p>Are you sure you want to delete these torrent?</p>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label"></label>
|
|
||||||
<div class="control">
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" [(ngModel)]="deleteData" (change)="updateDeleteSelectAll()" />
|
|
||||||
Delete Torrents from client
|
|
||||||
</label>
|
|
||||||
<br />
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" [(ngModel)]="deleteRdTorrent" (change)="updateDeleteSelectAll()" />
|
|
||||||
Delete Torrents from provider
|
|
||||||
</label>
|
|
||||||
<br />
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" [(ngModel)]="deleteLocalFiles" (change)="updateDeleteSelectAll()" />
|
|
||||||
Delete local files
|
|
||||||
</label>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" [(ngModel)]="deleteSelectAll" (change)="toggleDeleteSelectAllOptions()" />
|
|
||||||
Select All
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="notification is-primary">
|
|
||||||
Deleting a torrent from Real-Debrid will automatically delete it here too.
|
|
||||||
</div>
|
|
||||||
@if (deleteError?.length > 0) {
|
|
||||||
<div class="notification is-danger is-light">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 selected
|
|
||||||
</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 selected torrents</p>
|
|
||||||
<button class="delete" aria-label="close" (click)="retryCancel()"></button>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body">
|
|
||||||
<p>Are you sure you want to retry these 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 the debrid provider.
|
|
||||||
</p>
|
|
||||||
@if (retryError?.length > 0) {
|
|
||||||
<div class="notification is-danger is-light">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 selected
|
|
||||||
</button>
|
|
||||||
<button class="button" (click)="retryCancel()" [disabled]="retrying" [ngClass]="{ 'is-loading': retrying }">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal" [class.is-active]="isChangeSettingsModalActive">
|
|
||||||
<div class="modal-background"></div>
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<p class="modal-card-title">Change settings for selected torrents</p>
|
|
||||||
<button class="delete" aria-label="close" (click)="changeSettingsCancel()"></button>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Downloader</label>
|
|
||||||
<div class="control select is-fullwidth">
|
|
||||||
<select [(ngModel)]="updateSettingsDownloadClient">
|
|
||||||
<option [ngValue]="null"></option>
|
|
||||||
<option [ngValue]="0">Bezzad</option>
|
|
||||||
<option [ngValue]="1">Aria2c</option>
|
|
||||||
<option [ngValue]="2">Symlink Downloader</option>
|
|
||||||
<option [ngValue]="3">Synology DownloadStation</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<p class="help">
|
|
||||||
Select which downloader is used to download this torrent from the debrid provider to your host.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Post Download Action</label>
|
|
||||||
<div class="control select is-fullwidth">
|
|
||||||
<select [(ngModel)]="updateSettingsHostDownloadAction">
|
|
||||||
<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">Category</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="text" [(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>
|
|
||||||
|
|
||||||
@if (changeSettingsError?.length > 0) {
|
|
||||||
<div class="notification is-danger is-light">Error changing settings: {{ changeSettingsError }}</div>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<button
|
|
||||||
class="button is-success"
|
|
||||||
(click)="changeSettingsOk()"
|
|
||||||
[disabled]="changingSettings"
|
|
||||||
[ngClass]="{ 'is-loading': changingSettings }"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="button"
|
|
||||||
(click)="changeSettingsCancel()"
|
|
||||||
[disabled]="changingSettings"
|
|
||||||
[ngClass]="{ 'is-loading': changingSettings }"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,96 +1,5 @@
|
||||||
table {
|
table {
|
||||||
tr {
|
tr {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
td.break-all {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-cards {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.875rem 0.75rem !important;
|
|
||||||
margin-bottom: 0.5rem !important;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.is-selected {
|
|
||||||
border-left: 3px solid hsl(217, 71%, 53%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: hsl(0, 0%, 96%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-card-checkbox {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding-top: 0.125rem;
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
width: 1.1rem;
|
|
||||||
height: 1.1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-card-body {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-card-name {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-bottom: 0.375rem;
|
|
||||||
color: hsl(0, 0%, 21%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-card-details {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.25rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-card-detail {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: hsl(0, 0%, 29%);
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.detail-label {
|
|
||||||
color: hsl(0, 0%, 48%);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
margin-right: 0.2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-container {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0;
|
|
||||||
gap: 20px;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-top: 1rem;
|
|
||||||
|
|
||||||
@media screen and (max-width: 1279px) {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,400 +1,39 @@
|
||||||
import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Torrent } from '../models/torrent.model';
|
import { Torrent } from '../models/torrent.model';
|
||||||
import { DiskSpaceStatus } from '../models/disk-space-status.model';
|
|
||||||
import { RateLimitStatus } from '../models/rate-limit-status.model';
|
|
||||||
import { TorrentService } from '../torrent.service';
|
import { TorrentService } from '../torrent.service';
|
||||||
import { forkJoin, Observable } from 'rxjs';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { NgClass, DecimalPipe, DatePipe } from '@angular/common';
|
|
||||||
import { getTorrentStatus, TorrentStatusPipe } from '../torrent-status.pipe';
|
|
||||||
import { SortDirection, getSortFieldValue, sortItems } from '../sort.pipe';
|
|
||||||
import { FileSizePipe } from '../filesize.pipe';
|
|
||||||
|
|
||||||
@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'],
|
||||||
imports: [FormsModule, NgClass, DecimalPipe, DatePipe, TorrentStatusPipe, FileSizePipe],
|
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
export class TorrentTableComponent implements OnInit, OnDestroy {
|
export class TorrentTableComponent implements OnInit {
|
||||||
private destroyRef = inject(DestroyRef);
|
|
||||||
private router = inject(Router);
|
|
||||||
private torrentService = inject(TorrentService);
|
|
||||||
private selectedTorrentIds = new Set<string>();
|
|
||||||
|
|
||||||
public torrents: Torrent[] = [];
|
public torrents: Torrent[] = [];
|
||||||
public sortedTorrents: Torrent[] = [];
|
|
||||||
public selectedTorrents: string[] = [];
|
|
||||||
public error: string;
|
public error: string;
|
||||||
public sortProperty = 'added';
|
|
||||||
public sortDirection: SortDirection = 'desc';
|
|
||||||
|
|
||||||
public isDeleteModalActive: boolean;
|
constructor(private router: Router, private torrentService: TorrentService) {}
|
||||||
public deleteError: string;
|
|
||||||
public deleting: boolean;
|
|
||||||
public deleteSelectAll: boolean;
|
|
||||||
public deleteData: boolean;
|
|
||||||
public deleteRdTorrent: boolean;
|
|
||||||
public deleteLocalFiles: boolean;
|
|
||||||
|
|
||||||
public isRetryModalActive: boolean;
|
|
||||||
public retryError: string;
|
|
||||||
public retrying: boolean;
|
|
||||||
|
|
||||||
public isChangeSettingsModalActive: boolean;
|
|
||||||
public changeSettingsError: string;
|
|
||||||
public changingSettings: boolean;
|
|
||||||
|
|
||||||
public updateSettingsDownloadClient: number;
|
|
||||||
public updateSettingsHostDownloadAction: number;
|
|
||||||
public updateSettingsCategory: string;
|
|
||||||
public updateSettingsPriority: number;
|
|
||||||
public updateSettingsDownloadRetryAttempts: number;
|
|
||||||
public updateSettingsTorrentRetryAttempts: number;
|
|
||||||
public updateSettingsDeleteOnError: number;
|
|
||||||
public updateSettingsTorrentLifetime: number;
|
|
||||||
|
|
||||||
public diskSpaceStatus: DiskSpaceStatus | null = null;
|
|
||||||
public rateLimitStatus: RateLimitStatus | null = null;
|
|
||||||
|
|
||||||
public isMobile = false;
|
|
||||||
private mobileQuery: MediaQueryList;
|
|
||||||
private mobileQueryListener: (e: MediaQueryListEvent) => void;
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.mobileQuery = window.matchMedia('(max-width: 768px)');
|
this.torrentService.getList().subscribe(
|
||||||
this.isMobile = this.mobileQuery.matches;
|
(result) => {
|
||||||
this.mobileQueryListener = (e: MediaQueryListEvent) => {
|
this.torrents = result;
|
||||||
this.isMobile = e.matches;
|
|
||||||
};
|
|
||||||
this.mobileQuery.addEventListener('change', this.mobileQueryListener);
|
|
||||||
|
|
||||||
// Load persisted sort settings (if any)
|
this.torrentService.update$.subscribe((result2) => {
|
||||||
try {
|
this.torrents = result2;
|
||||||
const sp = localStorage.getItem('torrentTable.sortProperty');
|
});
|
||||||
const sd = localStorage.getItem('torrentTable.sortDirection');
|
},
|
||||||
if (sp) {
|
(err) => {
|
||||||
this.sortProperty = sp;
|
this.error = err.error;
|
||||||
}
|
}
|
||||||
if (sd === 'asc' || sd === 'desc') {
|
);
|
||||||
this.sortDirection = sd as 'asc' | 'desc';
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// Ignore storage errors (e.g., disabled storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.torrentService
|
|
||||||
.getDiskSpaceStatus()
|
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe({
|
|
||||||
next: (status) => {
|
|
||||||
this.diskSpaceStatus = status;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.torrentService.diskSpaceStatus$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((status) => {
|
|
||||||
this.diskSpaceStatus = status;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.torrentService
|
|
||||||
.getRateLimitStatus()
|
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe({
|
|
||||||
next: (status) => {
|
|
||||||
this.rateLimitStatus = status;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.torrentService.rateLimitStatus$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((status) => {
|
|
||||||
this.rateLimitStatus = status;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.torrentService.update$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((result) => {
|
|
||||||
this.setTorrents(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.torrentService
|
|
||||||
.getList()
|
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe({
|
|
||||||
next: (result) => {
|
|
||||||
this.setTorrents(result);
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
this.error = err.error;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sort(property: string): void {
|
public selectTorrent(torrentId: string): void {
|
||||||
if (this.sortProperty === property) {
|
|
||||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
||||||
} else {
|
|
||||||
this.sortProperty = property;
|
|
||||||
this.sortDirection = 'desc';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist sort settings
|
|
||||||
try {
|
|
||||||
localStorage.setItem('torrentTable.sortProperty', this.sortProperty);
|
|
||||||
localStorage.setItem('torrentTable.sortDirection', this.sortDirection);
|
|
||||||
} catch (_) {
|
|
||||||
// Ignore storage errors
|
|
||||||
}
|
|
||||||
|
|
||||||
this.applySorting();
|
|
||||||
}
|
|
||||||
|
|
||||||
public openTorrent(torrentId: string): void {
|
|
||||||
this.router.navigate([`/torrent/${torrentId}`]);
|
this.router.navigate([`/torrent/${torrentId}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleDeleteSelectAll(event: Event) {
|
public trackByMethod(index: number, el: Torrent): string {
|
||||||
const checked = (event.target as HTMLInputElement).checked;
|
return el.torrentId;
|
||||||
|
|
||||||
this.selectedTorrentIds = checked ? new Set(this.torrents.map((torrent) => torrent.torrentId)) : new Set<string>();
|
|
||||||
this.syncSelectedTorrents();
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggleSelect(torrentId: string) {
|
|
||||||
if (this.selectedTorrentIds.has(torrentId)) {
|
|
||||||
this.selectedTorrentIds.delete(torrentId);
|
|
||||||
} else {
|
|
||||||
this.selectedTorrentIds.add(torrentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncSelectedTorrents();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isSelected(torrentId: string): boolean {
|
|
||||||
return this.selectedTorrentIds.has(torrentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const calls: Observable<void>[] = [];
|
|
||||||
|
|
||||||
this.selectedTorrents.forEach((torrentId) => {
|
|
||||||
calls.push(this.torrentService.delete(torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles));
|
|
||||||
});
|
|
||||||
|
|
||||||
forkJoin(calls).subscribe({
|
|
||||||
complete: () => {
|
|
||||||
this.isDeleteModalActive = false;
|
|
||||||
this.deleting = false;
|
|
||||||
|
|
||||||
this.clearSelectedTorrents();
|
|
||||||
},
|
|
||||||
error: (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;
|
|
||||||
|
|
||||||
const calls: Observable<void>[] = [];
|
|
||||||
|
|
||||||
this.selectedTorrents.forEach((torrentId) => {
|
|
||||||
calls.push(this.torrentService.retry(torrentId));
|
|
||||||
});
|
|
||||||
|
|
||||||
forkJoin(calls).subscribe({
|
|
||||||
complete: () => {
|
|
||||||
this.isRetryModalActive = false;
|
|
||||||
this.retrying = false;
|
|
||||||
|
|
||||||
this.clearSelectedTorrents();
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
this.retryError = err.error;
|
|
||||||
this.retrying = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public changeSettingsModal(): void {
|
|
||||||
this.changeSettingsError = null;
|
|
||||||
|
|
||||||
const selectedTorrents = this.getSelectedTorrentModels();
|
|
||||||
|
|
||||||
this.updateSettingsDownloadClient = selectedTorrents.every(
|
|
||||||
(m, _, arr) => m.downloadClient === arr[0].downloadClient,
|
|
||||||
)
|
|
||||||
? selectedTorrents[0].downloadClient
|
|
||||||
: null;
|
|
||||||
this.updateSettingsHostDownloadAction = selectedTorrents.every(
|
|
||||||
(m, _, arr) => m.hostDownloadAction === arr[0].hostDownloadAction,
|
|
||||||
)
|
|
||||||
? selectedTorrents[0].hostDownloadAction
|
|
||||||
: null;
|
|
||||||
this.updateSettingsCategory = selectedTorrents.every((m, _, arr) => m.category === arr[0].category)
|
|
||||||
? selectedTorrents[0].category
|
|
||||||
: null;
|
|
||||||
this.updateSettingsPriority = selectedTorrents.every((m, _, arr) => m.priority === arr[0].priority)
|
|
||||||
? selectedTorrents[0].priority
|
|
||||||
: null;
|
|
||||||
this.updateSettingsDownloadRetryAttempts = selectedTorrents.every(
|
|
||||||
(m, _, arr) => m.downloadRetryAttempts === arr[0].downloadRetryAttempts,
|
|
||||||
)
|
|
||||||
? selectedTorrents[0].downloadRetryAttempts
|
|
||||||
: null;
|
|
||||||
this.updateSettingsTorrentRetryAttempts = selectedTorrents.every(
|
|
||||||
(m, _, arr) => m.torrentRetryAttempts === arr[0].torrentRetryAttempts,
|
|
||||||
)
|
|
||||||
? selectedTorrents[0].torrentRetryAttempts
|
|
||||||
: null;
|
|
||||||
this.updateSettingsDeleteOnError = selectedTorrents.every((m, _, arr) => m.deleteOnError === arr[0].deleteOnError)
|
|
||||||
? selectedTorrents[0].deleteOnError
|
|
||||||
: null;
|
|
||||||
this.updateSettingsTorrentLifetime = selectedTorrents.every((m, _, arr) => m.lifetime === arr[0].lifetime)
|
|
||||||
? selectedTorrents[0].lifetime
|
|
||||||
: null;
|
|
||||||
|
|
||||||
this.isChangeSettingsModalActive = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public changeSettingsCancel(): void {
|
|
||||||
this.isChangeSettingsModalActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public changeSettingsOk(): void {
|
|
||||||
this.changingSettings = true;
|
|
||||||
|
|
||||||
const calls: Observable<void>[] = [];
|
|
||||||
|
|
||||||
const selectedTorrents = this.getSelectedTorrentModels();
|
|
||||||
|
|
||||||
selectedTorrents.forEach((torrent) => {
|
|
||||||
if (this.updateSettingsDownloadClient != null) {
|
|
||||||
torrent.downloadClient = this.updateSettingsDownloadClient;
|
|
||||||
}
|
|
||||||
if (this.updateSettingsHostDownloadAction != null) {
|
|
||||||
torrent.hostDownloadAction = this.updateSettingsHostDownloadAction;
|
|
||||||
}
|
|
||||||
if (this.updateSettingsCategory != null) {
|
|
||||||
torrent.category = this.updateSettingsCategory;
|
|
||||||
}
|
|
||||||
if (this.updateSettingsPriority != null) {
|
|
||||||
torrent.priority = this.updateSettingsPriority;
|
|
||||||
}
|
|
||||||
if (this.updateSettingsDownloadRetryAttempts != null) {
|
|
||||||
torrent.downloadRetryAttempts = this.updateSettingsDownloadRetryAttempts;
|
|
||||||
}
|
|
||||||
if (this.updateSettingsTorrentRetryAttempts != null) {
|
|
||||||
torrent.torrentRetryAttempts = this.updateSettingsTorrentRetryAttempts;
|
|
||||||
}
|
|
||||||
if (this.updateSettingsDeleteOnError != null) {
|
|
||||||
torrent.deleteOnError = this.updateSettingsDeleteOnError;
|
|
||||||
}
|
|
||||||
if (this.updateSettingsTorrentLifetime != null) {
|
|
||||||
torrent.lifetime = this.updateSettingsTorrentLifetime;
|
|
||||||
}
|
|
||||||
|
|
||||||
calls.push(this.torrentService.update(torrent));
|
|
||||||
});
|
|
||||||
|
|
||||||
forkJoin(calls).subscribe({
|
|
||||||
complete: () => {
|
|
||||||
this.isChangeSettingsModalActive = false;
|
|
||||||
this.changingSettings = false;
|
|
||||||
|
|
||||||
this.clearSelectedTorrents();
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
this.changeSettingsError = err.error;
|
|
||||||
this.changingSettings = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
toggleDeleteSelectAllOptions() {
|
|
||||||
this.deleteData = this.deleteSelectAll;
|
|
||||||
this.deleteRdTorrent = this.deleteSelectAll;
|
|
||||||
this.deleteLocalFiles = this.deleteSelectAll;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDeleteSelectAll() {
|
|
||||||
this.deleteSelectAll = this.deleteData && this.deleteRdTorrent && this.deleteLocalFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.mobileQuery?.removeEventListener('change', this.mobileQueryListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setTorrents(torrents: Torrent[]): void {
|
|
||||||
this.torrents = torrents;
|
|
||||||
this.pruneSelectedTorrents();
|
|
||||||
this.applySorting();
|
|
||||||
}
|
|
||||||
|
|
||||||
private applySorting(): void {
|
|
||||||
this.sortedTorrents = sortItems(this.torrents, this.sortProperty, this.sortDirection, (torrent, field) => {
|
|
||||||
switch (field) {
|
|
||||||
case 'files.length':
|
|
||||||
return torrent.filesCount ?? torrent.files?.length ?? 0;
|
|
||||||
case 'downloads.length':
|
|
||||||
return torrent.downloadsCount ?? torrent.downloads?.length ?? 0;
|
|
||||||
case 'status':
|
|
||||||
return torrent.statusText ?? getTorrentStatus(torrent);
|
|
||||||
default:
|
|
||||||
return getSortFieldValue(torrent, field);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSelectedTorrentModels(): Torrent[] {
|
|
||||||
return this.torrents.filter((torrent) => this.selectedTorrentIds.has(torrent.torrentId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private pruneSelectedTorrents(): void {
|
|
||||||
const torrentIds = new Set(this.torrents.map((torrent) => torrent.torrentId));
|
|
||||||
|
|
||||||
for (const torrentId of this.selectedTorrentIds) {
|
|
||||||
if (!torrentIds.has(torrentId)) {
|
|
||||||
this.selectedTorrentIds.delete(torrentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncSelectedTorrents();
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearSelectedTorrents(): void {
|
|
||||||
this.selectedTorrentIds.clear();
|
|
||||||
this.syncSelectedTorrents();
|
|
||||||
}
|
|
||||||
|
|
||||||
private syncSelectedTorrents(): void {
|
|
||||||
this.selectedTorrents = this.torrents
|
|
||||||
.filter((torrent) => this.selectedTorrentIds.has(torrent.torrentId))
|
|
||||||
.map((torrent) => torrent.torrentId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,18 @@
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable, inject } 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, TorrentFileAvailability } from './models/torrent.model';
|
import { Torrent, TorrentFileAvailability } from './models/torrent.model';
|
||||||
import { DiskSpaceStatus } from './models/disk-space-status.model';
|
|
||||||
import { RateLimitStatus } from './models/rate-limit-status.model';
|
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class TorrentService {
|
export class TorrentService {
|
||||||
private http = inject(HttpClient);
|
|
||||||
private baseHref = inject(APP_BASE_HREF);
|
|
||||||
|
|
||||||
public update$: Subject<Torrent[]> = new Subject();
|
public update$: Subject<Torrent[]> = new Subject();
|
||||||
public diskSpaceStatus$: Subject<DiskSpaceStatus> = new Subject();
|
|
||||||
public rateLimitStatus$: Subject<RateLimitStatus> = new Subject();
|
|
||||||
|
|
||||||
private connection: signalR.HubConnection;
|
private connection: signalR.HubConnection;
|
||||||
|
|
||||||
constructor() {
|
constructor(private http: HttpClient) {
|
||||||
this.connect();
|
this.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,54 +21,24 @@ export class TorrentService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connection = new signalR.HubConnectionBuilder()
|
this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build();
|
||||||
.withUrl(`${this.baseHref}hub`)
|
this.connection.start().catch((err) => console.error(err));
|
||||||
.withAutomaticReconnect()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
this.connection.on('update', (torrents: Torrent[]) => {
|
this.connection.on('update', (torrents: Torrent[]) => {
|
||||||
this.update$.next(torrents);
|
this.update$.next(torrents);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.connection.on('diskSpaceStatus', (status: any) => {
|
|
||||||
this.diskSpaceStatus$.next(status);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connection.on('rateLimitStatus', (status: any) => {
|
|
||||||
this.rateLimitStatus$.next(status);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connection.onreconnected(() => {
|
|
||||||
this.getDiskSpaceStatus().subscribe({
|
|
||||||
next: (status) => {
|
|
||||||
if (status) {
|
|
||||||
this.diskSpaceStatus$.next(status);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connection.start().catch((err) => console.error(err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getList(): Observable<Torrent[]> {
|
public getList(): Observable<Torrent[]> {
|
||||||
return this.http.get<Torrent[]>(`${this.baseHref}Api/Torrents`);
|
return this.http.get<Torrent[]>(`/Api/Torrents`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(torrentId: string): Observable<Torrent> {
|
public get(torrentId: string): Observable<Torrent> {
|
||||||
return this.http.get<Torrent>(`${this.baseHref}Api/Torrents/Get/${torrentId}`);
|
return this.http.get<Torrent>(`/Api/Torrents/Get/${torrentId}`);
|
||||||
}
|
|
||||||
|
|
||||||
public getDiskSpaceStatus(): Observable<DiskSpaceStatus | null> {
|
|
||||||
return this.http.get<DiskSpaceStatus | null>(`${this.baseHref}Api/Torrents/DiskSpaceStatus`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRateLimitStatus(): Observable<RateLimitStatus | null> {
|
|
||||||
return this.http.get<RateLimitStatus | null>(`${this.baseHref}Api/Torrents/RateLimitStatus`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public uploadMagnet(magnetLink: string, torrent: Torrent): Observable<void> {
|
public uploadMagnet(magnetLink: string, torrent: Torrent): Observable<void> {
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadMagnet`, {
|
return this.http.post<void>(`/Api/Torrents/UploadMagnet`, {
|
||||||
magnetLink,
|
magnetLink,
|
||||||
torrent,
|
torrent,
|
||||||
});
|
});
|
||||||
|
|
@ -86,25 +48,11 @@ export class TorrentService {
|
||||||
const formData: FormData = new FormData();
|
const formData: FormData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('formData', JSON.stringify({ torrent }));
|
formData.append('formData', JSON.stringify({ torrent }));
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadFile`, formData);
|
return this.http.post<void>(`/Api/Torrents/UploadFile`, formData);
|
||||||
}
|
|
||||||
|
|
||||||
public uploadNzbLink(nzbLink: string, torrent: Torrent): Observable<void> {
|
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadNzbLink`, {
|
|
||||||
nzbLink,
|
|
||||||
torrent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public uploadNzbFile(file: File, torrent: Torrent): Observable<void> {
|
|
||||||
const formData: FormData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
formData.append('formData', JSON.stringify({ torrent }));
|
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadNzbFile`, formData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkFilesMagnet(magnetLink: string): Observable<TorrentFileAvailability[]> {
|
public checkFilesMagnet(magnetLink: string): Observable<TorrentFileAvailability[]> {
|
||||||
return this.http.post<TorrentFileAvailability[]>(`${this.baseHref}Api/Torrents/CheckFilesMagnet`, {
|
return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFilesMagnet`, {
|
||||||
magnetLink,
|
magnetLink,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -112,16 +60,16 @@ export class TorrentService {
|
||||||
public checkFiles(file: File): Observable<TorrentFileAvailability[]> {
|
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<TorrentFileAvailability[]>(`${this.baseHref}Api/Torrents/CheckFiles`, formData);
|
return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFiles`, formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public delete(
|
public delete(
|
||||||
torrentId: string,
|
torrentId: string,
|
||||||
deleteData: boolean,
|
deleteData: boolean,
|
||||||
deleteRdTorrent: boolean,
|
deleteRdTorrent: boolean,
|
||||||
deleteLocalFiles: boolean,
|
deleteLocalFiles: boolean
|
||||||
): Observable<void> {
|
): Observable<void> {
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Torrents/Delete/${torrentId}`, {
|
return this.http.post<void>(`/Api/Torrents/Delete/${torrentId}`, {
|
||||||
deleteData,
|
deleteData,
|
||||||
deleteRdTorrent,
|
deleteRdTorrent,
|
||||||
deleteLocalFiles,
|
deleteLocalFiles,
|
||||||
|
|
@ -129,29 +77,14 @@ export class TorrentService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public retry(torrentId: string): Observable<void> {
|
public retry(torrentId: string): Observable<void> {
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Torrents/Retry/${torrentId}`, {});
|
return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public retryDownload(downloadId: string): Observable<void> {
|
public retryDownload(downloadId: string): Observable<void> {
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Torrents/RetryDownload/${downloadId}`, {});
|
return this.http.post<void>(`/Api/Torrents/RetryDownload/${downloadId}`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(torrent: Torrent): Observable<void> {
|
public update(torrent: Torrent): Observable<void> {
|
||||||
return this.http.put<void>(`${this.baseHref}Api/Torrents/Update`, torrent);
|
return this.http.put<void>(`/Api/Torrents/Update`, torrent);
|
||||||
}
|
|
||||||
|
|
||||||
public verifyRegex(
|
|
||||||
includeRegex: string,
|
|
||||||
excludeRegex: string,
|
|
||||||
magnetLink: string,
|
|
||||||
): Observable<{ includeError: string; excludeError: string; selectedFiles: TorrentFileAvailability[] }> {
|
|
||||||
return this.http.post<{ includeError: string; excludeError: string; selectedFiles: TorrentFileAvailability[] }>(
|
|
||||||
`${this.baseHref}Api/Torrents/VerifyRegex`,
|
|
||||||
{
|
|
||||||
includeRegex,
|
|
||||||
excludeRegex,
|
|
||||||
magnetLink,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,436 +12,327 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (torrent === null || torrent === undefined) {
|
<div *ngIf="torrent == null">
|
||||||
<div>
|
<div class="fa-3x">
|
||||||
<div class="fa-3x">
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="torrent != null">
|
||||||
|
<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">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>
|
||||||
|
</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>
|
</div>
|
||||||
} @else {
|
<div *ngIf="activeTab === 1">
|
||||||
<div>
|
<div class="field">
|
||||||
@if (activeTab === 0) {
|
<table class="table is-fullwidth">
|
||||||
<div class="flex-container">
|
<thead>
|
||||||
<div style="flex: 1 1 0">
|
<tr>
|
||||||
<div class="field is-grouped is-grouped-multiline action-buttons">
|
<th>ID</th>
|
||||||
<div class="control">
|
<th>Path</th>
|
||||||
<button class="button is-danger" (click)="showDeleteModal()">Delete Torrent</button>
|
<th>Size</th>
|
||||||
</div>
|
<th>Selected</th>
|
||||||
<div class="control">
|
</tr>
|
||||||
<button class="button is-primary" (click)="showRetryModal()">Retry Torrent</button>
|
</thead>
|
||||||
</div>
|
<tbody>
|
||||||
<div class="control">
|
<tr *ngFor="let file of torrent.files">
|
||||||
<button class="button is-light" (click)="showUpdateSettingsModal()">Change Settings</button>
|
<td>
|
||||||
</div>
|
{{ file.id }}
|
||||||
</div>
|
</td>
|
||||||
<div class="field">
|
<td>
|
||||||
<label class="label">Status</label>
|
{{ file.path }}
|
||||||
{{ torrent | status }}
|
</td>
|
||||||
</div>
|
<td>
|
||||||
<div class="field">
|
{{ file.bytes | filesize }}
|
||||||
<label class="label">Retry count</label>
|
</td>
|
||||||
{{ torrent.retryCount }} / {{ torrent.torrentRetryAttempts }}
|
<td>
|
||||||
</div>
|
<i class="fas fa-check" *ngIf="file.selected" style="color: green"></i>
|
||||||
<div class="field">
|
<i class="fas fa-times" *ngIf="!file.selected" style="color: red"></i>
|
||||||
<label class="label">Hash</label>
|
</td>
|
||||||
<span class="break-all">{{ torrent.hash }}</span>
|
</tr>
|
||||||
</div>
|
</tbody>
|
||||||
<div class="field">
|
</table>
|
||||||
<label class="label">Priority</label>
|
</div>
|
||||||
{{ torrent.priority || "" }}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Category</label>
|
|
||||||
{{ torrent.category || "(no category set)" }}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Downloader</label>
|
|
||||||
@switch (torrent.downloadClient) {
|
|
||||||
@case (0) {
|
|
||||||
Bezadd
|
|
||||||
}
|
|
||||||
@case (1) {
|
|
||||||
Aria2c
|
|
||||||
}
|
|
||||||
@case (2) {
|
|
||||||
Symlink Downloader
|
|
||||||
}
|
|
||||||
@case (3) {
|
|
||||||
Synology DownloadStation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Post Download Action</label>
|
|
||||||
@switch (torrent.hostDownloadAction) {
|
|
||||||
@case (0) {
|
|
||||||
Download all files to host
|
|
||||||
}
|
|
||||||
@case (1) {
|
|
||||||
Don't download files to host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Post Torrent Download Action</label>
|
|
||||||
@switch (torrent.downloadAction) {
|
|
||||||
@case (0) {
|
|
||||||
Download all files above a certain size
|
|
||||||
}
|
|
||||||
@case (1) {
|
|
||||||
Download all available files on Real-Debrid above a certain size
|
|
||||||
}
|
|
||||||
@case (2) {
|
|
||||||
Pick files I want to download
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Finished action</label>
|
|
||||||
@switch (torrent.finishedAction) {
|
|
||||||
@case (0) {
|
|
||||||
Do nothing
|
|
||||||
}
|
|
||||||
@case (1) {
|
|
||||||
Remove torrent from Real-Debrid and Real-Debrid Client
|
|
||||||
}
|
|
||||||
@case (2) {
|
|
||||||
Remove torrent from Real-Debrid
|
|
||||||
}
|
|
||||||
@case (3) {
|
|
||||||
Remove torrent from client
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Finished action delay</label>
|
|
||||||
{{ torrent.finishedActionDelay || 0 }}
|
|
||||||
@if (torrent.finishedActionDelay === 1) {
|
|
||||||
minute
|
|
||||||
} @else {
|
|
||||||
minutes
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Minimum file size to download</label>
|
|
||||||
{{ torrent.downloadMinSize }}MB
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Include files</label>
|
|
||||||
{{ torrent.includeRegex }}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Exclude files</label>
|
|
||||||
{{ torrent.excludeRegex }}
|
|
||||||
</div>
|
|
||||||
@if (!torrent.isFile) {
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Magnet</label>
|
|
||||||
@if (!copied) {
|
|
||||||
<span [cdkCopyToClipboard]="torrent.fileOrMagnet" (click)="copied = true">
|
|
||||||
Click to copy magnet link to clipboard
|
|
||||||
</span>
|
|
||||||
} @else {
|
|
||||||
<span>Link copied to clipboard!</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
<div class="field">
|
|
||||||
<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>
|
|
||||||
@if (torrent.filesSelected === null) {
|
|
||||||
(no files selected yet)
|
|
||||||
} @else {
|
|
||||||
{{ torrent.filesSelected | date: "fullDate" }} {{ torrent.filesSelected | date: "mediumTime" }}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Completed on</label>
|
|
||||||
@if (torrent.completed === null) {
|
|
||||||
(not completed yet)
|
|
||||||
} @else {
|
|
||||||
{{ torrent.completed | date: "fullDate" }} {{ torrent.completed | date: "mediumTime" }}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="flex: 1 1 0">
|
|
||||||
<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>
|
|
||||||
@switch (torrent.rdStatus) {
|
|
||||||
@case (0) {
|
|
||||||
Processing
|
|
||||||
}
|
|
||||||
@case (1) {
|
|
||||||
Waiting For File Selection
|
|
||||||
}
|
|
||||||
@case (2) {
|
|
||||||
Downloading
|
|
||||||
}
|
|
||||||
@case (3) {
|
|
||||||
Finished
|
|
||||||
}
|
|
||||||
@case (99) {
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
({{ 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>
|
|
||||||
}
|
|
||||||
@if (activeTab === 1) {
|
|
||||||
<div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="table-container">
|
|
||||||
<table class="table is-fullwidth">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Path</th>
|
|
||||||
<th>Size</th>
|
|
||||||
<th>Selected</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@for (file of torrent.files; track file.id) {
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{{ file.id }}
|
|
||||||
</td>
|
|
||||||
<td class="break-all">
|
|
||||||
{{ file.path }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ file.bytes | filesize }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@if (file.selected) {
|
|
||||||
<i class="fas fa-check" style="color: green"></i>
|
|
||||||
} @else {
|
|
||||||
<i class="fas fa-times" style="color: red"></i>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (activeTab === 2) {
|
|
||||||
<div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="table-container">
|
|
||||||
<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>
|
|
||||||
@for (download of torrent.downloads; track download.downloadId) {
|
|
||||||
@let expanded = downloadExpanded[download.downloadId];
|
|
||||||
<tr (click)="downloadExpanded[download.downloadId] = !expanded">
|
|
||||||
<td style="width: 35px">
|
|
||||||
@if (!expanded) {
|
|
||||||
<i class="fas fa-caret-right"></i>
|
|
||||||
} @else {
|
|
||||||
<i class="fas fa-caret-down"></i>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td class="break-all">
|
|
||||||
@if (download.link) {
|
|
||||||
{{ download.link | decodeURI }}
|
|
||||||
}
|
|
||||||
@if (!download.link) {
|
|
||||||
{{ download.path }}
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ download.bytesTotal | filesize }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ download | downloadStatus }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@if (expanded) {
|
|
||||||
<tr class="separator">
|
|
||||||
<td style="width: 35px"></td>
|
|
||||||
<td colspan="5">
|
|
||||||
<div class="flex-container">
|
|
||||||
<div style="flex: 1 1 0">
|
|
||||||
<div class="field is-grouped">
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-primary" (click)="showDownloadRetryModal(download.downloadId)">
|
|
||||||
Retry Download
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@if (download.error) {
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Error</label>
|
|
||||||
{{ download.error }}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Real-Debrid Unrestricted Link</label>
|
|
||||||
@if (download.link) {
|
|
||||||
<a class="break-all" href="{{ download.link }}" target="_blank"> {{ download.link | decodeURI }}</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field break-all">
|
|
||||||
<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 style="flex: 1 1 0">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Added</label>
|
|
||||||
@if (download.added) {
|
|
||||||
{{ download.added | date: "fullDate" }} {{ download.added | date: "mediumTime" }}
|
|
||||||
} @else {
|
|
||||||
(not added yet)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Download Queued</label>
|
|
||||||
@if (download.downloadQueued) {
|
|
||||||
{{ download.downloadQueued | date: "fullDate" }}
|
|
||||||
{{ download.downloadQueued | date: "mediumTime" }}
|
|
||||||
} @else {
|
|
||||||
(not queued for downloading yet)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Download Started</label>
|
|
||||||
@if (download.downloadStarted) {
|
|
||||||
{{ download.downloadStarted | date: "fullDate" }}
|
|
||||||
{{ download.downloadStarted | date: "mediumTime" }}
|
|
||||||
} @else {
|
|
||||||
(not started downloading yet)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Download Finished</label>
|
|
||||||
@if (download.downloadFinished) {
|
|
||||||
{{ download.downloadFinished | date: "fullDate" }}
|
|
||||||
{{ download.downloadFinished | date: "mediumTime" }}
|
|
||||||
} @else {
|
|
||||||
(not finished yet)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Unpacking Queued</label>
|
|
||||||
@if (download.unpackingQueued) {
|
|
||||||
{{ download.unpackingQueued | date: "fullDate" }}
|
|
||||||
{{ download.unpackingQueued | date: "mediumTime" }}
|
|
||||||
} @else {
|
|
||||||
(not queued for unpacking yet)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Unpacking Started</label>
|
|
||||||
@if (download.unpackingStarted) {
|
|
||||||
{{ download.unpackingStarted | date: "fullDate" }}
|
|
||||||
{{ download.unpackingStarted | date: "mediumTime" }}
|
|
||||||
} @else {
|
|
||||||
(not started unpacking yet)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Unpacking Finished</label>
|
|
||||||
@if (download.unpackingFinished) {
|
|
||||||
{{ download.unpackingFinished | date: "fullDate" }}
|
|
||||||
{{ download.unpackingFinished | date: "mediumTime" }}
|
|
||||||
} @else {
|
|
||||||
(not finished unpacking yet)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Completed</label>
|
|
||||||
@if (download.completed) {
|
|
||||||
{{ download.completed | date: "fullDate" }}
|
|
||||||
{{ download.completed | date: "mediumTime" }}
|
|
||||||
} @else {
|
|
||||||
(not completed yet)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</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" [class.is-active]="isDeleteModalActive">
|
||||||
<div class="modal-background"></div>
|
<div class="modal-background"></div>
|
||||||
|
|
@ -456,33 +347,27 @@
|
||||||
<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" (change)="updateDeleteSelectAll()" />
|
<input type="checkbox" [(ngModel)]="deleteData" />
|
||||||
Delete Torrent from client
|
Delete Torrent from client
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="deleteRdTorrent" (change)="updateDeleteSelectAll()" />
|
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
|
||||||
Delete Torrent from provider
|
Delete Torrent from provider
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="deleteLocalFiles" (change)="updateDeleteSelectAll()" />
|
<input type="checkbox" [(ngModel)]="deleteLocalFiles" />
|
||||||
Delete local files
|
Delete local files
|
||||||
</label>
|
</label>
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" [(ngModel)]="deleteSelectAll" (change)="toggleDeleteSelectAllOptions()" />
|
|
||||||
Select All
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="notification is-primary">
|
<div class="notification is-primary">
|
||||||
Deleting a torrent from Real-Debrid will automatically delete it here too.
|
Deleting a torrent from Real-Debrid will automatically delete it here too.
|
||||||
</div>
|
</div>
|
||||||
@if (deleteError?.length > 0) {
|
<div class="notification is-danger is-light" *ngIf="deleteError?.length > 0">
|
||||||
<div class="notification is-danger is-light">Error deleting torrent: {{ deleteError }}</div>
|
Error deleting torrent: {{ deleteError }}
|
||||||
}
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
<button
|
<button
|
||||||
|
|
@ -513,9 +398,9 @@
|
||||||
This action will delete all the torrent data + all local downloads. Then it will re-add the original magnet link
|
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.
|
or torrent file to Real-Debrid.
|
||||||
</p>
|
</p>
|
||||||
@if (retryError?.length > 0) {
|
<div class="notification is-danger is-light" *ngIf="retryError?.length > 0">
|
||||||
<div class="notification is-danger is-light">Error retrying torrent: {{ retryError }}</div>
|
Error retrying torrent: {{ retryError }}
|
||||||
}
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
<button
|
<button
|
||||||
|
|
@ -543,9 +428,9 @@
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
<p>Are you sure you want to retry this download?</p>
|
<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>
|
<p>This action will remove the local download and re-download the file from Real-Debrid.</p>
|
||||||
@if (downloadRetryError?.length > 0) {
|
<div class="notification is-danger is-light" *ngIf="downloadRetryError?.length > 0">
|
||||||
<div class="notification is-danger is-light">Error retrying download: {{ downloadRetryError }}</div>
|
Error retrying download: {{ downloadRetryError }}
|
||||||
}
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
<button
|
<button
|
||||||
|
|
@ -576,45 +461,6 @@
|
||||||
<button class="delete" aria-label="close" (click)="updateSettingsCancel()"></button>
|
<button class="delete" aria-label="close" (click)="updateSettingsCancel()"></button>
|
||||||
</header>
|
</header>
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
<p>
|
|
||||||
Settings that are blank do not have the same values for each torrent. Updating a setting with a blank value will
|
|
||||||
not update it.
|
|
||||||
</p>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Downloader</label>
|
|
||||||
<div class="control select is-fullwidth">
|
|
||||||
<select [(ngModel)]="updateSettingsDownloadClient">
|
|
||||||
<option [ngValue]="0">Bezzad</option>
|
|
||||||
<option [ngValue]="1">Aria2c</option>
|
|
||||||
<option [ngValue]="2">Symlink Downloader</option>
|
|
||||||
<option [ngValue]="3">Synology DownloadStation</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<p class="help">
|
|
||||||
Select which downloader is used to download this torrent from the debrid provider to your host.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Post Download Action</label>
|
|
||||||
<div class="control select is-fullwidth">
|
|
||||||
<select [(ngModel)]="updateSettingsHostDownloadAction">
|
|
||||||
<option [ngValue]="null"></option>
|
|
||||||
<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">Category</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="text" [(ngModel)]="updateSettingsCategory" />
|
|
||||||
</div>
|
|
||||||
<p class="help">The category becomes a sub-folder in your main download path.</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Priority</label>
|
<label class="label">Priority</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
@ -655,33 +501,6 @@
|
||||||
error it will retry the full torrent this many times before marking it failed.
|
error it will retry the full torrent this many times before marking it failed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
<button
|
<button
|
||||||
|
|
@ -690,7 +509,7 @@
|
||||||
[disabled]="updating"
|
[disabled]="updating"
|
||||||
[ngClass]="{ 'is-loading': updating }"
|
[ngClass]="{ 'is-loading': updating }"
|
||||||
>
|
>
|
||||||
Save
|
Retry
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button"
|
class="button"
|
||||||
|
|
|
||||||
|
|
@ -8,38 +8,6 @@ table {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.break-all {
|
.fa-download {
|
||||||
word-break: break-all;
|
margin-left :12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-container {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0;
|
|
||||||
gap: 20px;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
@media screen and (max-width: 1279px) {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.action-buttons {
|
|
||||||
.control {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs ul {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
li {
|
|
||||||
flex-grow: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +1,15 @@
|
||||||
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { saveAs } from 'file-saver-es';
|
import { saveAs } from 'file-saver';
|
||||||
import { Torrent } from '../models/torrent.model';
|
import { Torrent } from '../models/torrent.model';
|
||||||
import { TorrentService } from '../torrent.service';
|
import { TorrentService } from '../torrent.service';
|
||||||
import { NgClass, DatePipe } from '@angular/common';
|
|
||||||
import { CdkCopyToClipboard } from '@angular/cdk/clipboard';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { TorrentStatusPipe } from '../torrent-status.pipe';
|
|
||||||
import { DownloadStatusPipe } from '../download-status.pipe';
|
|
||||||
import { DecodeURIPipe } from '../decode-uri.pipe';
|
|
||||||
import { FileSizePipe } from '../filesize.pipe';
|
|
||||||
import { EMPTY, distinctUntilChanged, map, switchMap, tap, catchError } from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-torrent',
|
selector: 'app-torrent',
|
||||||
templateUrl: './torrent.component.html',
|
templateUrl: './torrent.component.html',
|
||||||
styleUrls: ['./torrent.component.scss'],
|
styleUrls: ['./torrent.component.scss'],
|
||||||
imports: [
|
|
||||||
NgClass,
|
|
||||||
CdkCopyToClipboard,
|
|
||||||
FormsModule,
|
|
||||||
DatePipe,
|
|
||||||
TorrentStatusPipe,
|
|
||||||
DownloadStatusPipe,
|
|
||||||
DecodeURIPipe,
|
|
||||||
FileSizePipe,
|
|
||||||
],
|
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
export class TorrentComponent implements OnInit {
|
export class TorrentComponent implements OnInit {
|
||||||
private destroyRef = inject(DestroyRef);
|
|
||||||
private activatedRoute = inject(ActivatedRoute);
|
|
||||||
private router = inject(Router);
|
|
||||||
private torrentService = inject(TorrentService);
|
|
||||||
private currentTorrentId: string | null = null;
|
|
||||||
|
|
||||||
public torrent: Torrent;
|
public torrent: Torrent;
|
||||||
|
|
||||||
public activeTab: number = 0;
|
public activeTab: number = 0;
|
||||||
|
|
@ -47,7 +21,6 @@ export class TorrentComponent implements OnInit {
|
||||||
public isDeleteModalActive: boolean;
|
public isDeleteModalActive: boolean;
|
||||||
public deleteError: string;
|
public deleteError: string;
|
||||||
public deleting: boolean;
|
public deleting: boolean;
|
||||||
public deleteSelectAll: boolean;
|
|
||||||
public deleteData: boolean;
|
public deleteData: boolean;
|
||||||
public deleteRdTorrent: boolean;
|
public deleteRdTorrent: boolean;
|
||||||
public deleteLocalFiles: boolean;
|
public deleteLocalFiles: boolean;
|
||||||
|
|
@ -62,80 +35,53 @@ export class TorrentComponent implements OnInit {
|
||||||
public downloadRetryId: string;
|
public downloadRetryId: string;
|
||||||
|
|
||||||
public isUpdateSettingsModalActive: boolean;
|
public isUpdateSettingsModalActive: boolean;
|
||||||
|
|
||||||
public updateSettingsDownloadClient: number;
|
|
||||||
public updateSettingsHostDownloadAction: number;
|
|
||||||
public updateSettingsCategory: string;
|
|
||||||
public updateSettingsPriority: number;
|
public updateSettingsPriority: number;
|
||||||
public updateSettingsDownloadRetryAttempts: number;
|
public updateSettingsDownloadRetryAttempts: number;
|
||||||
public updateSettingsTorrentRetryAttempts: number;
|
public updateSettingsTorrentRetryAttempts: number;
|
||||||
public updateSettingsDeleteOnError: number;
|
|
||||||
public updateSettingsTorrentLifetime: number;
|
|
||||||
|
|
||||||
public updating: boolean;
|
public updating: boolean;
|
||||||
|
|
||||||
ngOnInit(): void {
|
constructor(private activatedRoute: ActivatedRoute, private router: Router, private torrentService: TorrentService) {}
|
||||||
this.activatedRoute.params
|
|
||||||
.pipe(
|
|
||||||
map((params) => params['id'] as string),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
tap((torrentId) => {
|
|
||||||
this.currentTorrentId = torrentId;
|
|
||||||
}),
|
|
||||||
switchMap((torrentId) =>
|
|
||||||
this.torrentService.get(torrentId).pipe(
|
|
||||||
catchError(() => {
|
|
||||||
this.router.navigate(['/']);
|
|
||||||
return EMPTY;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
takeUntilDestroyed(this.destroyRef),
|
|
||||||
)
|
|
||||||
.subscribe((torrent) => {
|
|
||||||
this.torrent = torrent;
|
|
||||||
this.currentTorrentId = torrent.torrentId;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.torrentService.update$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((result) => {
|
ngOnInit(): void {
|
||||||
this.update(result);
|
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 {
|
public update(torrents: Torrent[]): void {
|
||||||
const torrentId = this.currentTorrentId ?? this.torrent?.torrentId;
|
const updatedTorrent = torrents.firstOrDefault((m) => m.torrentId === this.torrent.torrentId);
|
||||||
|
|
||||||
if (!torrentId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedTorrent = torrents.find((m) => m.torrentId === torrentId);
|
|
||||||
|
|
||||||
if (updatedTorrent == null) {
|
if (updatedTorrent == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTorrent = this.torrent;
|
this.torrent = updatedTorrent;
|
||||||
const hasIncomingFiles = Array.isArray(updatedTorrent.files) && updatedTorrent.files.length > 0;
|
|
||||||
|
|
||||||
this.torrent = {
|
|
||||||
...currentTorrent,
|
|
||||||
...updatedTorrent,
|
|
||||||
fileOrMagnet: updatedTorrent.fileOrMagnet ?? currentTorrent?.fileOrMagnet,
|
|
||||||
files: hasIncomingFiles ? updatedTorrent.files : currentTorrent?.files ?? [],
|
|
||||||
downloads: updatedTorrent.downloads ?? currentTorrent?.downloads ?? [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public download(): void {
|
public download(): void {
|
||||||
const binaryString = window.atob(this.torrent.fileOrMagnet);
|
const byteArray = new Uint8Array(
|
||||||
const byteArray = new Uint8Array(binaryString.length);
|
window
|
||||||
|
.atob(this.torrent.fileOrMagnet)
|
||||||
|
.split('')
|
||||||
|
.map(function (c) {
|
||||||
|
return c.charCodeAt(0);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
for (let index = 0; index < binaryString.length; index += 1) {
|
var blob = new Blob([byteArray], { type: 'application/x-bittorrent' });
|
||||||
byteArray[index] = binaryString.charCodeAt(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([byteArray], { type: 'application/x-bittorrent' });
|
|
||||||
saveAs(blob, `${this.torrent.rdName}.torrent`);
|
saveAs(blob, `${this.torrent.rdName}.torrent`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,18 +103,18 @@ export class TorrentComponent implements OnInit {
|
||||||
|
|
||||||
this.torrentService
|
this.torrentService
|
||||||
.delete(this.torrent.torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
|
.delete(this.torrent.torrentId, this.deleteData, this.deleteRdTorrent, this.deleteLocalFiles)
|
||||||
.subscribe({
|
.subscribe(
|
||||||
next: () => {
|
() => {
|
||||||
this.isDeleteModalActive = false;
|
this.isDeleteModalActive = false;
|
||||||
this.deleting = false;
|
this.deleting = false;
|
||||||
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.deleteError = err.error;
|
this.deleteError = err.error;
|
||||||
this.deleting = false;
|
this.deleting = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public showRetryModal(): void {
|
public showRetryModal(): void {
|
||||||
|
|
@ -184,18 +130,18 @@ export class TorrentComponent implements OnInit {
|
||||||
public retryOk(): void {
|
public retryOk(): void {
|
||||||
this.retrying = true;
|
this.retrying = true;
|
||||||
|
|
||||||
this.torrentService.retry(this.torrent.torrentId).subscribe({
|
this.torrentService.retry(this.torrent.torrentId).subscribe(
|
||||||
next: () => {
|
() => {
|
||||||
this.isRetryModalActive = false;
|
this.isRetryModalActive = false;
|
||||||
this.retrying = false;
|
this.retrying = false;
|
||||||
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.retryError = err.error;
|
this.retryError = err.error;
|
||||||
this.retrying = false;
|
this.retrying = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public showDownloadRetryModal(downloadId: string): void {
|
public showDownloadRetryModal(downloadId: string): void {
|
||||||
|
|
@ -212,27 +158,22 @@ export class TorrentComponent implements OnInit {
|
||||||
public downloadRetryOk(): void {
|
public downloadRetryOk(): void {
|
||||||
this.downloadRetrying = true;
|
this.downloadRetrying = true;
|
||||||
|
|
||||||
this.torrentService.retryDownload(this.downloadRetryId).subscribe({
|
this.torrentService.retryDownload(this.downloadRetryId).subscribe(
|
||||||
next: () => {
|
() => {
|
||||||
this.isDownloadRetryModalActive = false;
|
this.isDownloadRetryModalActive = false;
|
||||||
this.downloadRetrying = false;
|
this.downloadRetrying = false;
|
||||||
},
|
},
|
||||||
error: (err) => {
|
(err) => {
|
||||||
this.downloadRetryError = err.error;
|
this.downloadRetryError = err.error;
|
||||||
this.downloadRetrying = false;
|
this.downloadRetrying = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public showUpdateSettingsModal(): void {
|
public showUpdateSettingsModal(): void {
|
||||||
this.updateSettingsDownloadClient = this.torrent.downloadClient;
|
|
||||||
this.updateSettingsHostDownloadAction = this.torrent.hostDownloadAction;
|
|
||||||
this.updateSettingsCategory = this.torrent.category;
|
|
||||||
this.updateSettingsPriority = this.torrent.priority;
|
this.updateSettingsPriority = this.torrent.priority;
|
||||||
this.updateSettingsDownloadRetryAttempts = this.torrent.downloadRetryAttempts;
|
this.updateSettingsDownloadRetryAttempts = this.torrent.downloadRetryAttempts;
|
||||||
this.updateSettingsTorrentRetryAttempts = this.torrent.torrentRetryAttempts;
|
this.updateSettingsTorrentRetryAttempts = this.torrent.torrentRetryAttempts;
|
||||||
this.updateSettingsDeleteOnError = this.torrent.deleteOnError;
|
|
||||||
this.updateSettingsTorrentLifetime = this.torrent.lifetime;
|
|
||||||
|
|
||||||
this.isUpdateSettingsModalActive = true;
|
this.isUpdateSettingsModalActive = true;
|
||||||
}
|
}
|
||||||
|
|
@ -244,33 +185,19 @@ export class TorrentComponent implements OnInit {
|
||||||
public updateSettingsOk(): void {
|
public updateSettingsOk(): void {
|
||||||
this.updating = true;
|
this.updating = true;
|
||||||
|
|
||||||
this.torrent.downloadClient = this.updateSettingsDownloadClient;
|
|
||||||
this.torrent.hostDownloadAction = this.updateSettingsHostDownloadAction;
|
|
||||||
this.torrent.category = this.updateSettingsCategory;
|
|
||||||
this.torrent.priority = this.updateSettingsPriority;
|
this.torrent.priority = this.updateSettingsPriority;
|
||||||
this.torrent.downloadRetryAttempts = this.updateSettingsDownloadRetryAttempts;
|
this.torrent.downloadRetryAttempts = this.updateSettingsDownloadRetryAttempts;
|
||||||
this.torrent.torrentRetryAttempts = this.updateSettingsTorrentRetryAttempts;
|
this.torrent.torrentRetryAttempts = this.updateSettingsTorrentRetryAttempts;
|
||||||
this.torrent.deleteOnError = this.updateSettingsDeleteOnError;
|
|
||||||
this.torrent.lifetime = this.updateSettingsTorrentLifetime;
|
|
||||||
|
|
||||||
this.torrentService.update(this.torrent).subscribe({
|
this.torrentService.update(this.torrent).subscribe(
|
||||||
next: () => {
|
() => {
|
||||||
this.isUpdateSettingsModalActive = false;
|
this.isUpdateSettingsModalActive = false;
|
||||||
this.updating = false;
|
this.updating = false;
|
||||||
},
|
},
|
||||||
error: () => {
|
() => {
|
||||||
this.isUpdateSettingsModalActive = false;
|
this.isUpdateSettingsModalActive = false;
|
||||||
this.updating = false;
|
this.updating = false;
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
}
|
|
||||||
toggleDeleteSelectAllOptions() {
|
|
||||||
this.deleteData = this.deleteSelectAll;
|
|
||||||
this.deleteRdTorrent = this.deleteSelectAll;
|
|
||||||
this.deleteLocalFiles = this.deleteSelectAll;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDeleteSelectAll() {
|
|
||||||
this.deleteSelectAll = this.deleteData && this.deleteRdTorrent && this.deleteLocalFiles;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
// 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 = {
|
||||||
production: false,
|
production: false
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|
@ -6,12 +6,8 @@
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const baseElement = document.querySelector("base");
|
window["_app_base"] = "/" + window.location.pathname.split("/")[1];
|
||||||
|
console.log("setting base href to " + window["_app_base"]);
|
||||||
if (baseElement) {
|
|
||||||
window["_app_base"] = baseElement.getAttribute("href");
|
|
||||||
console.log("setting base href to " + window["_app_base"]);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,12 @@
|
||||||
import { enableProdMode, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
import { environment } from './environments/environment';
|
import { environment } from './environments/environment';
|
||||||
import { FileSizePipe } from './app/filesize.pipe';
|
|
||||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
|
||||||
import { AuthInterceptor } from './app/auth.interceptor';
|
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
|
|
||||||
import { AppRoutingModule } from './app/app-routing.module';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
|
||||||
import { AppComponent } from './app/app.component';
|
|
||||||
|
|
||||||
if (environment.production) {
|
if (environment.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapApplication(AppComponent, {
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
providers: [
|
.catch(err => console.error(err));
|
||||||
provideZoneChangeDetection(),
|
|
||||||
importProvidersFrom(BrowserModule, AppRoutingModule, FormsModule, ClipboardModule),
|
|
||||||
FileSizePipe,
|
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
|
||||||
{ provide: APP_BASE_HREF, useValue: (window as any)['_app_base'] || '/' },
|
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
|
||||||
],
|
|
||||||
}).catch((err) => console.error(err));
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@
|
||||||
/***************************************************************************************************
|
/***************************************************************************************************
|
||||||
* Zone JS is required by default for Angular itself.
|
* Zone JS is required by default for Angular itself.
|
||||||
*/
|
*/
|
||||||
import 'zone.js'; // Included with Angular CLI.
|
import 'zone.js'; // Included with Angular CLI.
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************************************
|
/***************************************************************************************************
|
||||||
* APPLICATION IMPORTS
|
* APPLICATION IMPORTS
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
@forward "../node_modules/bulma/bulma.scss";
|
@import "../node_modules/bulma/bulma.sass";
|
||||||
@forward "../node_modules/@fortawesome/fontawesome-free/css/all.css";
|
@import "../node_modules/@fortawesome/fontawesome-free/css/all.css";
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@
|
||||||
"outDir": "./out-tsc/app",
|
"outDir": "./out-tsc/app",
|
||||||
"types": []
|
"types": []
|
||||||
},
|
},
|
||||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
"files": [
|
||||||
"include": ["src/**/*.d.ts"]
|
"src/main.ts",
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"outDir": "./dist/out-tsc",
|
"outDir": "./dist/out-tsc",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"esModuleInterop": true,
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
|
@ -13,13 +12,17 @@
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
|
"downlevelIteration": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "node",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false,
|
||||||
"target": "ES2022",
|
"target": "es2017",
|
||||||
"module": "es2020",
|
"module": "es2020",
|
||||||
"useDefineForClassFields": false
|
"lib": [
|
||||||
|
"es2020",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|
|
||||||
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
rdtclient:
|
||||||
|
image: rogerfar/rdtclient
|
||||||
|
volumes:
|
||||||
|
- ./data/downloads:/data/downloads
|
||||||
|
- ./data/db:/data/db
|
||||||
|
container_name: rdtclient
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 10m
|
||||||
|
ports:
|
||||||
|
- '6500:6500'
|
||||||
26
tools/docker-publish.ps1 → docker-publish.ps1
Normal file → Executable file
26
tools/docker-publish.ps1 → docker-publish.ps1
Normal file → Executable file
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env pwsh
|
#!/bin/pwsh
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Helper script to execute docker buildx
|
Helper script to execute docker buildx
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
The docker account to use to push the image
|
The docker account to use to push the image
|
||||||
|
|
||||||
.PARAMETER Platforms
|
.PARAMETER Platforms
|
||||||
The platforms to target for the image (defaults to linux/arm64/v8,linux/amd64)
|
The platforms to target for the image (defaults to linux/arm/v7,linux/arm64/v8,linux/amd64)
|
||||||
|
|
||||||
.INPUTS
|
.INPUTS
|
||||||
None. You cannot pipe objects to to this script.
|
None. You cannot pipe objects to to this script.
|
||||||
|
|
@ -34,29 +34,12 @@
|
||||||
param(
|
param(
|
||||||
[string]$Version = "",
|
[string]$Version = "",
|
||||||
[string]$DockerAccount = "rogerfar",
|
[string]$DockerAccount = "rogerfar",
|
||||||
[string]$Platforms = "linux/arm64/v8,linux/amd64",
|
[string]$Platforms = "linux/arm/v7,linux/arm64/v8,linux/amd64"
|
||||||
[string]$Dockerfile = "Dockerfile",
|
|
||||||
[switch]$SkipPush,
|
|
||||||
[switch]$SkipCache,
|
|
||||||
[switch]$OutputToDocker,
|
|
||||||
[string]$BuildProgress="auto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
$imageName = "$($DockerAccount)/rdtclient"
|
$imageName = "$($DockerAccount)/rdtclient"
|
||||||
|
|
||||||
$dockerArgs = @( "buildx", "build", "--network=default", "--platform", $Platforms, "--progress=$BuildProgress", "--tag", "$($imageName):latest", "--file", $Dockerfile, "." )
|
$dockerArgs = @( "buildx", "build", "--no-cache", "--push", "--platform", $Platforms, "--tag", "$($imageName):latest", "." )
|
||||||
|
|
||||||
if (-Not $SkipPush.IsPresent) {
|
|
||||||
$dockerArgs += @("--push")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($SkipCache.IsPresent) {
|
|
||||||
$dockerArgs += @("--no-cache")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($OutputToDocker.IsPresent) {
|
|
||||||
$dockerArgs += @("--load")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([string]::IsNullOrEmpty($Version)) {
|
if ([string]::IsNullOrEmpty($Version)) {
|
||||||
$Version = (Get-Content "package.json" | ConvertFrom-Json).version
|
$Version = (Get-Content "package.json" | ConvertFrom-Json).version
|
||||||
|
|
@ -66,5 +49,4 @@ $dockerArgs += @("--tag", "$($imageName):$($Version)" )
|
||||||
$dockerApps += @("--build-arg", "VERSION=$($Version)" )
|
$dockerApps += @("--build-arg", "VERSION=$($Version)" )
|
||||||
|
|
||||||
Write-Host "Generating docker image $imageName for $Platforms"
|
Write-Host "Generating docker image $imageName for $Platforms"
|
||||||
Write-Host "Args: $dockerArgs"
|
|
||||||
& docker $dockerArgs
|
& docker $dockerArgs
|
||||||
|
|
@ -3,17 +3,17 @@ $utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
|
||||||
$version = (Get-Content "package.json" | ConvertFrom-Json).version
|
$version = (Get-Content "package.json" | ConvertFrom-Json).version
|
||||||
|
|
||||||
$csProj = "$pwd\server\RdtClient.Web\RdtClient.Web.csproj"
|
$csProj = "$pwd\server\RdtClient.Web\RdtClient.Web.csproj"
|
||||||
|
$navbar = "$pwd\client\src\app\navbar\navbar.component.html";
|
||||||
|
|
||||||
$newCsProj = (Get-Content $csProj) -replace '<Version>.*?<\/Version>', "<Version>$version</Version>"
|
$newCsProj = (Get-Content $csProj) -replace '<Version>.*?<\/Version>', "<Version>$version</Version>"
|
||||||
[System.IO.File]::WriteAllLines($csProj, $newCsProj, $utf8NoBomEncoding)
|
[System.IO.File]::WriteAllLines($csProj, $newCsProj, $utf8NoBomEncoding)
|
||||||
|
|
||||||
Write-Output "Commit and push now, press any key to continue"
|
$newNavbar = (Get-Content $navbar) -replace 'Version .*?<', "Version $version<"
|
||||||
|
[System.IO.File]::WriteAllLines($navbar, $newNavbar, $utf8NoBomEncoding)
|
||||||
[Console]::ReadKey()
|
|
||||||
|
|
||||||
cd client
|
cd client
|
||||||
npm install
|
npm install
|
||||||
ng build
|
ng build --configuration production --output-path=..\server\RdtClient.Web\wwwroot
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
cd server
|
cd server
|
||||||
1988
package-lock.json
generated
Normal file
1988
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
20
package.json
Normal file
20
package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "rdt-client",
|
||||||
|
"version": "2.0.5",
|
||||||
|
"description": "This is a web interface to manage your torrents on Real-Debrid.",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"gh-release": "^5.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/rogerfar/rdt-client.git"
|
||||||
|
},
|
||||||
|
"author": "Roger Far",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/rogerfar/rdt-client/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/rogerfar/rdt-client#readme"
|
||||||
|
}
|
||||||
|
|
@ -10,4 +10,4 @@
|
||||||
# permissions
|
# permissions
|
||||||
echo "Setting permissions"
|
echo "Setting permissions"
|
||||||
chown -R abc:abc \
|
chown -R abc:abc \
|
||||||
/app /data
|
/app /data /config
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
oneshot
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/etc/s6-overlay/s6-rc.d/init-rdt-client/run
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
longrun
|
|
||||||
|
|
@ -5,11 +5,6 @@ if [ -n "${UMASK_SET}" ] && [ -z "${UMASK}" ]; then
|
||||||
umask ${UMASK_SET}
|
umask ${UMASK_SET}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# permissions
|
|
||||||
echo "Setting permissions"
|
|
||||||
chown -R abc:abc \
|
|
||||||
/app /data
|
|
||||||
|
|
||||||
echo "Changing to /app folder"
|
echo "Changing to /app folder"
|
||||||
cd /app || exit
|
cd /app || exit
|
||||||
|
|
||||||
|
|
@ -1,36 +1,196 @@
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using System.Collections.Generic;
|
||||||
|
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(DbContextOptions options) : IdentityDbContext(options)
|
|
||||||
{
|
{
|
||||||
public DbSet<Download> Downloads { get; set; }
|
public class DataContext : IdentityDbContext
|
||||||
public DbSet<Setting> Settings { get; set; }
|
|
||||||
public DbSet<Torrent> Torrents { get; set; }
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
|
||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
public DataContext(DbContextOptions options) : base(options)
|
||||||
|
|
||||||
builder.Entity<Download>()
|
|
||||||
.HasIndex(m => new
|
|
||||||
{
|
|
||||||
m.TorrentId,
|
|
||||||
m.Path
|
|
||||||
})
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
var cascadeFKs = builder.Model.GetEntityTypes()
|
|
||||||
.SelectMany(t => t.GetForeignKeys())
|
|
||||||
.Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
foreach (var fk in cascadeFKs)
|
|
||||||
{
|
{
|
||||||
fk.DeleteBehavior = DeleteBehavior.Restrict;
|
}
|
||||||
|
|
||||||
|
public DbSet<Download> Downloads { get; set; }
|
||||||
|
public DbSet<Setting> Settings { get; set; }
|
||||||
|
public DbSet<Torrent> Torrents { get; set; }
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
|
var cascadeFKs = builder.Model.GetEntityTypes()
|
||||||
|
.SelectMany(t => t.GetForeignKeys())
|
||||||
|
.Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
foreach (var fk in cascadeFKs)
|
||||||
|
{
|
||||||
|
fk.DeleteBehavior = DeleteBehavior.Restrict;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Seed()
|
||||||
|
{
|
||||||
|
var seedSettings = new List<Setting>
|
||||||
|
{
|
||||||
|
new Setting
|
||||||
|
{
|
||||||
|
SettingId = "Provider",
|
||||||
|
Type = "String",
|
||||||
|
Value = "RealDebrid"
|
||||||
|
},
|
||||||
|
new Setting
|
||||||
|
{
|
||||||
|
SettingId = "ProviderAutoImport",
|
||||||
|
Type = "Int32",
|
||||||
|
Value = "0"
|
||||||
|
},
|
||||||
|
new Setting
|
||||||
|
{
|
||||||
|
SettingId = "ProviderAutoDelete",
|
||||||
|
Type = "Int32",
|
||||||
|
Value = "0"
|
||||||
|
},
|
||||||
|
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"
|
||||||
|
},
|
||||||
|
new Setting
|
||||||
|
{
|
||||||
|
SettingId = "Categories",
|
||||||
|
Type = "String",
|
||||||
|
Value = ""
|
||||||
|
},
|
||||||
|
new Setting
|
||||||
|
{
|
||||||
|
SettingId = "Aria2cUrl",
|
||||||
|
Type = "String",
|
||||||
|
Value = "http://127.0.0.1:6800/jsonrpc"
|
||||||
|
},
|
||||||
|
new Setting
|
||||||
|
{
|
||||||
|
SettingId = "Aria2cSecret",
|
||||||
|
Type = "String",
|
||||||
|
Value = ""
|
||||||
|
},
|
||||||
|
new Setting
|
||||||
|
{
|
||||||
|
SettingId = "DownloadRetryAttempts",
|
||||||
|
Type = "Int32",
|
||||||
|
Value = "3"
|
||||||
|
},
|
||||||
|
new Setting
|
||||||
|
{
|
||||||
|
SettingId = "TorrentRetryAttempts",
|
||||||
|
Type = "Int32",
|
||||||
|
Value = "1"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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,9 +0,0 @@
|
||||||
namespace RdtClient.Data.Data;
|
|
||||||
|
|
||||||
public enum DownloadAddResult
|
|
||||||
{
|
|
||||||
Added,
|
|
||||||
AlreadyExists,
|
|
||||||
TorrentMissing,
|
|
||||||
InvalidInput
|
|
||||||
}
|
|
||||||
|
|
@ -1,315 +1,267 @@
|
||||||
using Microsoft.Data.Sqlite;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using RdtClient.Data.Models.Data;
|
|
||||||
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(DataContext dataContext, ILogger<DownloadData>? logger = null)
|
|
||||||
{
|
{
|
||||||
public async Task<List<Download>> GetForTorrent(Guid torrentId)
|
public class DownloadData
|
||||||
{
|
{
|
||||||
return await dataContext.Downloads
|
private readonly DataContext _dataContext;
|
||||||
.AsNoTracking()
|
|
||||||
.Where(m => m.TorrentId == torrentId)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Download?> GetById(Guid downloadId)
|
public DownloadData(DataContext dataContext)
|
||||||
{
|
|
||||||
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<DownloadAddResult> TryAddForTorrent(Guid torrentId, DownloadInfo downloadInfo)
|
|
||||||
{
|
|
||||||
if (String.IsNullOrWhiteSpace(downloadInfo.RestrictedLink))
|
|
||||||
{
|
{
|
||||||
logger?.LogDebug("Skipped download creation because the restricted link was blank. TorrentId: {torrentId}", torrentId);
|
_dataContext = dataContext;
|
||||||
|
|
||||||
return DownloadAddResult.InvalidInput;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await dataContext.Torrents.AsNoTracking().AnyAsync(m => m.TorrentId == torrentId))
|
public async Task<List<Download>> GetForTorrent(Guid torrentId)
|
||||||
{
|
{
|
||||||
logger?.LogDebug("Skipped download creation because the torrent no longer exists. TorrentId: {torrentId}, Path: {path}", torrentId, downloadInfo.RestrictedLink);
|
return await _dataContext.Downloads
|
||||||
|
.AsNoTracking()
|
||||||
return DownloadAddResult.TorrentMissing;
|
.Where(m => m.TorrentId == torrentId)
|
||||||
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await dataContext.Downloads.AsNoTracking().AnyAsync(m => m.TorrentId == torrentId && m.Path == downloadInfo.RestrictedLink))
|
public async Task<Download> GetById(Guid downloadId)
|
||||||
{
|
{
|
||||||
logger?.LogDebug("Skipped download creation because it already exists. TorrentId: {torrentId}, Path: {path}", torrentId, downloadInfo.RestrictedLink);
|
return await _dataContext.Downloads
|
||||||
|
.Include(m => m.Torrent)
|
||||||
return DownloadAddResult.AlreadyExists;
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var download = new Download
|
public async Task<Download> Get(Guid torrentId, String path)
|
||||||
{
|
{
|
||||||
DownloadId = Guid.NewGuid(),
|
return await _dataContext.Downloads
|
||||||
TorrentId = torrentId,
|
.Include(m => m.Torrent)
|
||||||
FileName = downloadInfo.FileName,
|
.AsNoTracking()
|
||||||
Path = downloadInfo.RestrictedLink,
|
.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
|
||||||
Added = DateTimeOffset.UtcNow,
|
|
||||||
DownloadQueued = DateTimeOffset.UtcNow,
|
|
||||||
RetryCount = 0
|
|
||||||
};
|
|
||||||
|
|
||||||
await dataContext.Downloads.AddAsync(download);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
|
|
||||||
return DownloadAddResult.Added;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// These shouldn't be possible any longer, but added for safety and until confirmed.
|
public async Task<Download> Add(Guid torrentId, String path)
|
||||||
catch (DbUpdateException ex)
|
|
||||||
{
|
{
|
||||||
dataContext.Entry(download).State = EntityState.Detached;
|
var download = new Download
|
||||||
|
|
||||||
if (IsDuplicateDownloadViolation(ex))
|
|
||||||
{
|
{
|
||||||
logger?.LogDebug("Skipped download creation after a concurrent duplicate insert. TorrentId: {torrentId}, Path: {path}", torrentId, downloadInfo.RestrictedLink);
|
DownloadId = Guid.NewGuid(),
|
||||||
|
TorrentId = torrentId,
|
||||||
|
Path = path,
|
||||||
|
Added = DateTimeOffset.UtcNow,
|
||||||
|
DownloadQueued = DateTimeOffset.UtcNow,
|
||||||
|
RetryCount = 0
|
||||||
|
};
|
||||||
|
|
||||||
return DownloadAddResult.AlreadyExists;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsForeignKeyViolation(ex) && !await dataContext.Torrents.AsNoTracking().AnyAsync(m => m.TorrentId == torrentId))
|
dbDownload.Link = unrestrictedLink;
|
||||||
{
|
|
||||||
logger?.LogDebug("Skipped download creation after the torrent was deleted concurrently. TorrentId: {torrentId}, Path: {path}",
|
|
||||||
torrentId,
|
|
||||||
downloadInfo.RestrictedLink);
|
|
||||||
|
|
||||||
return DownloadAddResult.TorrentMissing;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw;
|
dbDownload.DownloadStarted = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
|
public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime)
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbDownload.DownloadFinished = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDownload.Link = unrestrictedLink;
|
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime)
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateFileName(Guid downloadId, String fileName)
|
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbDownload.UnpackingQueued = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDownload.FileName = fileName;
|
public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime)
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime)
|
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbDownload.UnpackingStarted = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDownload.DownloadStarted = dateTime;
|
public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? 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;
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbDownload.UnpackingFinished = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDownload.DownloadFinished = dateTime;
|
public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime)
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime)
|
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbDownload.Completed = dateTime;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDownload.UnpackingQueued = dateTime;
|
public async Task UpdateError(Guid downloadId, String error)
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime)
|
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbDownload.Error = error;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDownload.UnpackingStarted = dateTime;
|
public async Task UpdateRetryCount(Guid downloadId, Int32 retryCount)
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime)
|
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbDownload.RetryCount = retryCount;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDownload.UnpackingFinished = dateTime;
|
public async Task UpdateRemoteId(Guid downloadId, String remoteId)
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime)
|
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
if (dbDownload == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbDownload.RemoteId = remoteId;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDownload.Completed = dateTime;
|
public async Task DeleteForTorrent(Guid torrentId)
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateError(Guid downloadId, String? error)
|
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var downloads = await _dataContext.Downloads
|
||||||
|
.Where(m => m.TorrentId == torrentId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
_dataContext.Downloads.RemoveRange(downloads);
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDownload.Error = error;
|
public async Task Reset(Guid downloadId)
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateRetryCount(Guid downloadId, Int32 retryCount)
|
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbDownload = await _dataContext.Downloads
|
||||||
|
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
||||||
|
|
||||||
|
dbDownload.RetryCount = 0;
|
||||||
|
dbDownload.Link = null;
|
||||||
|
dbDownload.Added = DateTimeOffset.UtcNow;
|
||||||
|
dbDownload.DownloadQueued = DateTimeOffset.UtcNow;
|
||||||
|
dbDownload.DownloadStarted = null;
|
||||||
|
dbDownload.DownloadFinished = null;
|
||||||
|
dbDownload.UnpackingQueued = null;
|
||||||
|
dbDownload.UnpackingStarted = null;
|
||||||
|
dbDownload.UnpackingFinished = null;
|
||||||
|
dbDownload.Completed = null;
|
||||||
|
dbDownload.Error = null;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await TorrentData.VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDownload.RetryCount = retryCount;
|
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public async Task UpdateRemoteId(Guid downloadId, String remoteId)
|
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
|
|
||||||
|
|
||||||
if (dbDownload == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Reset(Guid downloadId, DateTimeOffset? downloadQueued = null)
|
|
||||||
{
|
|
||||||
var dbDownload = await dataContext.Downloads
|
|
||||||
.FirstOrDefaultAsync(m => m.DownloadId == downloadId)
|
|
||||||
?? throw new($"Cannot find download with ID {downloadId}");
|
|
||||||
|
|
||||||
dbDownload.RetryCount = 0;
|
|
||||||
dbDownload.Link = null;
|
|
||||||
dbDownload.Added = DateTimeOffset.UtcNow;
|
|
||||||
dbDownload.DownloadQueued = 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean IsDuplicateDownloadViolation(DbUpdateException exception)
|
|
||||||
{
|
|
||||||
var sqliteException = exception.InnerException as SqliteException;
|
|
||||||
|
|
||||||
return sqliteException?.SqliteExtendedErrorCode == 2067
|
|
||||||
|| sqliteException?.Message.Contains("UNIQUE constraint failed: Downloads.TorrentId, Downloads.Path", StringComparison.Ordinal) == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean IsForeignKeyViolation(DbUpdateException exception)
|
|
||||||
{
|
|
||||||
var sqliteException = exception.InnerException as SqliteException;
|
|
||||||
|
|
||||||
return sqliteException?.SqliteExtendedErrorCode == 787
|
|
||||||
|| sqliteException?.Message.Contains("FOREIGN KEY constraint failed", StringComparison.Ordinal) == true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
using RdtClient.Data.Enums;
|
|
||||||
using RdtClient.Data.Models.Data;
|
|
||||||
|
|
||||||
namespace RdtClient.Data.Data;
|
|
||||||
|
|
||||||
public interface ITorrentData
|
|
||||||
{
|
|
||||||
Task<IList<Torrent>> Get();
|
|
||||||
Task<Torrent?> GetById(Guid torrentId);
|
|
||||||
Task<Torrent?> GetByHash(String hash);
|
|
||||||
|
|
||||||
Task<Torrent> Add(String? rdId,
|
|
||||||
String hash,
|
|
||||||
String? fileOrMagnetContents,
|
|
||||||
Boolean isFile,
|
|
||||||
DownloadType downloadType,
|
|
||||||
DownloadClient downloadClient,
|
|
||||||
Torrent torrent);
|
|
||||||
|
|
||||||
Task UpdateRdData(Torrent torrent);
|
|
||||||
Task UpdateRdId(Torrent torrent, String rdId);
|
|
||||||
Task UpdateHash(Torrent torrent, String hash);
|
|
||||||
Task Update(Torrent torrent);
|
|
||||||
Task UpdateCategory(Guid torrentId, String? category);
|
|
||||||
Task UpdateComplete(Guid torrentId, String? error, DateTimeOffset? datetime, Boolean retry);
|
|
||||||
Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime);
|
|
||||||
Task UpdatePriority(Guid torrentId, Int32? priority);
|
|
||||||
Task UpdateRetry(Guid torrentId, DateTimeOffset? dateTime, Int32 retryCount);
|
|
||||||
Task UpdateError(Guid torrentId, String error);
|
|
||||||
Task Delete(Guid torrentId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,221 +1,132 @@
|
||||||
using System.ComponentModel;
|
using System;
|
||||||
using System.Reflection;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using RdtClient.Data.Models.Data;
|
using RdtClient.Data.Models.Data;
|
||||||
using RdtClient.Data.Models.Internal;
|
using RdtClient.Data.Models.Internal;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace RdtClient.Data.Data;
|
namespace RdtClient.Data.Data
|
||||||
|
|
||||||
public class SettingData(DataContext dataContext, ILogger<SettingData> logger)
|
|
||||||
{
|
{
|
||||||
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local
|
public class SettingData
|
||||||
public static DbSettings Get { get; private set; } = new();
|
|
||||||
|
|
||||||
public static IList<SettingProperty> GetAll()
|
|
||||||
{
|
{
|
||||||
return GetSettings(Get, null);
|
private static readonly SemaphoreSlim _settingCacheLock = new(1);
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Update(IList<SettingProperty> settings)
|
private readonly DataContext _dataContext;
|
||||||
{
|
|
||||||
var dbSettings = await dataContext.Settings.ToListAsync();
|
|
||||||
|
|
||||||
foreach (var dbSetting in dbSettings)
|
public SettingData(DataContext dataContext)
|
||||||
{
|
{
|
||||||
var setting = settings.FirstOrDefault(m => m.Key == dbSetting.SettingId);
|
_dataContext = dataContext;
|
||||||
|
}
|
||||||
|
|
||||||
if (setting != null)
|
public static DbSettings Get { get; private set; }
|
||||||
|
|
||||||
|
public async Task ResetCache()
|
||||||
|
{
|
||||||
|
var allSettings = await _dataContext.Settings.AsNoTracking().ToListAsync();
|
||||||
|
|
||||||
|
String GetString(String name)
|
||||||
{
|
{
|
||||||
dbSetting.Value = setting.Value?.ToString();
|
return allSettings.FirstOrDefault(m => m.SettingId == name)?.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("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.Count > 0)
|
|
||||||
{
|
|
||||||
await dataContext.Settings.AddRangeAsync(newSettings);
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldSettings = dbSettings.Where(m => expectedSettings.All(p => p.SettingId != m.SettingId)).ToList();
|
|
||||||
|
|
||||||
if (oldSettings.Count > 0)
|
|
||||||
{
|
|
||||||
dataContext.Settings.RemoveRange(oldSettings);
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<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
|
Int32 GetInt32(String name)
|
||||||
{
|
{
|
||||||
Key = propertyName,
|
var strVal = GetString(name);
|
||||||
DisplayName = displayName?.DisplayName,
|
|
||||||
Description = description?.Description,
|
if (!Int32.TryParse(strVal, out var intVal))
|
||||||
Type = property.PropertyType.Name
|
{
|
||||||
|
Log.Error("Unable to parse setting {name} to Int32", name);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return intVal;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Get = new DbSettings
|
||||||
|
{
|
||||||
|
Provider = GetString("Provider"),
|
||||||
|
ProviderAutoImport = GetInt32("ProviderAutoImport"),
|
||||||
|
ProviderAutoDelete= GetInt32("ProviderAutoDelete"),
|
||||||
|
RealDebridApiKey = GetString("RealDebridApiKey"),
|
||||||
|
DownloadPath = GetString("DownloadPath"),
|
||||||
|
DownloadClient = GetString("DownloadClient"),
|
||||||
|
TempPath = GetString("TempPath"),
|
||||||
|
MappedPath = GetString("MappedPath"),
|
||||||
|
DownloadLimit = GetInt32("DownloadLimit"),
|
||||||
|
UnpackLimit = GetInt32("UnpackLimit"),
|
||||||
|
MinFileSize = GetInt32("MinFileSize"),
|
||||||
|
OnlyDownloadAvailableFiles = GetInt32("OnlyDownloadAvailableFiles"),
|
||||||
|
DownloadChunkCount = GetInt32("DownloadChunkCount"),
|
||||||
|
DownloadMaxSpeed = GetInt32("DownloadMaxSpeed"),
|
||||||
|
ProxyServer = GetString("ProxyServer"),
|
||||||
|
LogLevel = GetString("LogLevel"),
|
||||||
|
Categories = GetString("Categories"),
|
||||||
|
Aria2cUrl = GetString("Aria2cUrl"),
|
||||||
|
Aria2cSecret = GetString("Aria2cSecret"),
|
||||||
|
DownloadRetryAttempts = GetInt32("DownloadRetryAttempts"),
|
||||||
|
TorrentRetryAttempts = GetInt32("TorrentRetryAttempts")
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (property.PropertyType.IsEnum ||
|
public async Task<IList<Setting>> GetAll()
|
||||||
property.PropertyType.IsValueType ||
|
{
|
||||||
property.PropertyType == typeof(String))
|
return await _dataContext.Settings.AsNoTracking().ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Update(IList<Setting> settings)
|
||||||
|
{
|
||||||
|
await _settingCacheLock.WaitAsync();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
settingProperty.Value = property.GetValue(defaultSetting);
|
var dbSettings = await _dataContext.Settings.ToListAsync();
|
||||||
|
|
||||||
if (property.PropertyType.IsEnum)
|
foreach (var dbSetting in dbSettings)
|
||||||
{
|
{
|
||||||
settingProperty.Type = "Enum";
|
var setting = settings.FirstOrDefault(m => m.SettingId == dbSetting.SettingId);
|
||||||
settingProperty.EnumValues = [];
|
|
||||||
|
|
||||||
foreach (var e in Enum.GetValues(property.PropertyType).Cast<Enum>())
|
if (setting != null)
|
||||||
{
|
{
|
||||||
var enumMember = property.PropertyType.GetMember(e.ToString()).First();
|
dbSetting.Value = setting.Value;
|
||||||
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);
|
await _dataContext.SaveChangesAsync();
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
settingProperty.Type = "Object";
|
|
||||||
result.Add(settingProperty);
|
|
||||||
|
|
||||||
var childResults = GetSettings(property.GetValue(defaultSetting)!, propertyName);
|
await ResetCache();
|
||||||
result.AddRange(childResults);
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_settingCacheLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
public async Task UpdateString(String key, String value)
|
||||||
}
|
|
||||||
|
|
||||||
private 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;
|
await _settingCacheLock.WaitAsync();
|
||||||
|
|
||||||
if (parent != null)
|
try
|
||||||
{
|
{
|
||||||
propertyName = $"{parent}:{propertyName}";
|
var dbSetting = await _dataContext.Settings.FirstOrDefaultAsync(m => m.SettingId == key);
|
||||||
}
|
|
||||||
|
|
||||||
if (property.PropertyType.IsEnum ||
|
if (dbSetting == null)
|
||||||
property.PropertyType.IsValueType ||
|
|
||||||
property.PropertyType == typeof(String))
|
|
||||||
{
|
|
||||||
var setting = settings.FirstOrDefault(m => m.SettingId == propertyName);
|
|
||||||
|
|
||||||
if (setting != null)
|
|
||||||
{
|
{
|
||||||
if (property.PropertyType.IsEnum)
|
throw new Exception($"Cannot find setting with key {key}");
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var newValue = Enum.Parse(property.PropertyType, setting.Value ?? "0");
|
|
||||||
property.SetValue(defaultSetting, newValue);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
logger.LogWarning("Invalid value for setting {propertyName}: {setting.Value}", propertyName, setting.Value);
|
|
||||||
|
|
||||||
var defaultValue = property.GetValue(defaultSetting);
|
|
||||||
property.SetValue(defaultSetting, defaultValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
{
|
|
||||||
logger.LogWarning("Invalid value for setting {propertyName}: {setting.Value}", propertyName, setting.Value);
|
|
||||||
|
|
||||||
var defaultValue = property.GetValue(defaultSetting);
|
|
||||||
property.SetValue(defaultSetting, defaultValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbSetting.Value = value;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await ResetCache();
|
||||||
}
|
}
|
||||||
else
|
finally
|
||||||
{
|
{
|
||||||
SetSettings(settings, property.GetValue(defaultSetting)!, propertyName);
|
_settingCacheLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,310 +1,312 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using RdtClient.Data.Enums;
|
|
||||||
using RdtClient.Data.Models.Data;
|
using RdtClient.Data.Models.Data;
|
||||||
|
|
||||||
namespace RdtClient.Data.Data;
|
namespace RdtClient.Data.Data
|
||||||
|
|
||||||
public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger = null) : ITorrentData
|
|
||||||
{
|
{
|
||||||
public async Task<IList<Torrent>> Get()
|
public class TorrentData
|
||||||
{
|
{
|
||||||
var torrents = await dataContext.Torrents
|
private static IList<Torrent> _torrentCache;
|
||||||
.AsNoTracking()
|
private static readonly SemaphoreSlim TorrentCacheLock = new(1, 1);
|
||||||
.AsSplitQuery()
|
|
||||||
.Include(m => m.Downloads)
|
|
||||||
.OrderBy(m => m.Priority ?? 9999)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return torrents.OrderBy(m => m.Priority ?? 9999)
|
private readonly DataContext _dataContext;
|
||||||
.ThenBy(m => m.Added)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Torrent?> GetById(Guid torrentId)
|
public TorrentData(DataContext dataContext)
|
||||||
{
|
|
||||||
var dbTorrent = await dataContext.Torrents
|
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.Downloads)
|
|
||||||
.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
{
|
||||||
return null;
|
_dataContext = dataContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var file in dbTorrent.Downloads)
|
public async Task<IList<Torrent>> Get()
|
||||||
{
|
{
|
||||||
file.Torrent = null;
|
await TorrentCacheLock.WaitAsync();
|
||||||
}
|
|
||||||
|
|
||||||
return dbTorrent;
|
try
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Torrent?> GetByHash(String hash)
|
|
||||||
{
|
|
||||||
hash = hash.ToLower();
|
|
||||||
|
|
||||||
var dbTorrent = await dataContext.Torrents
|
|
||||||
.AsNoTracking()
|
|
||||||
.Include(m => m.Downloads)
|
|
||||||
.FirstOrDefaultAsync(m => m.Hash == hash);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var file in dbTorrent.Downloads)
|
|
||||||
{
|
|
||||||
file.Torrent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbTorrent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Torrent> Add(String? rdId,
|
|
||||||
String hash,
|
|
||||||
String? fileOrMagnetContents,
|
|
||||||
Boolean isFile,
|
|
||||||
DownloadType downloadType,
|
|
||||||
DownloadClient downloadClient,
|
|
||||||
Torrent torrent)
|
|
||||||
{
|
|
||||||
var newTorrent = new Torrent
|
|
||||||
{
|
|
||||||
TorrentId = Guid.NewGuid(),
|
|
||||||
Added = DateTimeOffset.UtcNow,
|
|
||||||
RdId = rdId,
|
|
||||||
Hash = hash.ToLower(),
|
|
||||||
Category = torrent.Category,
|
|
||||||
HostDownloadAction = torrent.HostDownloadAction,
|
|
||||||
FinishedActionDelay = torrent.FinishedActionDelay,
|
|
||||||
DownloadAction = torrent.DownloadAction,
|
|
||||||
FinishedAction = torrent.FinishedAction,
|
|
||||||
DownloadMinSize = torrent.DownloadMinSize,
|
|
||||||
IncludeRegex = torrent.IncludeRegex,
|
|
||||||
ExcludeRegex = torrent.ExcludeRegex,
|
|
||||||
DownloadManualFiles = torrent.DownloadManualFiles,
|
|
||||||
DownloadClient = downloadClient,
|
|
||||||
Type = downloadType,
|
|
||||||
FileOrMagnet = fileOrMagnetContents,
|
|
||||||
IsFile = isFile,
|
|
||||||
Priority = torrent.Priority,
|
|
||||||
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
|
||||||
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
|
|
||||||
DeleteOnError = torrent.DeleteOnError,
|
|
||||||
Lifetime = torrent.Lifetime,
|
|
||||||
RdStatus = torrent.RdStatus,
|
|
||||||
RdName = torrent.RdName
|
|
||||||
};
|
|
||||||
|
|
||||||
await dataContext.Torrents.AddAsync(newTorrent);
|
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
|
|
||||||
return newTorrent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateRdData(Torrent torrent)
|
|
||||||
{
|
|
||||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbTorrent.RdName = torrent.RdName;
|
|
||||||
dbTorrent.RdSize = torrent.RdSize;
|
|
||||||
dbTorrent.RdHost = torrent.RdHost;
|
|
||||||
dbTorrent.RdSplit = torrent.RdSplit;
|
|
||||||
dbTorrent.RdProgress = torrent.RdProgress;
|
|
||||||
dbTorrent.RdStatus = torrent.RdStatus;
|
|
||||||
dbTorrent.RdStatusRaw = torrent.RdStatusRaw;
|
|
||||||
dbTorrent.RdAdded = torrent.RdAdded;
|
|
||||||
dbTorrent.RdEnded = torrent.RdEnded;
|
|
||||||
dbTorrent.RdSpeed = torrent.RdSpeed;
|
|
||||||
dbTorrent.RdSeeders = torrent.RdSeeders;
|
|
||||||
dbTorrent.RdFiles = torrent.RdFiles;
|
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateRdId(Torrent torrent, String rdId)
|
|
||||||
{
|
|
||||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbTorrent.RdId = rdId;
|
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateHash(Torrent torrent, String hash)
|
|
||||||
{
|
|
||||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbTorrent.Hash = hash.ToLower();
|
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Update(Torrent torrent)
|
|
||||||
{
|
|
||||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbTorrent.DownloadClient = torrent.DownloadClient;
|
|
||||||
dbTorrent.HostDownloadAction = torrent.HostDownloadAction;
|
|
||||||
dbTorrent.Category = torrent.Category;
|
|
||||||
dbTorrent.Priority = torrent.Priority;
|
|
||||||
dbTorrent.DownloadRetryAttempts = torrent.DownloadRetryAttempts;
|
|
||||||
dbTorrent.TorrentRetryAttempts = torrent.TorrentRetryAttempts;
|
|
||||||
dbTorrent.DeleteOnError = torrent.DeleteOnError;
|
|
||||||
dbTorrent.Lifetime = torrent.Lifetime;
|
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateCategory(Guid torrentId, String? category)
|
|
||||||
{
|
|
||||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbTorrent.Category = category;
|
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Count > 0)
|
|
||||||
{
|
{
|
||||||
error = $"{downloadWithErrors.Count}/{downloads.Count} downloads failed with errors";
|
_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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!String.IsNullOrWhiteSpace(error) && retry)
|
public async Task<Torrent> GetById(Guid torrentId)
|
||||||
{
|
{
|
||||||
if (dbTorrent.RetryCount < dbTorrent.TorrentRetryAttempts)
|
var dbTorrent = await _dataContext.Torrents
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(m => m.Downloads)
|
||||||
|
.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
{
|
{
|
||||||
dbTorrent.RetryCount += 1;
|
return null;
|
||||||
dbTorrent.Retry = DateTime.UtcNow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var file in dbTorrent.Downloads)
|
||||||
|
{
|
||||||
|
file.Torrent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbTorrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
dbTorrent.Completed = datetime;
|
public async Task<Torrent> GetByHash(String hash)
|
||||||
dbTorrent.Error = error;
|
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime)
|
|
||||||
{
|
|
||||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbTorrent = await _dataContext.Torrents
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(m => m.Downloads)
|
||||||
|
.FirstOrDefaultAsync(m => m.Hash.ToLower() == hash.ToLower());
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in dbTorrent.Downloads)
|
||||||
|
{
|
||||||
|
file.Torrent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbTorrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
dbTorrent.FilesSelected = datetime;
|
public async Task<Torrent> Add(String realDebridId,
|
||||||
|
String hash,
|
||||||
await dataContext.SaveChangesAsync();
|
String fileOrMagnetContents,
|
||||||
}
|
Boolean isFile,
|
||||||
|
Torrent torrent)
|
||||||
public async Task UpdatePriority(Guid torrentId, Int32? priority)
|
|
||||||
{
|
|
||||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var newTorrent = new Torrent
|
||||||
|
{
|
||||||
|
TorrentId = Guid.NewGuid(),
|
||||||
|
Added = DateTimeOffset.UtcNow,
|
||||||
|
RdId = realDebridId,
|
||||||
|
Hash = hash.ToLower(),
|
||||||
|
Category = torrent.Category,
|
||||||
|
DownloadAction = torrent.DownloadAction,
|
||||||
|
FinishedAction = torrent.FinishedAction,
|
||||||
|
DownloadMinSize = torrent.DownloadMinSize,
|
||||||
|
DownloadManualFiles = torrent.DownloadManualFiles,
|
||||||
|
FileOrMagnet = fileOrMagnetContents,
|
||||||
|
IsFile = isFile,
|
||||||
|
Priority = torrent.Priority,
|
||||||
|
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
|
||||||
|
DownloadRetryAttempts = torrent.DownloadRetryAttempts
|
||||||
|
};
|
||||||
|
|
||||||
|
await _dataContext.Torrents.AddAsync(newTorrent);
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
|
|
||||||
|
return newTorrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
dbTorrent.Priority = priority;
|
public async Task UpdateRdData(Torrent torrent)
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateRetry(Guid torrentId, DateTimeOffset? dateTime, Int32 retryCount)
|
|
||||||
{
|
|
||||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbTorrent.RdName = torrent.RdName;
|
||||||
|
dbTorrent.RdSize = torrent.RdSize;
|
||||||
|
dbTorrent.RdHost = torrent.RdHost;
|
||||||
|
dbTorrent.RdSplit = torrent.RdSplit;
|
||||||
|
dbTorrent.RdProgress = torrent.RdProgress;
|
||||||
|
dbTorrent.RdStatus = torrent.RdStatus;
|
||||||
|
dbTorrent.RdStatusRaw = torrent.RdStatusRaw;
|
||||||
|
dbTorrent.RdAdded = torrent.RdAdded;
|
||||||
|
dbTorrent.RdEnded = torrent.RdEnded;
|
||||||
|
dbTorrent.RdSpeed = torrent.RdSpeed;
|
||||||
|
dbTorrent.RdSeeders = torrent.RdSeeders;
|
||||||
|
|
||||||
|
if (torrent.Files != null)
|
||||||
|
{
|
||||||
|
dbTorrent.RdFiles = torrent.RdFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbTorrent.RetryCount = retryCount;
|
public async Task Update(Torrent torrent)
|
||||||
dbTorrent.Retry = dateTime;
|
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateError(Guid torrentId, String error)
|
|
||||||
{
|
|
||||||
var dbTorrent = await dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
|
||||||
|
|
||||||
if (dbTorrent == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId);
|
||||||
|
|
||||||
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbTorrent.Priority = torrent.Priority;
|
||||||
|
dbTorrent.DownloadRetryAttempts = torrent.DownloadRetryAttempts;
|
||||||
|
dbTorrent.TorrentRetryAttempts = torrent.TorrentRetryAttempts;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await VoidCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbTorrent.Error = error;
|
public async Task UpdateCategory(Guid torrentId, String category)
|
||||||
|
|
||||||
await dataContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Delete(Guid torrentId)
|
|
||||||
{
|
|
||||||
await using var transaction = await dataContext.Database.BeginTransactionAsync();
|
|
||||||
|
|
||||||
await dataContext.Downloads
|
|
||||||
.Where(m => m.TorrentId == torrentId)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
|
|
||||||
var deletedTorrents = await dataContext.Torrents
|
|
||||||
.Where(m => m.TorrentId == torrentId)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
|
|
||||||
await transaction.CommitAsync();
|
|
||||||
|
|
||||||
if (deletedTorrents == 0)
|
|
||||||
{
|
{
|
||||||
logger?.LogDebug("Skipped torrent graph deletion because the torrent was not found. TorrentId: {torrentId}", torrentId);
|
var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId);
|
||||||
|
|
||||||
return;
|
if (dbTorrent == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
error = $"{downloadWithErrors.Count}/{downloads.Count} downloads failed with errors";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!String.IsNullOrWhiteSpace(error) && retry)
|
||||||
|
{
|
||||||
|
if (dbTorrent.RetryCount < dbTorrent.TorrentRetryAttempts)
|
||||||
|
{
|
||||||
|
dbTorrent.RetryCount += 1;
|
||||||
|
dbTorrent.Retry = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,12 +1,21 @@
|
||||||
using Microsoft.AspNetCore.Identity;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace RdtClient.Data.Data;
|
namespace RdtClient.Data.Data
|
||||||
|
|
||||||
public class UserData(DataContext dataContext)
|
|
||||||
{
|
{
|
||||||
public async Task<IdentityUser?> GetUser()
|
public class UserData
|
||||||
{
|
{
|
||||||
return await dataContext.Users.OrderBy(m => m.Id).FirstOrDefaultAsync();
|
private readonly DataContext _dataContext;
|
||||||
|
|
||||||
|
public UserData(DataContext dataContext)
|
||||||
|
{
|
||||||
|
_dataContext = dataContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IdentityUser> GetUser()
|
||||||
|
{
|
||||||
|
return await _dataContext.Users.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,16 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
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 void Config(IServiceCollection services, AppSettings appSettings)
|
public static class DiConfig
|
||||||
{
|
{
|
||||||
if (String.IsNullOrWhiteSpace(appSettings.Database?.Path))
|
public static void Config(IServiceCollection services)
|
||||||
{
|
{
|
||||||
throw new("Invalid database path found in appSettings");
|
services.AddScoped<DownloadData>();
|
||||||
|
services.AddScoped<SettingData>();
|
||||||
|
services.AddScoped<TorrentData>();
|
||||||
|
services.AddScoped<UserData>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionString = $"Data Source={appSettings.Database.Path};Cache=Shared;Pooling=True;Command Timeout=30";
|
|
||||||
services.AddDbContext<DataContext>(options => options.UseSqlite(connectionString));
|
|
||||||
|
|
||||||
services.AddScoped<DownloadData>();
|
|
||||||
services.AddScoped<SettingData>();
|
|
||||||
services.AddScoped<ITorrentData, TorrentData>();
|
|
||||||
services.AddScoped<UserData>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace RdtClient.Data.Enums;
|
|
||||||
|
|
||||||
public enum AuthenticationType
|
|
||||||
{
|
|
||||||
[Description("Username + Password")]
|
|
||||||
UserNamePassword,
|
|
||||||
|
|
||||||
[Description("No Authentication")]
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace RdtClient.Data.Enums;
|
|
||||||
|
|
||||||
public enum DownloadClient
|
|
||||||
{
|
|
||||||
[Description("Bezzad Downloader")]
|
|
||||||
Bezzad,
|
|
||||||
|
|
||||||
[Description("Aria2c")]
|
|
||||||
Aria2c,
|
|
||||||
|
|
||||||
[Description("Symlink Downloader")]
|
|
||||||
Symlink,
|
|
||||||
|
|
||||||
[Description("Synology DownloadStation")]
|
|
||||||
DownloadStation
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace RdtClient.Data.Enums;
|
|
||||||
|
|
||||||
public enum DownloadClientLogLevel
|
|
||||||
{
|
|
||||||
[Description("Verbose")]
|
|
||||||
Verbose,
|
|
||||||
|
|
||||||
[Description("Debug")]
|
|
||||||
Debug,
|
|
||||||
|
|
||||||
[Description("Information")]
|
|
||||||
Information,
|
|
||||||
|
|
||||||
[Description("Warning")]
|
|
||||||
Warning,
|
|
||||||
|
|
||||||
[Description("Error")]
|
|
||||||
Error,
|
|
||||||
|
|
||||||
[Description("None")]
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
namespace RdtClient.Data.Enums;
|
|
||||||
|
|
||||||
public enum DownloadType
|
|
||||||
{
|
|
||||||
Torrent = 0,
|
|
||||||
Nzb = 1
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace RdtClient.Data.Enums;
|
|
||||||
|
|
||||||
public enum LogLevel
|
|
||||||
{
|
|
||||||
[Description("Verbose")]
|
|
||||||
Verbose,
|
|
||||||
|
|
||||||
[Description("Debug")]
|
|
||||||
Debug,
|
|
||||||
|
|
||||||
[Description("Information")]
|
|
||||||
Information,
|
|
||||||
|
|
||||||
[Description("Warning")]
|
|
||||||
Warning,
|
|
||||||
|
|
||||||
[Description("Error")]
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue