Merge pull request #5 from rogerfar/master

Merging from parent
This commit is contained in:
Shawn 2023-09-03 23:15:12 -04:00 committed by GitHub
commit fe0c3a1643
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 7857 additions and 4884 deletions

View file

@ -13,4 +13,6 @@ assignees: ''
**Are you using Docker or as a service?** **Are you using Docker or as a service?**
**Which debrid provider 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**

View file

@ -9,10 +9,10 @@ permissions:
packages: write packages: write
env: env:
APP_NAME: rdt-client APP_NAME: rdtclient
DOCKER_FILE: ./Dockerfile DOCKER_FILE: ./Dockerfile
DOCKER_PLATFORMS: "linux/arm/v7,linux/arm64/v8,linux/amd64" DOCKER_PLATFORMS: "linux/arm64/v8,linux/amd64"
ENABLE_DOCKERHUB: 0 ENABLE_DOCKERHUB: 1
ENABLE_GHCR: 1 ENABLE_GHCR: 1
jobs: jobs:
@ -30,7 +30,7 @@ jobs:
uses: docker/metadata-action@v4.3.0 uses: docker/metadata-action@v4.3.0
with: with:
images: | images: |
# ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }} ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}
ghcr.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }} ghcr.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}
tags: | tags: |
type=schedule type=schedule

3
.gitignore vendored
View file

@ -6,4 +6,5 @@ node_modules/
RealDebridClient.zip RealDebridClient.zip
server/RdtClient.Web/appsettings.Development.json server/RdtClient.Web/appsettings.Development.json
data data
Dockerfile.dev Dockerfile.dev
test.bat

View file

@ -4,6 +4,71 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.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.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 ## [2.0.24] - 2023-03-16
### Changed ### Changed
- Fixed docker run issues. - Fixed docker run issues.

View file

@ -1,5 +1,5 @@
# Stage 1 - Build the frontend # Stage 1 - Build the frontend
FROM node:16-alpine3.17 AS node-build-env FROM node:18-alpine3.18 AS node-build-env
ARG TARGETPLATFORM ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
ARG BUILDPLATFORM ARG BUILDPLATFORM
@ -8,6 +8,8 @@ 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++
RUN \ RUN \
echo "**** Cloning Source Code ****" && \ echo "**** Cloning Source Code ****" && \
git clone https://github.com/rogerfar/rdt-client.git . && \ git clone https://github.com/rogerfar/rdt-client.git . && \
@ -45,7 +47,7 @@ RUN \
fi fi
# Stage 3 - Build runtime image # Stage 3 - Build runtime image
FROM ghcr.io/linuxserver/baseimage-alpine:3.17 FROM ghcr.io/linuxserver/baseimage-alpine:3.18
ARG TARGETPLATFORM ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
ARG BUILDPLATFORM ARG BUILDPLATFORM

View file

