commit
fe0c3a1643
43 changed files with 7857 additions and 4884 deletions
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -13,4 +13,6 @@ assignees: ''
|
|||
|
||||
**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**
|
||||
|
|
|
|||
8
.github/workflows/docker-image.yml
vendored
8
.github/workflows/docker-image.yml
vendored
|
|
@ -9,10 +9,10 @@ permissions:
|
|||
packages: write
|
||||
|
||||
env:
|
||||
APP_NAME: rdt-client
|
||||
APP_NAME: rdtclient
|
||||
DOCKER_FILE: ./Dockerfile
|
||||
DOCKER_PLATFORMS: "linux/arm/v7,linux/arm64/v8,linux/amd64"
|
||||
ENABLE_DOCKERHUB: 0
|
||||
DOCKER_PLATFORMS: "linux/arm64/v8,linux/amd64"
|
||||
ENABLE_DOCKERHUB: 1
|
||||
ENABLE_GHCR: 1
|
||||
|
||||
jobs:
|
||||
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: |
|
||||
# ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}
|
||||
ghcr.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.APP_NAME }}
|
||||
tags: |
|
||||
type=schedule
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -6,4 +6,5 @@ node_modules/
|
|||
RealDebridClient.zip
|
||||
server/RdtClient.Web/appsettings.Development.json
|
||||
data
|
||||
Dockerfile.dev
|
||||
Dockerfile.dev
|
||||
test.bat
|
||||
|
|
|
|||
65
CHANGELOG.md
65
CHANGELOG.md
|
|
@ -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/),
|
||||
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
|
||||
### Changed
|
||||
- Fixed docker run issues.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||
ARG BUILDPLATFORM
|
||||
|
|
@ -8,6 +8,8 @@ ENV BUILDPLATFORM=${BUILDPLATFORM:-linux/amd64}
|
|||
RUN mkdir /appclient
|
||||
WORKDIR /appclient
|
||||
|
||||
RUN apk add --no-cache git python3 py3-pip make g++
|
||||
|
||||
RUN \
|
||||
echo "**** Cloning Source Code ****" && \
|
||||
git clone https://github.com/rogerfar/rdt-client.git . && \
|
||||
|
|
@ -45,7 +47,7 @@ RUN \
|
|||
fi
|
||||
|
||||
# Stage 3 - Build runtime image
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.17
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.18
|
||||
ARG TARGETPLATFORM
|
||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||
ARG BUILDPLATFORM
|
||||
|
|
|
|||
|
|
@ -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 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
|
||||
|
||||
### 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.
|
||||
|
||||
### 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
|
||||
|
||||
### Prerequisites
|
||||
|
|
|
|||
11992
client/package-lock.json
generated
11992
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -12,43 +12,43 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^15.2.2",
|
||||
"@angular/cdk": "^15.2.2",
|
||||
"@angular/common": "^15.2.2",
|
||||
"@angular/compiler": "^15.2.2",
|
||||
"@angular/core": "^15.2.2",
|
||||
"@angular/animations": "^16.2.3",
|
||||
"@angular/cdk": "^16.2.2",
|
||||
"@angular/common": "^16.2.3",
|
||||
"@angular/compiler": "^16.2.3",
|
||||
"@angular/core": "^16.2.3",
|
||||
"@angular/flex-layout": "^15.0.0-beta.42",
|
||||
"@angular/forms": "^15.2.2",
|
||||
"@angular/platform-browser": "^15.2.2",
|
||||
"@angular/platform-browser-dynamic": "^15.2.2",
|
||||
"@angular/router": "^15.2.2",
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"@microsoft/signalr": "^7.0.3",
|
||||
"@angular/forms": "^16.2.3",
|
||||
"@angular/platform-browser": "^16.2.3",
|
||||
"@angular/platform-browser-dynamic": "^16.2.3",
|
||||
"@angular/router": "^16.2.3",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@microsoft/signalr": "^6.0.21",
|
||||
"bulma": "^0.9.4",
|
||||
"curray": "^1.0.11",
|
||||
"file-saver-es": "^2.0.5",
|
||||
"ngx-filesize": "^3.0.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.5.0",
|
||||
"zone.js": "~0.13.0"
|
||||
"ngx-filesize": "^3.0.2",
|
||||
"rxjs": "~7.8.1",
|
||||
"tslib": "^2.6.2",
|
||||
"zone.js": "~0.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^15.2.2",
|
||||
"@angular-eslint/builder": "15.2.1",
|
||||
"@angular-eslint/eslint-plugin": "15.2.1",
|
||||
"@angular-eslint/eslint-plugin-template": "15.2.1",
|
||||
"@angular-eslint/schematics": "15.2.1",
|
||||
"@angular-eslint/template-parser": "15.2.1",
|
||||
"@angular/cli": "^15.2.2",
|
||||
"@angular/compiler-cli": "^15.2.2",
|
||||
"@angular/language-service": "^15.2.2",
|
||||
"@angular-devkit/build-angular": "^16.2.1",
|
||||
"@angular-eslint/builder": "16.1.1",
|
||||
"@angular-eslint/eslint-plugin": "16.1.1",
|
||||
"@angular-eslint/eslint-plugin-template": "16.1.1",
|
||||
"@angular-eslint/schematics": "16.1.1",
|
||||
"@angular-eslint/template-parser": "16.1.1",
|
||||
"@angular/cli": "^16.2.1",
|
||||
"@angular/compiler-cli": "^16.2.3",
|
||||
"@angular/language-service": "^16.2.3",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/file-saver-es": "^2.0.1",
|
||||
"@types/node": "^18.14.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||
"@typescript-eslint/parser": "5.54.1",
|
||||
"eslint": "^8.35.0",
|
||||
"prettier": "^2.8.4",
|
||||
"typescript": "~4.9.5"
|
||||
"@types/node": "^20.5.8",
|
||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||
"@typescript-eslint/parser": "6.5.0",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "5.1.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Magnet Link</label>
|
||||
<div class="control">
|
||||
|
|
@ -33,8 +31,16 @@
|
|||
></textarea>
|
||||
</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">
|
||||
<label class="label">Post Download Action</label>
|
||||
|
|
|
|||
|
|
@ -17,3 +17,28 @@
|
|||
.is-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;
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Resolve } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthResolverService implements Resolve<Observable<any>> {
|
||||
export class AuthResolverService {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
resolve() {
|
||||
|
|
|
|||
|
|
@ -1,44 +1,45 @@
|
|||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
constructor(private http: HttpClient) {}
|
||||
constructor(private http: HttpClient, @Inject(APP_BASE_HREF) private baseHref: string) {}
|
||||
|
||||
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> {
|
||||
return this.http.post<void>(`/Api/Authentication/Create`, {
|
||||
return this.http.post<void>(`${this.baseHref}Api/Authentication/Create`, {
|
||||
userName,
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
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> {
|
||||
return this.http.post<void>(`/Api/Authentication/Update`, {
|
||||
return this.http.post<void>(`${this.baseHref}Api/Authentication/Update`, {
|
||||
userName,
|
||||
password,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<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">
|
||||
<form (ngSubmit)="login()">
|
||||
<div class="field">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<nav class="navbar is-dark is-fixed-top" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" routerLink="/">
|
||||
<img src="../../assets/logo.png" />
|
||||
<img src="assets/logo.png" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
<a class="navbar-item" routerLink="profile"> Profile </a>
|
||||
<a class="navbar-item" (click)="logout()"> Logout </a>
|
||||
<hr class="navbar-divider" />
|
||||
<div class="navbar-item">Version 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>
|
||||
|
|
|
|||
|
|
@ -1,41 +1,42 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Profile } from './models/profile.model';
|
||||
import { Setting } from './models/setting.model';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SettingsService {
|
||||
constructor(private http: HttpClient) {}
|
||||
constructor(private http: HttpClient, @Inject(APP_BASE_HREF) private baseHref: string) {}
|
||||
|
||||
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> {
|
||||
return this.http.put<void>(`/Api/Settings`, settings);
|
||||
return this.http.put<void>(`${this.baseHref}Api/Settings`, settings);
|
||||
}
|
||||
|
||||
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> {
|
||||
return this.http.post<void>(`/Api/Settings/TestPath`, { path });
|
||||
return this.http.post<void>(`${this.baseHref}Api/Settings/TestPath`, { path });
|
||||
}
|
||||
|
||||
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> {
|
||||
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 }> {
|
||||
return this.http.post<{ version: string }>(`/Api/Settings/TestAria2cConnection`, {
|
||||
return this.http.post<{ version: string }>(`${this.baseHref}Api/Settings/TestAria2cConnection`, {
|
||||
url,
|
||||
secret,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<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="notification is-primary">
|
||||
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
|
||||
>
|
||||
<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
|
||||
>
|
||||
<br />
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
<select [(ngModel)]="provider">
|
||||
<option [value]="0">Real-Debrid</option>
|
||||
<option [value]="1">AllDebrid</option>
|
||||
<option [value]="2">Premiumize</option>
|
||||
<option [value]="2">Premiumize</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
>https://alldebrid.com/apikeys/</a
|
||||
>.
|
||||
</p>
|
||||
<p class="help" *ngIf="provider === 'Premiumize'">
|
||||
<p class="help" *ngIf="provider === 'Premiumize'">
|
||||
You can find your API key here:
|
||||
<a href="https://www.premiumize.me/account" target="_blank" rel="noopener"
|
||||
>https://www.premiumize.me/account</a
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import * as signalR from '@microsoft/signalr';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { Torrent, TorrentFileAvailability } from './models/torrent.model';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
|
@ -12,7 +13,7 @@ export class TorrentService {
|
|||
|
||||
private connection: signalR.HubConnection;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
constructor(private http: HttpClient, @Inject(APP_BASE_HREF) private baseHref: string) {
|
||||
this.connect();
|
||||
}
|
||||
|
||||
|
|
@ -21,7 +22,10 @@ export class TorrentService {
|
|||
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.on('update', (torrents: Torrent[]) => {
|
||||
|
|
@ -30,15 +34,15 @@ export class TorrentService {
|
|||
}
|
||||
|
||||
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> {
|
||||
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> {
|
||||
return this.http.post<void>(`/Api/Torrents/UploadMagnet`, {
|
||||
return this.http.post<void>(`${this.baseHref}Api/Torrents/UploadMagnet`, {
|
||||
magnetLink,
|
||||
torrent,
|
||||
});
|
||||
|
|
@ -48,11 +52,11 @@ export class TorrentService {
|
|||
const formData: FormData = new FormData();
|
||||
formData.append('file', file);
|
||||
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[]> {
|
||||
return this.http.post<TorrentFileAvailability[]>(`/Api/Torrents/CheckFilesMagnet`, {
|
||||
return this.http.post<TorrentFileAvailability[]>(`${this.baseHref}Api/Torrents/CheckFilesMagnet`, {
|
||||
magnetLink,
|
||||
});
|
||||
}
|
||||
|
|
@ -60,7 +64,7 @@ export class TorrentService {
|
|||
public checkFiles(file: File): Observable<TorrentFileAvailability[]> {
|
||||
const formData: FormData = new FormData();
|
||||
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(
|
||||
|
|
@ -69,7 +73,7 @@ export class TorrentService {
|
|||
deleteRdTorrent: boolean,
|
||||
deleteLocalFiles: boolean
|
||||
): Observable<void> {
|
||||
return this.http.post<void>(`/Api/Torrents/Delete/${torrentId}`, {
|
||||
return this.http.post<void>(`${this.baseHref}Api/Torrents/Delete/${torrentId}`, {
|
||||
deleteData,
|
||||
deleteRdTorrent,
|
||||
deleteLocalFiles,
|
||||
|
|
@ -77,14 +81,14 @@ export class TorrentService {
|
|||
}
|
||||
|
||||
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> {
|
||||
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> {
|
||||
return this.http.put<void>(`/Api/Torrents/Update`, torrent);
|
||||
return this.http.put<void>(`${this.baseHref}Api/Torrents/Update`, torrent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -469,6 +469,19 @@
|
|||
<button class="delete" aria-label="close" (click)="updateSettingsCancel()"></button>
|
||||
</header>
|
||||
<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">
|
||||
<label class="label">Category</label>
|
||||
<div class="control">
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export class TorrentComponent implements OnInit {
|
|||
|
||||
public isUpdateSettingsModalActive: boolean;
|
||||
|
||||
public updateSettingsHostDownloadAction: number;
|
||||
public updateSettingsCategory: string;
|
||||
public updateSettingsPriority: number;
|
||||
public updateSettingsDownloadRetryAttempts: number;
|
||||
|
|
@ -176,6 +177,7 @@ export class TorrentComponent implements OnInit {
|
|||
}
|
||||
|
||||
public showUpdateSettingsModal(): void {
|
||||
this.updateSettingsHostDownloadAction = this.torrent.hostDownloadAction;
|
||||
this.updateSettingsCategory = this.torrent.category;
|
||||
this.updateSettingsPriority = this.torrent.priority;
|
||||
this.updateSettingsDownloadRetryAttempts = this.torrent.downloadRetryAttempts;
|
||||
|
|
@ -193,6 +195,7 @@ export class TorrentComponent implements OnInit {
|
|||
public updateSettingsOk(): void {
|
||||
this.updating = true;
|
||||
|
||||
this.torrent.hostDownloadAction = this.updateSettingsHostDownloadAction;
|
||||
this.torrent.category = this.updateSettingsCategory;
|
||||
this.torrent.priority = this.updateSettingsPriority;
|
||||
this.torrent.downloadRetryAttempts = this.updateSettingsDownloadRetryAttempts;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@
|
|||
<base href="/" />
|
||||
<script>
|
||||
(function () {
|
||||
window["_app_base"] = "/" + window.location.pathname.split("/")[1];
|
||||
console.log("setting base href to " + window["_app_base"]);
|
||||
if (window.location.host !== 'localhost:4200') {
|
||||
var pathSegment = window.location.pathname.split("/")[1];
|
||||
window["_app_base"] = pathSegment ? "/" + pathSegment + "/" : "/";
|
||||
console.log("setting base href to " + window["_app_base"]);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ if ($SkipCache.IsPresent) { $dockerArgs += @("--no-cache" ) }
|
|||
& docker $dockerArgs
|
||||
|
||||
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) {
|
||||
docker exec -it rdtclientdev /bin/bash
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
The docker account to use to push the image
|
||||
|
||||
.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
|
||||
None. You cannot pipe objects to to this script.
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
param(
|
||||
[string]$Version = "",
|
||||
[string]$DockerAccount = "rogerfar",
|
||||
[string]$Platforms = "linux/arm/v7,linux/arm64/v8,linux/amd64",
|
||||
[string]$Platforms = "linux/arm64/v8,linux/amd64",
|
||||
[string]$Dockerfile = "Dockerfile",
|
||||
[switch]$SkipPush,
|
||||
[switch]$SkipCache,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "rdt-client",
|
||||
"version": "2.0.24",
|
||||
"version": "2.0.36",
|
||||
"description": "This is a web interface to manage your torrents on Real-Debrid.",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ public class TorrentData
|
|||
return;
|
||||
}
|
||||
|
||||
dbTorrent.HostDownloadAction = torrent.HostDownloadAction;
|
||||
dbTorrent.Category = torrent.Category;
|
||||
dbTorrent.Priority = torrent.Priority;
|
||||
dbTorrent.DownloadRetryAttempts = torrent.DownloadRetryAttempts;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,6 @@ public class UserData
|
|||
|
||||
public async Task<IdentityUser?> GetUser()
|
||||
{
|
||||
return await _dataContext.Users.FirstOrDefaultAsync();
|
||||
return await _dataContext.Users.OrderBy(m => m.Id).FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ public class AppSettings
|
|||
public AppSettingsDatabase? Database { get; set; }
|
||||
|
||||
public Int32 Port { get; set; }
|
||||
public String? BasePath { get; set; }
|
||||
}
|
||||
|
||||
public class AppSettingsLogging
|
||||
|
|
|
|||
|
|
@ -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.")]
|
||||
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)")]
|
||||
[Description("Timeout in milliseconds before the downloader times out.")]
|
||||
public Int32 Timeout { get; set; } = 5000;
|
||||
|
|
@ -120,7 +116,7 @@ http://127.0.0.1:6800/jsonrpc.")]
|
|||
public class DbSettingsProvider
|
||||
{
|
||||
[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://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>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.14" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="6.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.14">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.21" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.21" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.21" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="6.0.21" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.21">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ public static class DownloadHelper
|
|||
return null;
|
||||
}
|
||||
|
||||
var directory = RemoveInvalidPathChars(torrent.RdName);
|
||||
|
||||
var uri = new Uri(fileUrl);
|
||||
var torrentPath = Path.Combine(downloadPath, torrent.RdName);
|
||||
var torrentPath = Path.Combine(downloadPath, directory);
|
||||
|
||||
if (!Directory.Exists(torrentPath))
|
||||
{
|
||||
|
|
@ -26,8 +28,20 @@ public static class DownloadHelper
|
|||
|
||||
fileName = HttpUtility.UrlDecode(fileName);
|
||||
|
||||
fileName = RemoveInvalidFileNameChars(fileName);
|
||||
|
||||
var filePath = Path.Combine(torrentPath, fileName);
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -16,9 +16,9 @@ public static class Logger
|
|||
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)
|
||||
|
|
|
|||
55
server/RdtClient.Service/Middleware/BaseHrefMiddleware.cs
Normal file
55
server/RdtClient.Service/Middleware/BaseHrefMiddleware.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,13 +11,13 @@
|
|||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="AllDebrid.NET" Version="1.0.12" />
|
||||
<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="Premiumize.NET" Version="1.0.1" />
|
||||
<PackageReference Include="Premiumize.NET" Version="1.0.4" />
|
||||
<PackageReference Include="RD.NET" Version="2.1.4" />
|
||||
<PackageReference Include="Serilog" Version="2.12.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>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public class DownloadClient
|
|||
|
||||
public IDownloader? Downloader;
|
||||
|
||||
public Data.Enums.DownloadClient Type { get; set; }
|
||||
public Data.Enums.DownloadClient Type { get; private set; }
|
||||
|
||||
public Boolean Finished { get; private set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Net;
|
||||
using Downloader;
|
||||
using DownloaderNET;
|
||||
using Serilog;
|
||||
|
||||
namespace RdtClient.Service.Services.Downloaders;
|
||||
|
|
@ -9,14 +8,16 @@ public class InternalDownloader : IDownloader
|
|||
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
|
||||
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
|
||||
|
||||
private readonly DownloadService _downloadService;
|
||||
private readonly DownloadConfiguration _downloadConfiguration;
|
||||
private readonly Downloader _downloadService;
|
||||
private readonly DownloaderNET.Settings _downloadConfiguration;
|
||||
|
||||
private readonly String _filePath;
|
||||
private readonly String _uri;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly CancellationTokenSource _cancellationToken = new();
|
||||
|
||||
private Boolean _finished;
|
||||
|
||||
public InternalDownloader(String uri, String filePath)
|
||||
|
|
@ -25,40 +26,14 @@ public class InternalDownloader : IDownloader
|
|||
|
||||
_uri = uri;
|
||||
_filePath = filePath;
|
||||
|
||||
var settingProxyServer = Settings.Get.DownloadClient.ProxyServer;
|
||||
|
||||
// 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
|
||||
}
|
||||
};
|
||||
|
||||
_downloadConfiguration = new DownloaderNET.Settings();
|
||||
|
||||
SetSettings();
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(settingProxyServer))
|
||||
{
|
||||
_downloadConfiguration.RequestConfiguration.Proxy = new WebProxy(new Uri(settingProxyServer), false);
|
||||
}
|
||||
_downloadService = new Downloader(_uri, _filePath, _downloadConfiguration, _cancellationToken.Token);
|
||||
|
||||
_downloadService = new DownloadService(_downloadConfiguration);
|
||||
|
||||
_downloadService.DownloadProgressChanged += (_, args) =>
|
||||
_downloadService.OnProgress += args =>
|
||||
{
|
||||
if (DownloadProgress == null)
|
||||
{
|
||||
|
|
@ -68,32 +43,23 @@ public class InternalDownloader : IDownloader
|
|||
DownloadProgress.Invoke(this,
|
||||
new DownloadProgressEventArgs
|
||||
{
|
||||
Speed = (Int64)args.BytesPerSecondSpeed,
|
||||
BytesDone = args.ReceivedBytesSize,
|
||||
BytesTotal = args.TotalBytesToReceive
|
||||
Speed = (Int64) args.Sum(m => m.Speed),
|
||||
BytesDone = args.Sum(m => m.DownloadBytes),
|
||||
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,
|
||||
new DownloadCompleteEventArgs
|
||||
{
|
||||
Error = error
|
||||
Error = error?.Message
|
||||
});
|
||||
|
||||
_finished = true;
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -101,11 +67,7 @@ public class InternalDownloader : IDownloader
|
|||
{
|
||||
_logger.Debug($"Starting download of {_uri}, writing to path: {_filePath}");
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await _downloadService.DownloadFileTaskAsync(_uri, _filePath);
|
||||
});
|
||||
|
||||
Task.Run(_downloadService.Download);
|
||||
Task.Run(StartTimer);
|
||||
|
||||
return Task.FromResult<String?>(null);
|
||||
|
|
@ -115,7 +77,7 @@ public class InternalDownloader : IDownloader
|
|||
{
|
||||
_logger.Debug($"Cancelling download {_uri}");
|
||||
|
||||
_downloadService.CancelAsync();
|
||||
_cancellationToken.Cancel(false);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
|
@ -138,14 +100,7 @@ public class InternalDownloader : IDownloader
|
|||
{
|
||||
settingDownloadParallelCount = 1;
|
||||
}
|
||||
|
||||
var settingDownloadChunkCount = Settings.Get.DownloadClient.ChunkCount;
|
||||
|
||||
if (settingDownloadChunkCount <= 0)
|
||||
{
|
||||
settingDownloadChunkCount = 1;
|
||||
}
|
||||
|
||||
|
||||
var settingDownloadMaxSpeed = Settings.Get.DownloadClient.MaxSpeed;
|
||||
|
||||
if (settingDownloadMaxSpeed <= 0)
|
||||
|
|
@ -162,10 +117,8 @@ public class InternalDownloader : IDownloader
|
|||
settingDownloadTimeout = 1000;
|
||||
}
|
||||
|
||||
_downloadConfiguration.ChunkCount = settingDownloadChunkCount;
|
||||
_downloadConfiguration.ChunkCount = settingDownloadParallelCount;
|
||||
_downloadConfiguration.MaximumBytesPerSecond = settingDownloadMaxSpeed;
|
||||
_downloadConfiguration.ParallelDownload = settingDownloadChunkCount > 1;
|
||||
_downloadConfiguration.ParallelCount = settingDownloadParallelCount;
|
||||
_downloadConfiguration.Timeout = settingDownloadTimeout;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -228,29 +228,47 @@ public class PremiumizeTorrentClient : ITorrentClient
|
|||
}
|
||||
|
||||
var transfers = await GetClient().Transfers.ListAsync();
|
||||
|
||||
Log($"Found {transfers.Count} transfers", torrent);
|
||||
|
||||
var transfer = transfers.FirstOrDefault(m => m.Id == torrent.RdId);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(transfer.FolderId))
|
||||
foreach (var link in downloadLinks)
|
||||
{
|
||||
return null;
|
||||
Log($"Found {link}", torrent);
|
||||
}
|
||||
|
||||
var zip = await GetClient()
|
||||
.Zip.Generate(new List<String>(),
|
||||
new List<String>
|
||||
{
|
||||
transfer.FolderId
|
||||
});
|
||||
|
||||
return new List<String>
|
||||
{
|
||||
zip
|
||||
};
|
||||
return downloadLinks;
|
||||
}
|
||||
|
||||
private async Task<TorrentClientTorrent> GetInfo(String id)
|
||||
|
|
@ -266,6 +284,44 @@ public class PremiumizeTorrentClient : ITorrentClient
|
|||
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)
|
||||
{
|
||||
if (torrent != null)
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ public class TorrentRunner
|
|||
if (!String.IsNullOrWhiteSpace(downloadClient.Error))
|
||||
{
|
||||
// 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);
|
||||
|
||||
if (download.RetryCount < download.Torrent.DownloadRetryAttempts)
|
||||
|
|
@ -512,9 +512,19 @@ public class TorrentRunner
|
|||
if ((torrent.Downloads.Count > 0) ||
|
||||
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);
|
||||
|
||||
|
|
@ -558,7 +568,7 @@ public class TorrentRunner
|
|||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,24 @@ public class Torrents
|
|||
private readonly ILogger<Torrents> _logger;
|
||||
private readonly TorrentData _torrentData;
|
||||
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);
|
||||
|
||||
|
|
@ -38,14 +54,9 @@ public class Torrents
|
|||
_logger = logger;
|
||||
_torrentData = torrentData;
|
||||
_downloads = downloads;
|
||||
|
||||
_torrentClient = Settings.Get.Provider.Provider switch
|
||||
{
|
||||
Provider.Premiumize => premiumizeTorrentClient,
|
||||
Provider.RealDebrid => realDebridTorrentClient,
|
||||
Provider.AllDebrid => allDebridTorrentClient,
|
||||
_ => throw new Exception("Invalid Provider")
|
||||
};
|
||||
_allDebridTorrentClient = allDebridTorrentClient;
|
||||
_premiumizeTorrentClient = premiumizeTorrentClient;
|
||||
_realDebridTorrentClient = realDebridTorrentClient;
|
||||
}
|
||||
|
||||
public async Task<IList<Torrent>> Get()
|
||||
|
|
@ -114,7 +125,7 @@ public class Torrents
|
|||
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();
|
||||
|
||||
|
|
@ -140,7 +151,7 @@ public class Torrents
|
|||
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();
|
||||
|
||||
|
|
@ -153,7 +164,7 @@ public class Torrents
|
|||
|
||||
public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
|
||||
{
|
||||
var result = await _torrentClient.GetAvailableFiles(hash);
|
||||
var result = await TorrentClient.GetAvailableFiles(hash);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -167,7 +178,7 @@ public class Torrents
|
|||
return;
|
||||
}
|
||||
|
||||
await _torrentClient.SelectFiles(torrent);
|
||||
await TorrentClient.SelectFiles(torrent);
|
||||
}
|
||||
|
||||
public async Task CreateDownloads(Guid torrentId)
|
||||
|
|
@ -179,7 +190,7 @@ public class Torrents
|
|||
return;
|
||||
}
|
||||
|
||||
var downloadLinks = await _torrentClient.GetDownloadLinks(torrent);
|
||||
var downloadLinks = await TorrentClient.GetDownloadLinks(torrent);
|
||||
|
||||
if (downloadLinks == null)
|
||||
{
|
||||
|
|
@ -264,7 +275,7 @@ public class Torrents
|
|||
|
||||
try
|
||||
{
|
||||
await _torrentClient.Delete(torrent.RdId);
|
||||
await TorrentClient.Delete(torrent.RdId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -318,7 +329,7 @@ public class Torrents
|
|||
|
||||
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);
|
||||
|
||||
|
|
@ -327,7 +338,7 @@ public class Torrents
|
|||
|
||||
public async Task<Profile> GetProfile()
|
||||
{
|
||||
var user = await _torrentClient.GetUser();
|
||||
var user = await TorrentClient.GetUser();
|
||||
|
||||
var profile = new Profile
|
||||
{
|
||||
|
|
@ -349,7 +360,7 @@ public class Torrents
|
|||
|
||||
try
|
||||
{
|
||||
var rdTorrents = await _torrentClient.GetTorrents();
|
||||
var rdTorrents = await TorrentClient.GetTorrents();
|
||||
|
||||
foreach (var rdTorrent in rdTorrents)
|
||||
{
|
||||
|
|
@ -728,7 +739,7 @@ public class Torrents
|
|||
ReferenceHandler = ReferenceHandler.IgnoreCycles
|
||||
});
|
||||
|
||||
await _torrentClient.UpdateData(torrent, torrentClientTorrent);
|
||||
await TorrentClient.UpdateData(torrent, torrentClientTorrent);
|
||||
|
||||
var newTorrent = JsonSerializer.Serialize(torrent,
|
||||
new JsonSerializerOptions
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class AuthController : Controller
|
|||
return StatusCode(402, "Setup required");
|
||||
}
|
||||
|
||||
return Unauthorized();
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
|
|
|
|||
|
|
@ -164,6 +164,13 @@ try
|
|||
}
|
||||
});
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(appSettings.BasePath))
|
||||
{
|
||||
app.UseMiddleware<BaseHrefMiddleware>();
|
||||
}
|
||||
|
||||
app.UseMiddleware<RequestLoggingMiddleware>();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
},
|
||||
"RdtClient.Web": {
|
||||
"commandName": "Project",
|
||||
"launchUrl": "weatherforecast",
|
||||
"launchUrl": "",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<TargetFramework>net6.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId>
|
||||
<Version>2.0.24</Version>
|
||||
<Version>2.0.36</Version>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
|
@ -29,15 +29,15 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="6.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.14">
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.21" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.21" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="6.0.21" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.21">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<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.AspNetCore" Version="6.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
|
|
|
|||
|
|
@ -9,5 +9,6 @@
|
|||
"Database": {
|
||||
"Path": "/data/db/rdtclient.db"
|
||||
},
|
||||
"Port": "6500"
|
||||
"Port": "6500",
|
||||
"BasePath": null
|
||||
}
|
||||
Loading…
Reference in a new issue