@ -127,6 +127,11 @@ navigate to http://<ipaddress>:6500, if all is good then we'll create a service
```sudo systemctl enable rdtc``` ```sudo systemctl enable rdtc```
```sudo systemctl start 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
### First Login ### First Login
@ -188,6 +193,10 @@ 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.
### 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.
## Build instructions ## Build instructions
### Prerequisites ### Prerequisites

11992
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,43 +12,43 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^15.2.2", "@angular/animations": "^16.2.3",
"@angular/cdk": "^15.2.2", "@angular/cdk": "^16.2.2",
"@angular/common": "^15.2.2", "@angular/common": "^16.2.3",
"@angular/compiler": "^15.2.2", "@angular/compiler": "^16.2.3",
"@angular/core": "^15.2.2", "@angular/core": "^16.2.3",
"@angular/flex-layout": "^15.0.0-beta.42", "@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "^15.2.2", "@angular/forms": "^16.2.3",
"@angular/platform-browser": "^15.2.2", "@angular/platform-browser": "^16.2.3",
"@angular/platform-browser-dynamic": "^15.2.2", "@angular/platform-browser-dynamic": "^16.2.3",
"@angular/router": "^15.2.2", "@angular/router": "^16.2.3",
"@fortawesome/fontawesome-free": "^6.3.0", "@fortawesome/fontawesome-free": "^6.4.2",
"@microsoft/signalr": "^7.0.3", "@microsoft/signalr": "^6.0.21",
"bulma": "^0.9.4", "bulma": "^0.9.4",
"curray": "^1.0.11", "curray": "^1.0.11",
"file-saver-es": "^2.0.5", "file-saver-es": "^2.0.5",
"ngx-filesize": "^3.0.1", "ngx-filesize": "^3.0.2",
"rxjs": "~7.8.0", "rxjs": "~7.8.1",
"tslib": "^2.5.0", "tslib": "^2.6.2",
"zone.js": "~0.13.0" "zone.js": "~0.13.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^15.2.2", "@angular-devkit/build-angular": "^16.2.1",
"@angular-eslint/builder": "15.2.1", "@angular-eslint/builder": "16.1.1",
"@angular-eslint/eslint-plugin": "15.2.1", "@angular-eslint/eslint-plugin": "16.1.1",
"@angular-eslint/eslint-plugin-template": "15.2.1", "@angular-eslint/eslint-plugin-template": "16.1.1",
"@angular-eslint/schematics": "15.2.1", "@angular-eslint/schematics": "16.1.1",
"@angular-eslint/template-parser": "15.2.1", "@angular-eslint/template-parser": "16.1.1",
"@angular/cli": "^15.2.2", "@angular/cli": "^16.2.1",
"@angular/compiler-cli": "^15.2.2", "@angular/compiler-cli": "^16.2.3",
"@angular/language-service": "^15.2.2", "@angular/language-service": "^16.2.3",
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.5",
"@types/file-saver-es": "^2.0.1", "@types/file-saver-es": "^2.0.1",
"@types/node": "^18.14.6", "@types/node": "^20.5.8",
"@typescript-eslint/eslint-plugin": "5.54.1", "@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "5.54.1", "@typescript-eslint/parser": "6.5.0",
"eslint": "^8.35.0", "eslint": "^8.48.0",
"prettier": "^2.8.4", "prettier": "^3.0.3",
"typescript": "~4.9.5" "typescript": "5.1.6"
} }
} }

View file

@ -18,8 +18,6 @@
</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">
@ -33,8 +31,16 @@
></textarea> ></textarea>
</div> </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>
<hr /> <div class="separator">Advanced settings</div>
<div class="field"> <div class="field">
<label class="label">Post Download Action</label> <label class="label">Post Download Action</label>

View file

@ -17,3 +17,28 @@
.is-block { .is-block {
display: block; display: block;
} }
.separator {
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: .25em;
}
.separator:not(:empty)::after {
margin-left: .25em;
}

View file

@ -1,12 +1,12 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AuthResolverService implements Resolve<Observable<any>> { export class AuthResolverService {
constructor(private authService: AuthService) {} constructor(private authService: AuthService) {}
resolve() { resolve() {

View file

@ -1,44 +1,45 @@
import { APP_BASE_HREF } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Inject, 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 {
constructor(private http: HttpClient) {} constructor(private http: HttpClient, @Inject(APP_BASE_HREF) private baseHref: string) {}
public isLoggedIn(): Observable<boolean> { public isLoggedIn(): Observable<boolean> {
return this.http.get<boolean>(`/Api/Authentication/IsLoggedIn`); return this.http.get<boolean>(`${this.baseHref}Api/Authentication/IsLoggedIn`);
} }
public create(userName: string, password: string): Observable<void> { public create(userName: string, password: string): Observable<void> {
return this.http.post<void>(`/Api/Authentication/Create`, { return this.http.post<void>(`${this.baseHref}Api/Authentication/Create`, {
userName, userName,
password, password,
}); });
} }
public setupProvider(provider: string, token: string): Observable<void> { public setupProvider(provider: string, token: string): Observable<void> {
return this.http.post<void>(`/Api/Authentication/SetupProvider`, { return this.http.post<void>(`${this.baseHref}Api/Authentication/SetupProvider`, {
provider, provider,
token, token,
}); });
} }
public login(userName: string, password: string): Observable<void> { public login(userName: string, password: string): Observable<void> {
return this.http.post<void>(`/Api/Authentication/Login`, { return this.http.post<void>(`${this.baseHref}Api/Authentication/Login`, {
userName, userName,
password, password,
}); });
} }
public logout() { public logout() {
return this.http.post<void>(`/Api/Authentication/Logout`, {}); return this.http.post<void>(`${this.baseHref}Api/Authentication/Logout`, {});
} }
public update(userName: string, password: string): Observable<void> { public update(userName: string, password: string): Observable<void> {
return this.http.post<void>(`/Api/Authentication/Update`, { return this.http.post<void>(`${this.baseHref}Api/Authentication/Update`, {
userName, userName,
password, password,
}); });

View file

@ -3,7 +3,7 @@
<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">

View file

@ -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
@ -55,7 +55,7 @@
<a class="navbar-item" routerLink="profile"> Profile </a> <a class="navbar-item" routerLink="profile"> Profile </a>
<a class="navbar-item" (click)="logout()"> Logout </a> <a class="navbar-item" (click)="logout()"> Logout </a>
<hr class="navbar-divider" /> <hr class="navbar-divider" />
<div class="navbar-item">Version 2.0.24</div> <a href="https://github.com/rogerfar/rdt-client" target="_blank" class="navbar-item">Version 2.0.36</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,41 +1,42 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Inject, 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';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class SettingsService { export class SettingsService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient, @Inject(APP_BASE_HREF) private baseHref: string) {}
public get(): Observable<Setting[]> { public get(): Observable<Setting[]> {
return this.http.get<Setting[]>(`/Api/Settings`); return this.http.get<Setting[]>(`${this.baseHref}Api/Settings`);
} }
public update(settings: Setting[]): Observable<void> { public update(settings: Setting[]): Observable<void> {
return this.http.put<void>(`/Api/Settings`, settings); return this.http.put<void>(`${this.baseHref}Api/Settings`, settings);
} }
public getProfile(): Observable<Profile> { public getProfile(): Observable<Profile> {
return this.http.get<Profile>(`/Api/Settings/Profile`); return this.http.get<Profile>(`${this.baseHref}Api/Settings/Profile`);
} }
public testPath(path: string): Observable<void> { public testPath(path: string): Observable<void> {
return this.http.post<void>(`/Api/Settings/TestPath`, { path }); return this.http.post<void>(`${this.baseHref}Api/Settings/TestPath`, { path });
} }
public testDownloadSpeed(): Observable<number> { public testDownloadSpeed(): Observable<number> {
return this.http.get<number>(`/Api/Settings/TestDownloadSpeed`); return this.http.get<number>(`${this.baseHref}Api/Settings/TestDownloadSpeed`);
} }
public testWriteSpeed(): Observable<number> { public testWriteSpeed(): Observable<number> {
return this.http.get<number>(`/Api/Settings/TestWriteSpeed`); return this.http.get<number>(`${this.baseHref}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 }>(`/Api/Settings/TestAria2cConnection`, { return this.http.post<{ version: string }>(`${this.baseHref}Api/Settings/TestAria2cConnection`, {
url, url,
secret, secret,
}); });

View file

@ -3,7 +3,7 @@
<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" *ngIf="step === 1"> <div class="box" *ngIf="step === 1">
<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.
@ -52,7 +52,7 @@
>Use this link to sign up to AllDebrid.</a >Use this link to sign up to AllDebrid.</a
> >
<br /> <br />
<a href="https://www.premiumize.me/" target="_blank" rel="noopener" <a href="https://www.premiumize.me/" target="_blank" rel="noopener"
>Use this link to sign up to Premiumize.</a >Use this link to sign up to Premiumize.</a
> >
<br /> <br />
@ -64,7 +64,7 @@
<select [(ngModel)]="provider"> <select [(ngModel)]="provider">
<option [value]="0">Real-Debrid</option> <option [value]="0">Real-Debrid</option>
<option [value]="1">AllDebrid</option> <option [value]="1">AllDebrid</option>
<option [value]="2">Premiumize</option> <option [value]="2">Premiumize</option>
</select> </select>
</div> </div>
</div> </div>
@ -85,7 +85,7 @@
>https://alldebrid.com/apikeys/</a >https://alldebrid.com/apikeys/</a
>. >.
</p> </p>
<p class="help" *ngIf="provider === 'Premiumize'"> <p class="help" *ngIf="provider === 'Premiumize'">
You can find your API key here: You can find your API key here:
<a href="https://www.premiumize.me/account" target="_blank" rel="noopener" <a href="https://www.premiumize.me/account" target="_blank" rel="noopener"
>https://www.premiumize.me/account</a >https://www.premiumize.me/account</a

View file

@ -1,8 +1,9 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Inject, 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 { APP_BASE_HREF } from '@angular/common';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -12,7 +13,7 @@ export class TorrentService {
private connection: signalR.HubConnection; private connection: signalR.HubConnection;
constructor(private http: HttpClient) { constructor(private http: HttpClient, @Inject(APP_BASE_HREF) private baseHref: string) {
this.connect(); this.connect();
} }
@ -21,7 +22,10 @@ export class TorrentService {
return; return;
} }
this.connection = new signalR.HubConnectionBuilder().withUrl('/hub').withAutomaticReconnect().build(); this.connection = new signalR.HubConnectionBuilder()
.withUrl(`${this.baseHref}hub`)
.withAutomaticReconnect()
.build();
this.connection.start().catch((err) => console.error(err)); this.connection.start().catch((err) => console.error(err));
this.connection.on('update', (torrents: Torrent[]) => { this.connection.on('update', (torrents: Torrent[]) => {
@ -30,15 +34,15 @@ export class TorrentService {
} }
public getList(): Observable<Torrent[]> { public getList(): Observable<Torrent[]> {
return this.http.get<Torrent[]>(`/Api/Torrents`); return this.http.get<Torrent[]>(`${this.baseHref}Api/Torrents`);
} }
public get(torrentId: string): Observable<Torrent> { public get(torrentId: string): Observable<Torrent> {
return this.http.get<Torrent>(`/Api/Torrents/Get/${torrentId}`); return this.http.get<Torrent>(`${this.baseHref}Api/Torrents/Get/${torrentId}`);
} }
public uploadMagnet(magnetLink: string, torrent: Torrent): Observable<void> { public uploadMagnet(magnetLink: string, torrent: Torrent): Observable<void> {
return this.http.post<void>(`/Api/Torrents/UploadMagnet`, { return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadMagnet`, {
magnetLink, magnetLink,
torrent, torrent,
}); });
@ -48,11 +52,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>(`/Api/Torrents/UploadFile`, formData); return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadFile`, formData);
} }
public checkFilesMagnet(magnetLink: string): Observable<TorrentFileAvailability[]> { public checkFilesMagnet(magnetLink: string): Observable<TorrentFileAvailability[]> {
return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFilesMagnet`, { return this.http.post<TorrentFileAvailability[]>(`${this.baseHref}Api/Torrents/CheckFilesMagnet`, {
magnetLink, magnetLink,
}); });
} }
@ -60,7 +64,7 @@ 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[]>(`/Api/Torrents/CheckFiles`, formData); return this.http.post<TorrentFileAvailability[]>(`${this.baseHref}Api/Torrents/CheckFiles`, formData);
} }
public delete( public delete(
@ -69,7 +73,7 @@ export class TorrentService {
deleteRdTorrent: boolean, deleteRdTorrent: boolean,
deleteLocalFiles: boolean deleteLocalFiles: boolean
): Observable<void> { ): Observable<void> {
return this.http.post<void>(`/Api/Torrents/Delete/${torrentId}`, { return this.http.post<void>(`${this.baseHref}Api/Torrents/Delete/${torrentId}`, {
deleteData, deleteData,
deleteRdTorrent, deleteRdTorrent,
deleteLocalFiles, deleteLocalFiles,
@ -77,14 +81,14 @@ export class TorrentService {
} }
public retry(torrentId: string): Observable<void> { public retry(torrentId: string): Observable<void> {
return this.http.post<void>(`/Api/Torrents/Retry/${torrentId}`, {}); return this.http.post<void>(`${this.baseHref}Api/Torrents/Retry/${torrentId}`, {});
} }
public retryDownload(downloadId: string): Observable<void> { public retryDownload(downloadId: string): Observable<void> {
return this.http.post<void>(`/Api/Torrents/RetryDownload/${downloadId}`, {}); return this.http.post<void>(`${this.baseHref}Api/Torrents/RetryDownload/${downloadId}`, {});
} }
public update(torrent: Torrent): Observable<void> { public update(torrent: Torrent): Observable<void> {
return this.http.put<void>(`/Api/Torrents/Update`, torrent); return this.http.put<void>(`${this.baseHref}Api/Torrents/Update`, torrent);
} }
} }

View file

@ -469,6 +469,19 @@
<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">
<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"> <div class="field">
<label class="label">Category</label> <label class="label">Category</label>
<div class="control"> <div class="control">

View file

@ -36,6 +36,7 @@ export class TorrentComponent implements OnInit {
public isUpdateSettingsModalActive: boolean; public isUpdateSettingsModalActive: boolean;
public updateSettingsHostDownloadAction: number;
public updateSettingsCategory: string; public updateSettingsCategory: string;
public updateSettingsPriority: number; public updateSettingsPriority: number;
public updateSettingsDownloadRetryAttempts: number; public updateSettingsDownloadRetryAttempts: number;
@ -176,6 +177,7 @@ export class TorrentComponent implements OnInit {
} }
public showUpdateSettingsModal(): void { public showUpdateSettingsModal(): void {
this.updateSettingsHostDownloadAction = this.torrent.hostDownloadAction;
this.updateSettingsCategory = this.torrent.category; 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;
@ -193,6 +195,7 @@ export class TorrentComponent implements OnInit {
public updateSettingsOk(): void { public updateSettingsOk(): void {
this.updating = true; this.updating = true;
this.torrent.hostDownloadAction = this.updateSettingsHostDownloadAction;
this.torrent.category = this.updateSettingsCategory; 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;

View file

@ -6,8 +6,11 @@
<base href="/" /> <base href="/" />
<script> <script>
(function () { (function () {
window["_app_base"] = "/" + window.location.pathname.split("/")[1]; if (window.location.host !== 'localhost:4200') {
console.log("setting base href to " + window["_app_base"]); var pathSegment = window.location.pathname.split("/")[1];
window["_app_base"] = pathSegment ? "/" + pathSegment + "/" : "/";
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" />

View file

@ -24,7 +24,7 @@ if ($SkipCache.IsPresent) { $dockerArgs += @("--no-cache" ) }
& docker $dockerArgs & docker $dockerArgs
Write-Host "Starting Container" Write-Host "Starting Container"
docker run --cap-add=NET_ADMIN -d -v $($downloadPath):/data/downloads -v $($dbPath):/data/db --log-driver json-file --log-opt max-size=10m -p 6500:6500 --name rdtclientdev rdtclientdev & docker run --cap-add=NET_ADMIN -d -v ${$downloadPath}:/data/downloads -v ${$dbPath}:/data/db --log-driver json-file --log-opt max-size=10m -p 6500:6500 --name rdtclientdev rdtclientdev
if ($AutoAttach.IsPresent) { if ($AutoAttach.IsPresent) {
docker exec -it rdtclientdev /bin/bash docker exec -it rdtclientdev /bin/bash

View file

@ -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/arm/v7,linux/arm64/v8,linux/amd64) The platforms to target for the image (defaults to 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,7 +34,7 @@
param( param(
[string]$Version = "", [string]$Version = "",
[string]$DockerAccount = "rogerfar", [string]$DockerAccount = "rogerfar",
[string]$Platforms = "linux/arm/v7,linux/arm64/v8,linux/amd64", [string]$Platforms = "linux/arm64/v8,linux/amd64",
[string]$Dockerfile = "Dockerfile", [string]$Dockerfile = "Dockerfile",
[switch]$SkipPush, [switch]$SkipPush,
[switch]$SkipCache, [switch]$SkipCache,

View file

@ -1,6 +1,6 @@
{ {
"name": "rdt-client", "name": "rdt-client",
"version": "2.0.24", "version": "2.0.36",
"description": "This is a web interface to manage your torrents on Real-Debrid.", "description": "This is a web interface to manage your torrents on Real-Debrid.",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {

View file

@ -147,6 +147,7 @@ public class TorrentData
return; return;
} }
dbTorrent.HostDownloadAction = torrent.HostDownloadAction;
dbTorrent.Category = torrent.Category; dbTorrent.Category = torrent.Category;
dbTorrent.Priority = torrent.Priority; dbTorrent.Priority = torrent.Priority;
dbTorrent.DownloadRetryAttempts = torrent.DownloadRetryAttempts; dbTorrent.DownloadRetryAttempts = torrent.DownloadRetryAttempts;

View file

@ -14,6 +14,6 @@ public class UserData
public async Task<IdentityUser?> GetUser() public async Task<IdentityUser?> GetUser()
{ {
return await _dataContext.Users.FirstOrDefaultAsync(); return await _dataContext.Users.OrderBy(m => m.Id).FirstOrDefaultAsync();
} }
} }

View file

@ -6,6 +6,7 @@ public class AppSettings
public AppSettingsDatabase? Database { get; set; } public AppSettingsDatabase? Database { get; set; }
public Int32 Port { get; set; } public Int32 Port { get; set; }
public String? BasePath { get; set; }
} }
public class AppSettingsLogging public class AppSettingsLogging

View file

@ -95,10 +95,6 @@ public class DbSettingsDownloadClient
[Description("Maximum amount of parallel threads that are used to download a single file to your host. If set to 0 no parallel downloading will be done.")] [Description("Maximum amount of parallel threads that are used to download a single file to your host. If set to 0 no parallel downloading will be done.")]
public Int32 ParallelCount { get; set; } = 0; public Int32 ParallelCount { get; set; } = 0;
[DisplayName("Parallel chunks per download (only used for the Internal Downloader)")]
[Description("Split each parallel download in chunks.")]
public Int32 ChunkCount { get; set; } = 1;
[DisplayName("Connection Timeout (only used for the Internal Downloader)")] [DisplayName("Connection Timeout (only used for the Internal Downloader)")]
[Description("Timeout in milliseconds before the downloader times out.")] [Description("Timeout in milliseconds before the downloader times out.")]
public Int32 Timeout { get; set; } = 5000; public Int32 Timeout { get; set; } = 5000;
@ -120,7 +116,7 @@ http://127.0.0.1:6800/jsonrpc.")]
public class DbSettingsProvider public class DbSettingsProvider
{ {
[DisplayName("Provider")] [DisplayName("Provider")]
[Description(@"The following 2 providers are supported: [Description(@"The following 3 providers are supported:
<a href=""https://real-debrid.com/?id=1348683"" target=""_blank"" rel=""noopener"">https://real-debrid.com</a> <a href=""https://real-debrid.com/?id=1348683"" target=""_blank"" rel=""noopener"">https://real-debrid.com</a>
<a href=""https://alldebrid.com/?uid=2v91l&lang=en"" target=""_blank"" rel=""noopener"">https://alldebrid.com</a> <a href=""https://alldebrid.com/?uid=2v91l&lang=en"" target=""_blank"" rel=""noopener"">https://alldebrid.com</a>
<a href=""https://www.premiumize.me/"" target=""_blank"" rel=""noopener"">https://www.premiumize.me/</a> <a href=""https://www.premiumize.me/"" target=""_blank"" rel=""noopener"">https://www.premiumize.me/</a>

View file

@ -8,11 +8,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.14" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.21" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.14" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.21" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.21" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="6.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="6.0.21" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.14"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.21">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -14,8 +14,10 @@ public static class DownloadHelper
return null; return null;
} }
var directory = RemoveInvalidPathChars(torrent.RdName);
var uri = new Uri(fileUrl); var uri = new Uri(fileUrl);
var torrentPath = Path.Combine(downloadPath, torrent.RdName); var torrentPath = Path.Combine(downloadPath, directory);
if (!Directory.Exists(torrentPath)) if (!Directory.Exists(torrentPath))
{ {
@ -26,8 +28,20 @@ public static class DownloadHelper
fileName = HttpUtility.UrlDecode(fileName); fileName = HttpUtility.UrlDecode(fileName);
fileName = RemoveInvalidFileNameChars(fileName);
var filePath = Path.Combine(torrentPath, fileName); var filePath = Path.Combine(torrentPath, fileName);
return filePath; return filePath;
} }
private static String RemoveInvalidPathChars(String path)
{
return String.Concat(path.Split(Path.GetInvalidPathChars()));
}
private static String RemoveInvalidFileNameChars(String filename)
{
return String.Concat(filename.Split(Path.GetInvalidFileNameChars()));
}
} }

View file

@ -16,9 +16,9 @@ public static class Logger
fileName = HttpUtility.UrlDecode(fileName); fileName = HttpUtility.UrlDecode(fileName);
} }
var done = (Int32)((Double)download.BytesDone / download.BytesTotal) * 100; var done = (Int32)((Double)download.BytesDone / download.BytesTotal * 100);
return $"for download {fileName} {done}% ({download.DownloadId})"; return $"for download {fileName}. Completed: {done}%, avg speed: {download.Speed}bytes/s ({download.DownloadId})";
} }
public static String ToLog(this Torrent torrent) public static String ToLog(this Torrent torrent)

View file

@ -0,0 +1,55 @@
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Http;
using RdtClient.Data.Models.Internal;
namespace RdtClient.Service.Middleware;
public class BaseHrefMiddleware
{
private readonly RequestDelegate _next;
public BaseHrefMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, AppSettings appSettings)
{
var originalBody = context.Response.Body;
try
{
using var newBody = new MemoryStream();
context.Response.Body = newBody;
await _next(context);
context.Response.Body = originalBody;
newBody.Seek(0, SeekOrigin.Begin);
var responseBody = await new StreamReader(newBody).ReadToEndAsync();
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
if (context.Response.ContentType?.Contains("text/html") == true)
{
var basePath = $"/{appSettings.BasePath!.TrimStart('/').TrimEnd('/')}/";
responseBody = Regex.Replace(responseBody, @"<base href=""/""", @$"<base href=""{basePath}""");
responseBody = Regex.Replace(responseBody, "(<script.*?src=\")(.*?)(\".*?</script>)", $"$1{basePath}$2$3");
responseBody = Regex.Replace(responseBody, "(<link.*?href=\")(.*?)(\".*?>)", $"$1{basePath}$2$3");
context.Response.Headers.Remove("Content-Length");
await context.Response.WriteAsync(responseBody);
}
else
{
await context.Response.BodyWriter.WriteAsync(newBody.ToArray());
}
}
finally
{
context.Response.Body = originalBody;
}
}
}

View file

@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Text;
namespace RdtClient.Service.Middleware;
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
_next = next;
_logger = loggerFactory.CreateLogger<RequestLoggingMiddleware>();
}
public async Task Invoke(HttpContext context)
{
if (!_logger.IsEnabled(LogLevel.Debug) || (!context.Request.Path.StartsWithSegments("/api/v2") && !context.Request.Path.StartsWithSegments("/api/torrents")))
{
await _next(context);
return;
}
var requestLog = $"Method: {context.Request.Method}, Path: {context.Request.Path}";
if (context.Request.QueryString.HasValue)
{
requestLog += $", QueryString: {context.Request.QueryString}";
}
if (context.Request.HasFormContentType && context.Request.Form.Any())
{
requestLog += $", Form: {String.Join(", ", context.Request.Form.Select(f => $"{f.Key}: {f.Value}"))}";
}
else if (context.Request.ContentType?.ToLower().Contains("application/json") == true)
{
var body = await ReadRequestBodyAsync(context.Request);
requestLog += $", Body: {body}";
}
_logger.LogDebug(requestLog);
await _next(context);
}
private static async Task<String> ReadRequestBodyAsync(HttpRequest request)
{
request.EnableBuffering();
using var reader = new StreamReader(request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
var body = await reader.ReadToEndAsync();
request.Body.Position = 0;
return body;
}
}

View file

@ -11,13 +11,13 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="AllDebrid.NET" Version="1.0.12" /> <PackageReference Include="AllDebrid.NET" Version="1.0.12" />
<PackageReference Include="Aria2.NET" Version="1.0.5" /> <PackageReference Include="Aria2.NET" Version="1.0.5" />
<PackageReference Include="Downloader" Version="3.0.4" /> <PackageReference Include="Downloader.NET" Version="1.0.4" />
<PackageReference Include="MonoTorrent" Version="2.0.7" /> <PackageReference Include="MonoTorrent" Version="2.0.7" />
<PackageReference Include="Premiumize.NET" Version="1.0.1" /> <PackageReference Include="Premiumize.NET" Version="1.0.4" />
<PackageReference Include="RD.NET" Version="2.1.4" /> <PackageReference Include="RD.NET" Version="2.1.4" />
<PackageReference Include="Serilog" Version="2.12.0" /> <PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.33.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -13,7 +13,7 @@ public class DownloadClient
public IDownloader? Downloader; public IDownloader? Downloader;
public Data.Enums.DownloadClient Type { get; set; } public Data.Enums.DownloadClient Type { get; private set; }
public Boolean Finished { get; private set; } public Boolean Finished { get; private set; }

View file

@ -1,5 +1,4 @@
using System.Net; using DownloaderNET;
using Downloader;
using Serilog; using Serilog;
namespace RdtClient.Service.Services.Downloaders; namespace RdtClient.Service.Services.Downloaders;
@ -9,14 +8,16 @@ public class InternalDownloader : IDownloader
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete; public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress; public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
private readonly DownloadService _downloadService; private readonly Downloader _downloadService;
private readonly DownloadConfiguration _downloadConfiguration; private readonly DownloaderNET.Settings _downloadConfiguration;
private readonly String _filePath; private readonly String _filePath;
private readonly String _uri; private readonly String _uri;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly CancellationTokenSource _cancellationToken = new();
private Boolean _finished; private Boolean _finished;
public InternalDownloader(String uri, String filePath) public InternalDownloader(String uri, String filePath)
@ -25,40 +26,14 @@ public class InternalDownloader : IDownloader
_uri = uri; _uri = uri;
_filePath = filePath; _filePath = filePath;
var settingProxyServer = Settings.Get.DownloadClient.ProxyServer; _downloadConfiguration = new DownloaderNET.Settings();
// For all options, see https://github.com/bezzad/Downloader
_downloadConfiguration = new DownloadConfiguration
{
BufferBlockSize = 1024 * 8,
MaxTryAgainOnFailover = 5,
RangeDownload = false,
ClearPackageOnCompletionWithFailure = false,
MinimumSizeOfChunking = 1024,
ReserveStorageSpaceBeforeStartingDownload = false,
CheckDiskSizeBeforeDownload = true,
RequestConfiguration =
{
Accept = "*/*",
UserAgent = $"rdt-client",
ProtocolVersion = HttpVersion.Version11,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
KeepAlive = true,
UseDefaultCredentials = false
}
};
SetSettings(); SetSettings();
if (!String.IsNullOrWhiteSpace(settingProxyServer)) _downloadService = new Downloader(_uri, _filePath, _downloadConfiguration, _cancellationToken.Token);
{
_downloadConfiguration.RequestConfiguration.Proxy = new WebProxy(new Uri(settingProxyServer), false);
}
_downloadService = new DownloadService(_downloadConfiguration); _downloadService.OnProgress += args =>
_downloadService.DownloadProgressChanged += (_, args) =>
{ {
if (DownloadProgress == null) if (DownloadProgress == null)
{ {
@ -68,32 +43,23 @@ public class InternalDownloader : IDownloader
DownloadProgress.Invoke(this, DownloadProgress.Invoke(this,
new DownloadProgressEventArgs new DownloadProgressEventArgs
{ {
Speed = (Int64)args.BytesPerSecondSpeed, Speed = (Int64) args.Sum(m => m.Speed),
BytesDone = args.ReceivedBytesSize, BytesDone = args.Sum(m => m.DownloadBytes),
BytesTotal = args.TotalBytesToReceive BytesTotal = args.Sum(m => m.LengthBytes)
}); });
}; };
_downloadService.DownloadFileCompleted += (_, args) => _downloadService.OnComplete += error =>
{ {
String? error = null;
if (args.Cancelled)
{
error = $"The download was cancelled";
}
else if (args.Error != null)
{
error = args.Error.Message;
}
DownloadComplete?.Invoke(this, DownloadComplete?.Invoke(this,
new DownloadCompleteEventArgs new DownloadCompleteEventArgs
{ {
Error = error Error = error?.Message
}); });
_finished = true; _finished = true;
return Task.CompletedTask;
}; };
} }
@ -101,11 +67,7 @@ public class InternalDownloader : IDownloader
{ {
_logger.Debug($"Starting download of {_uri}, writing to path: {_filePath}"); _logger.Debug($"Starting download of {_uri}, writing to path: {_filePath}");
Task.Run(async () => Task.Run(_downloadService.Download);
{
await _downloadService.DownloadFileTaskAsync(_uri, _filePath);
});
Task.Run(StartTimer); Task.Run(StartTimer);
return Task.FromResult<String?>(null); return Task.FromResult<String?>(null);
@ -115,7 +77,7 @@ public class InternalDownloader : IDownloader
{ {
_logger.Debug($"Cancelling download {_uri}"); _logger.Debug($"Cancelling download {_uri}");
_downloadService.CancelAsync(); _cancellationToken.Cancel(false);
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -138,14 +100,7 @@ public class InternalDownloader : IDownloader
{ {
settingDownloadParallelCount = 1; settingDownloadParallelCount = 1;
} }
var settingDownloadChunkCount = Settings.Get.DownloadClient.ChunkCount;
if (settingDownloadChunkCount <= 0)
{
settingDownloadChunkCount = 1;
}
var settingDownloadMaxSpeed = Settings.Get.DownloadClient.MaxSpeed; var settingDownloadMaxSpeed = Settings.Get.DownloadClient.MaxSpeed;
if (settingDownloadMaxSpeed <= 0) if (settingDownloadMaxSpeed <= 0)
@ -162,10 +117,8 @@ public class InternalDownloader : IDownloader
settingDownloadTimeout = 1000; settingDownloadTimeout = 1000;
} }
_downloadConfiguration.ChunkCount = settingDownloadChunkCount; _downloadConfiguration.ChunkCount = settingDownloadParallelCount;
_downloadConfiguration.MaximumBytesPerSecond = settingDownloadMaxSpeed; _downloadConfiguration.MaximumBytesPerSecond = settingDownloadMaxSpeed;
_downloadConfiguration.ParallelDownload = settingDownloadChunkCount > 1;
_downloadConfiguration.ParallelCount = settingDownloadParallelCount;
_downloadConfiguration.Timeout = settingDownloadTimeout; _downloadConfiguration.Timeout = settingDownloadTimeout;
} }

View file

@ -228,29 +228,47 @@ public class PremiumizeTorrentClient : ITorrentClient
} }
var transfers = await GetClient().Transfers.ListAsync(); var transfers = await GetClient().Transfers.ListAsync();
Log($"Found {transfers.Count} transfers", torrent);
var transfer = transfers.FirstOrDefault(m => m.Id == torrent.RdId); var transfer = transfers.FirstOrDefault(m => m.Id == torrent.RdId);
if (transfer == null) if (transfer == null)
{ {
throw new Exception($"Transfer {torrent.RdId} not found!");
}
Log($"Found transfer {transfer.Name} ({transfer.Id})", torrent);
var downloadLinks = await GetAllDownloadLinks(torrent, transfer.FolderId);
if (!String.IsNullOrWhiteSpace(transfer.FileId))
{
var file = await GetClient().Items.DetailsAsync(transfer.FileId);
Log($"Found {transfer.FileId}", torrent);
if (String.IsNullOrWhiteSpace(file.Link))
{
Log($"File {file.Name} ({file.Id}) does not contain a link", torrent);
}
downloadLinks.Add(file.Link);
}
if (downloadLinks.Count == 0)
{
Log($"No download links found for transfer {transfer.Name} ({transfer.Id})", torrent);
return null; return null;
} }
if (String.IsNullOrWhiteSpace(transfer.FolderId)) foreach (var link in downloadLinks)
{ {
return null; Log($"Found {link}", torrent);
} }
var zip = await GetClient() return downloadLinks;
.Zip.Generate(new List<String>(),
new List<String>
{
transfer.FolderId
});
return new List<String>
{
zip
};
} }
private async Task<TorrentClientTorrent> GetInfo(String id) private async Task<TorrentClientTorrent> GetInfo(String id)
@ -266,6 +284,44 @@ public class PremiumizeTorrentClient : ITorrentClient
return Map(result); return Map(result);
} }
private async Task<List<String>> GetAllDownloadLinks(Torrent torrent, String folderId)
{
var downloadLinks = new List<String>();
if (String.IsNullOrWhiteSpace(folderId))
{
return downloadLinks;
}
var folder = await GetClient().Folder.ListAsync(folderId);
if (folder.Content == null)
{
Log($"Found no items in folder {folder.Name} ({folderId})", torrent);
return downloadLinks;
}
Log($"Found {folder.Content.Count} items in folder {folder.Name} ({folderId})", torrent);
foreach (var item in folder.Content)
{
if (!String.IsNullOrWhiteSpace(item.Link))
{
Log($"Found item {item.Name} in folder {folder.Name} ({folderId}) with link {item.Link}", torrent);
downloadLinks.Add(item.Link);
}
else
{
Log($"Found item {item.Name} in folder {folder.Name} ({folderId}), but has no link", torrent);
}
var subDownloadLinks = await GetAllDownloadLinks(torrent, item.FolderId);
downloadLinks.AddRange(subDownloadLinks);
}
return downloadLinks;
}
private void Log(String message, Torrent? torrent = null) private void Log(String message, Torrent? torrent = null)
{ {
if (torrent != null) if (torrent != null)

View file

@ -161,7 +161,7 @@ public class TorrentRunner
if (!String.IsNullOrWhiteSpace(downloadClient.Error)) if (!String.IsNullOrWhiteSpace(downloadClient.Error))
{ {
// Retry the download if an error is encountered. // Retry the download if an error is encountered.
Log($"Download reported an error: {downloadClient.Error}", download, download.Torrent); LogError($"Download reported an error: {downloadClient.Error}", download, download.Torrent);
Log($"Download retry count {download.RetryCount}/{download.Torrent!.DownloadRetryAttempts}, torrent retry count {download.Torrent.RetryCount}/{download.Torrent.TorrentRetryAttempts}", download, download.Torrent); Log($"Download retry count {download.RetryCount}/{download.Torrent!.DownloadRetryAttempts}, torrent retry count {download.Torrent.RetryCount}/{download.Torrent.TorrentRetryAttempts}", download, download.Torrent);
if (download.RetryCount < download.Torrent.DownloadRetryAttempts) if (download.RetryCount < download.Torrent.DownloadRetryAttempts)
@ -512,9 +512,19 @@ public class TorrentRunner
if ((torrent.Downloads.Count > 0) || if ((torrent.Downloads.Count > 0) ||
torrent.RdStatus == TorrentStatus.Finished && torrent.HostDownloadAction == TorrentHostDownloadAction.DownloadNone) torrent.RdStatus == TorrentStatus.Finished && torrent.HostDownloadAction == TorrentHostDownloadAction.DownloadNone)
{ {
var allComplete = torrent.Downloads.Count(m => m.Completed != null); var completeCount = torrent.Downloads.Count(m => m.Completed != null);
if (allComplete == torrent.Downloads.Count) var completePerc = 0;
var totalDownloadBytes = torrent.Downloads.Sum(m => m.BytesTotal);
var totalDoneBytes = torrent.Downloads.Sum(m => m.BytesDone);
if (totalDownloadBytes > 0)
{
completePerc = (Int32)((Double)totalDoneBytes / totalDownloadBytes * 100);
}
if (completeCount == torrent.Downloads.Count)
{ {
Log($"All downloads complete, marking torrent as complete", torrent); Log($"All downloads complete, marking torrent as complete", torrent);
@ -558,7 +568,7 @@ public class TorrentRunner
} }
else else
{ {
Log($"Waiting for downloads to complete. {allComplete}/{torrent.Downloads.Count} complete", torrent); Log($"Waiting for downloads to complete. {completeCount}/{torrent.Downloads.Count} complete ({completePerc}%)", torrent);
} }
} }
} }
@ -603,4 +613,19 @@ public class TorrentRunner
_logger.LogDebug(message); _logger.LogDebug(message);
} }
private void LogError(String message, Download? download, Torrent? torrent)
{
if (download != null)
{
message = $"{message} {download.ToLog()}";
}
if (torrent != null)
{
message = $"{message} {torrent.ToLog()}";
}
_logger.LogError(message);
}
} }

View file

@ -23,8 +23,24 @@ public class Torrents
private readonly ILogger<Torrents> _logger; private readonly ILogger<Torrents> _logger;
private readonly TorrentData _torrentData; private readonly TorrentData _torrentData;
private readonly Downloads _downloads; private readonly Downloads _downloads;
private readonly ITorrentClient _torrentClient; private readonly AllDebridTorrentClient _allDebridTorrentClient;
private readonly PremiumizeTorrentClient _premiumizeTorrentClient;
private readonly RealDebridTorrentClient _realDebridTorrentClient;
private ITorrentClient TorrentClient
{
get
{
return Settings.Get.Provider.Provider switch
{
Provider.Premiumize => _premiumizeTorrentClient,
Provider.RealDebrid => _realDebridTorrentClient,
Provider.AllDebrid => _allDebridTorrentClient,
_ => throw new Exception("Invalid Provider")
};
}
}
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1); private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
@ -38,14 +54,9 @@ public class Torrents
_logger = logger; _logger = logger;
_torrentData = torrentData; _torrentData = torrentData;
_downloads = downloads; _downloads = downloads;
_allDebridTorrentClient = allDebridTorrentClient;
_torrentClient = Settings.Get.Provider.Provider switch _premiumizeTorrentClient = premiumizeTorrentClient;
{ _realDebridTorrentClient = realDebridTorrentClient;
Provider.Premiumize => premiumizeTorrentClient,
Provider.RealDebrid => realDebridTorrentClient,
Provider.AllDebrid => allDebridTorrentClient,
_ => throw new Exception("Invalid Provider")
};
} }
public async Task<IList<Torrent>> Get() public async Task<IList<Torrent>> Get()
@ -114,7 +125,7 @@ public class Torrents
throw new Exception($"{ex.Message}, trying to parse {magnetLink}"); throw new Exception($"{ex.Message}, trying to parse {magnetLink}");
} }
var id = await _torrentClient.AddMagnet(magnetLink); var id = await TorrentClient.AddMagnet(magnetLink);
var hash = magnet.InfoHash.ToHex(); var hash = magnet.InfoHash.ToHex();
@ -140,7 +151,7 @@ public class Torrents
throw new Exception($"{ex.Message}, trying to parse {fileAsBase64}"); throw new Exception($"{ex.Message}, trying to parse {fileAsBase64}");
} }
var id = await _torrentClient.AddFile(bytes); var id = await TorrentClient.AddFile(bytes);
var hash = monoTorrent.InfoHash.ToHex(); var hash = monoTorrent.InfoHash.ToHex();
@ -153,7 +164,7 @@ public class Torrents
public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
{ {
var result = await _torrentClient.GetAvailableFiles(hash); var result = await TorrentClient.GetAvailableFiles(hash);
return result; return result;
} }
@ -167,7 +178,7 @@ public class Torrents
return; return;
} }
await _torrentClient.SelectFiles(torrent); await TorrentClient.SelectFiles(torrent);
} }
public async Task CreateDownloads(Guid torrentId) public async Task CreateDownloads(Guid torrentId)
@ -179,7 +190,7 @@ public class Torrents
return; return;
} }
var downloadLinks = await _torrentClient.GetDownloadLinks(torrent); var downloadLinks = await TorrentClient.GetDownloadLinks(torrent);
if (downloadLinks == null) if (downloadLinks == null)
{ {
@ -264,7 +275,7 @@ public class Torrents
try try
{ {
await _torrentClient.Delete(torrent.RdId); await TorrentClient.Delete(torrent.RdId);
} }
catch catch
{ {
@ -318,7 +329,7 @@ public class Torrents
Log($"Unrestricting link", download, download.Torrent); Log($"Unrestricting link", download, download.Torrent);
var unrestrictedLink = await _torrentClient.Unrestrict(download.Path); var unrestrictedLink = await TorrentClient.Unrestrict(download.Path);
await _downloads.UpdateUnrestrictedLink(downloadId, unrestrictedLink); await _downloads.UpdateUnrestrictedLink(downloadId, unrestrictedLink);
@ -327,7 +338,7 @@ public class Torrents
public async Task<Profile> GetProfile() public async Task<Profile> GetProfile()
{ {
var user = await _torrentClient.GetUser(); var user = await TorrentClient.GetUser();
var profile = new Profile var profile = new Profile
{ {
@ -349,7 +360,7 @@ public class Torrents
try try
{ {
var rdTorrents = await _torrentClient.GetTorrents(); var rdTorrents = await TorrentClient.GetTorrents();
foreach (var rdTorrent in rdTorrents) foreach (var rdTorrent in rdTorrents)
{ {
@ -728,7 +739,7 @@ public class Torrents
ReferenceHandler = ReferenceHandler.IgnoreCycles ReferenceHandler = ReferenceHandler.IgnoreCycles
}); });
await _torrentClient.UpdateData(torrent, torrentClientTorrent); await TorrentClient.UpdateData(torrent, torrentClientTorrent);
var newTorrent = JsonSerializer.Serialize(torrent, var newTorrent = JsonSerializer.Serialize(torrent,
new JsonSerializerOptions new JsonSerializerOptions

View file

@ -36,7 +36,7 @@ public class AuthController : Controller
return StatusCode(402, "Setup required"); return StatusCode(402, "Setup required");
} }
return Unauthorized(); return StatusCode(403);
} }
return Ok(); return Ok();

View file

@ -164,6 +164,13 @@ try
} }
}); });
if (!String.IsNullOrWhiteSpace(appSettings.BasePath))
{
app.UseMiddleware<BaseHrefMiddleware>();
}
app.UseMiddleware<RequestLoggingMiddleware>();
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); app.UseAuthentication();

View file

@ -17,7 +17,7 @@
}, },
"RdtClient.Web": { "RdtClient.Web": {
"commandName": "Project", "commandName": "Project",
"launchUrl": "weatherforecast", "launchUrl": "",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },

View file

@ -4,7 +4,7 @@
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId> <UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId>
<Version>2.0.24</Version> <Version>2.0.36</Version>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
@ -29,15 +29,15 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.14" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.21" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.21" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="6.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="6.0.21" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.14"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.21">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="6.0.14" /> <PackageReference Include="Microsoft.Extensions.Identity.Core" Version="6.0.21" />
<PackageReference Include="Serilog" Version="2.12.0" /> <PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.1.0" /> <PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />

View file

@ -9,5 +9,6 @@
"Database": { "Database": {
"Path": "/data/db/rdtclient.db" "Path": "/data/db/rdtclient.db"
}, },
"Port": "6500" "Port": "6500",
"BasePath": null
} }