Merge remote-tracking branch 'origin/main' into fork/Cucumberrbob/refactor/get-download-info

# Conflicts:
#	server/RdtClient.Service.Test/Services/TorrentClients/AllDebridTorrentClientTest.cs
#	server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs
#	server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs
This commit is contained in:
Cucumberrbob 2025-03-23 13:30:05 +00:00
commit 5a1b96b909
No known key found for this signature in database
GPG key ID: 2B935C47401C3614
53 changed files with 2056 additions and 12891 deletions

140
.github/workflows/build-release.yaml vendored Normal file
View file

@ -0,0 +1,140 @@
name: Release
on:
push:
branches: [main]
permissions:
id-token: write
contents: write
packages: write
jobs:
test:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '9'
- name: Test Backend
working-directory: server
run: dotnet test
version:
name: Increment version
needs: test
runs-on: ubuntu-latest
outputs:
version: ${{ steps.increment-version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Increment latest version tag
id: increment-version
run: |
# Retry mechanism
$maxRetries = 5
$retryDelay = 5
$attempt = 0
$success = $false
while ($attempt -lt $maxRetries -and -not $success) {
try {
git fetch --tags
$tags = git tag --sort=-v:refname | Where-Object { $_ -match '^v\d+\.\d+\.\d+$' }
$latestTag = $tags -split '\n' | Select-Object -First 1
$versionNumbers = $latestTag -split '\.'
$versionNumbers[-1] = [string]([int]$versionNumbers[-1] + 1)
$newVersion = $versionNumbers -join '.'
Write-Output "New incremented version is $newVersion"
echo "version=$newVersion" | Out-File -Append -FilePath $Env:GITHUB_OUTPUT
git tag $newVersion
git push origin $newVersion
$success = $true
Write-Output "Tag $newVersion pushed successfully!"
} catch {
Write-Output "Failed to push tag $newVersion. Attempt $($attempt + 1) of $maxRetries."
Write-Output "Error details: $($_.Exception.Message)"
$attempt++
if ($attempt -lt $maxRetries) {
Write-Output "Retrying in $retryDelay seconds..."
Start-Sleep -Seconds $retryDelay
} else {
Write-Error "Failed to push tag $newVersion after $maxRetries attempts. Exiting."
exit 1
}
}
}
shell: pwsh
release:
name: Create GitHub release
needs: [test, version]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate changelog
id: changelog
uses: mikepenz/release-changelog-builder-action@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
- name: Set up .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '9'
- name: Build Frontend
working-directory: client
run: |
npm ci
npm run build
- name: Build and Publish Backend
working-directory: server
shell: pwsh
run: |
$v = "${{ needs.version.outputs.version }}".TrimStart('v')
dotnet restore
dotnet build -c Release --no-restore -p:Version=$v -p:AssemblyVersion=$v
dotnet publish RdtClient.Web/RdtClient.Web.csproj -c Release --no-build -p:Version=$v -p:AssemblyVersion=$v -o ../publish
- name: Create ZIP
run: |
cd publish
zip -r ../RealDebridClient.zip .
cd ..
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.version.outputs.version }}
name: ${{ needs.version.outputs.version }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
files: RealDebridClient.zip
token: ${{ secrets.GITHUB_TOKEN }}

14
client/.ncurc.js Normal file
View file

@ -0,0 +1,14 @@
const minorOnly = [];
const patchOnly = [];
module.exports = {
target: (name) => {
if (minorOnly.indexOf(name) > -1) {
return 'minor';
}
if (patchOnly.indexOf(name) > -1) {
return 'patch';
}
return 'latest';
}
};

View file

@ -57,8 +57,8 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "500kb", "maximumWarning": "2mb",
"maximumError": "1mb" "maximumError": "2mb"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",

9733
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,48 +6,48 @@
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"update": "ng update --force --allow-dirty @angular/cli @angular/core @angular/cdk @angular/flex-layout", "update": "ng update --force --allow-dirty @angular/cli @angular/core @angular/cdk",
"lint": "ng lint", "lint": "ng lint",
"prettier": "prettier --write \"./**/*.{ts,html,json}\"" "prettier": "prettier --write \"./**/*.{ts,html,json}\""
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^19.2.0", "@angular/animations": "^19.2.3",
"@angular/cdk": "^19.2.1", "@angular/cdk": "^19.2.6",
"@angular/common": "^19.2.0", "@angular/common": "^19.2.3",
"@angular/compiler": "^19.2.0", "@angular/compiler": "^19.2.3",
"@angular/core": "^19.2.0", "@angular/core": "^19.2.3",
"@angular/forms": "^19.2.0", "@angular/forms": "^19.2.3",
"@angular/platform-browser": "^19.2.0", "@angular/platform-browser": "^19.2.3",
"@angular/platform-browser-dynamic": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.3",
"@angular/router": "^19.2.0", "@angular/router": "^19.2.3",
"@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@microsoft/signalr": "^6.0.21", "@microsoft/signalr": "^8.0.7",
"bulma": "^0.9.4", "bulma": "^1.0.3",
"curray": "^1.0.11", "curray": "^1.0.12",
"file-saver-es": "^2.0.5", "file-saver-es": "^2.0.5",
"ngx-filesize": "^3.0.5", "filesize": "^10.1.6",
"rxjs": "~7.8.1", "rxjs": "^7.8.2",
"tslib": "^2.6.2", "tslib": "^2.8.1",
"zone.js": "~0.15.0" "zone.js": "^0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-eslint/builder": "19.1.0", "@angular-eslint/builder": "19.2.1",
"@angular-eslint/eslint-plugin": "19.1.0", "@angular-eslint/eslint-plugin": "19.2.1",
"@angular-eslint/eslint-plugin-template": "19.1.0", "@angular-eslint/eslint-plugin-template": "19.2.1",
"@angular-eslint/schematics": "19.1.0", "@angular-eslint/schematics": "19.2.1",
"@angular-eslint/template-parser": "19.1.0", "@angular-eslint/template-parser": "19.2.1",
"@angular/build": "^19.2.0", "@angular/build": "^19.2.4",
"@angular/cli": "^19.2.0", "@angular/cli": "^19.2.4",
"@angular/compiler-cli": "^19.2.0", "@angular/compiler-cli": "^19.2.3",
"@angular/language-service": "^19.2.0", "@angular/language-service": "^19.2.3",
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.7",
"@types/file-saver-es": "^2.0.1", "@types/file-saver-es": "^2.0.3",
"@types/node": "^20.5.8", "@types/node": "^22.13.11",
"@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^7.2.0", "@typescript-eslint/parser": "^8.27.0",
"eslint": "^8.57.0", "eslint": "^9.23.0",
"prettier": "^3.0.3", "prettier": "^3.5.3",
"typescript": "5.7.3" "typescript": "5.8.2"
} }
} }

View file

@ -5,10 +5,10 @@ import { Torrent, TorrentFileAvailability } from '../models/torrent.model';
import { SettingsService } from '../settings.service'; import { SettingsService } from '../settings.service';
@Component({ @Component({
selector: 'app-add-new-torrent', selector: 'app-add-new-torrent',
templateUrl: './add-new-torrent.component.html', templateUrl: './add-new-torrent.component.html',
styleUrls: ['./add-new-torrent.component.scss'], styleUrls: ['./add-new-torrent.component.scss'],
standalone: false standalone: false,
}) })
export class AddNewTorrentComponent implements OnInit { export class AddNewTorrentComponent implements OnInit {
public fileName: string; public fileName: string;

View file

@ -1,9 +1,9 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
template: '<router-outlet></router-outlet>', template: '<router-outlet></router-outlet>',
styles: [], styles: [],
standalone: false standalone: false,
}) })
export class AppComponent {} export class AppComponent {}

View file

@ -1,11 +1,10 @@
import 'curray';
import { ClipboardModule } from '@angular/cdk/clipboard'; import { ClipboardModule } from '@angular/cdk/clipboard';
import { APP_BASE_HREF } from '@angular/common'; import { APP_BASE_HREF } from '@angular/common';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { curray } from 'curray';
import { FileSizePipe, NgxFilesizeModule } from 'ngx-filesize';
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component'; import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@ -23,8 +22,7 @@ import { TorrentStatusPipe } from './torrent-status.pipe';
import { TorrentTableComponent } from './torrent-table/torrent-table.component'; import { TorrentTableComponent } from './torrent-table/torrent-table.component';
import { TorrentComponent } from './torrent/torrent.component'; import { TorrentComponent } from './torrent/torrent.component';
import { SortPipe } from './sort.pipe'; import { SortPipe } from './sort.pipe';
import { FileSizePipe } from './filesize.pipe';
curray();
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -43,9 +41,10 @@ curray();
ProfileComponent, ProfileComponent,
Nl2BrPipe, Nl2BrPipe,
SortPipe, SortPipe,
FileSizePipe,
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
imports: [BrowserModule, AppRoutingModule, FormsModule, NgxFilesizeModule, ClipboardModule], imports: [BrowserModule, AppRoutingModule, FormsModule, ClipboardModule],
providers: [ providers: [
FileSizePipe, FileSizePipe,
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },

View file

@ -1,8 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ @Pipe({
name: 'decodeURI', name: 'decodeURI',
standalone: false standalone: false,
}) })
export class DecodeURIPipe implements PipeTransform { export class DecodeURIPipe implements PipeTransform {
transform(input: any) { transform(input: any) {

View file

@ -1,10 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { FileSizePipe } from 'ngx-filesize';
import { Download } from './models/download.model'; import { Download } from './models/download.model';
import { FileSizePipe } from './filesize.pipe';
@Pipe({ @Pipe({
name: 'downloadStatus', name: 'downloadStatus',
standalone: false standalone: false,
}) })
export class DownloadStatusPipe implements PipeTransform { export class DownloadStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {} constructor(private pipe: FileSizePipe) {}

View file

@ -0,0 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core';
import { filesize } from 'filesize';
@Pipe({
name: 'filesize',
standalone: false,
})
export class FileSizePipe implements PipeTransform {
private static transformOne(value: number, options?: any): string {
return filesize(value, options).toString();
}
transform(value: number | number[], options?: any) {
if (Array.isArray(value)) {
return value.map((val) => FileSizePipe.transformOne(val, options));
}
return FileSizePipe.transformOne(value, options);
}
}

View file

@ -3,10 +3,10 @@ import { Router } from '@angular/router';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'], styleUrls: ['./login.component.scss'],
standalone: false standalone: false,
}) })
export class LoginComponent { export class LoginComponent {
public userName: string; public userName: string;

View file

@ -1,10 +1,10 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@Component({ @Component({
selector: 'app-main-layout', selector: 'app-main-layout',
templateUrl: './main-layout.component.html', templateUrl: './main-layout.component.html',
styleUrls: ['./main-layout.component.scss'], styleUrls: ['./main-layout.component.scss'],
standalone: false standalone: false,
}) })
export class MainLayoutComponent { export class MainLayoutComponent {
constructor() {} constructor() {}

View file

@ -4,4 +4,6 @@ export class Profile {
public expiration: Date; public expiration: Date;
public currentVersion: string; public currentVersion: string;
public latestVersion: string; public latestVersion: string;
public isInsecure: boolean;
public disableUpdateNotification: boolean;
} }

View file

@ -0,0 +1,3 @@
export class Version {
public version: string;
}

View file

@ -55,16 +55,25 @@
<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" />
<a href="https://github.com/rogerfar/rdt-client" target="_blank" class="navbar-item">Version 2.0.102</a> <a href="https://github.com/rogerfar/rdt-client" target="_blank" class="navbar-item">Version {{ version }}</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</nav> </nav>
<div <div
class="notification is-warning" class="notification"
*ngIf="profile && profile.latestVersion && profile.currentVersion !== profile.latestVersion" [ngClass]="profile.isInsecure ? 'is-danger' : 'is-warning'"
*ngIf="
profile &&
(!profile.disableUpdateNotification || profile.isInsecure) &&
profile.latestVersion &&
profile.currentVersion !== profile.latestVersion
"
> >
Version {{ profile.latestVersion }} of RealDebrid Client was found. You are currently on version <span *ngIf="profile.isInsecure"> Your current version is insecure. </span>
{{ profile.currentVersion }}. <span>
Version {{ profile.latestVersion }} of RealDebrid Client was found. You are currently on version
{{ profile.currentVersion }}.
</span>
</div> </div>

View file

@ -1,26 +1,33 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
import { Profile } from '../models/profile.model'; import { Profile } from '../models/profile.model';
import { SettingsService } from '../settings.service'; import { SettingsService } from '../settings.service';
@Component({ @Component({
selector: 'app-navbar', selector: 'app-navbar',
templateUrl: './navbar.component.html', templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.scss'], styleUrls: ['./navbar.component.scss'],
standalone: false standalone: false,
}) })
export class NavbarComponent implements OnInit { export class NavbarComponent implements OnInit {
public showMobileMenu = false; public showMobileMenu = false;
public profile: Profile; public profile: Profile;
public providerLink: string; public providerLink: string;
public version: string;
constructor( constructor(
private settingsService: SettingsService, private settingsService: SettingsService,
private authService: AuthService, private authService: AuthService,
private router: Router, private router: Router,
) {} ) {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.showMobileMenu = false;
}
});
}
ngOnInit(): void { ngOnInit(): void {
this.settingsService.getProfile().subscribe((result) => { this.settingsService.getProfile().subscribe((result) => {
@ -44,6 +51,10 @@ export class NavbarComponent implements OnInit {
break; break;
} }
}); });
this.settingsService.getVersion().subscribe((result) => {
this.version = result.version;
});
} }
public logout(): void { public logout(): void {

View file

@ -2,8 +2,8 @@ import { Pipe, PipeTransform, SecurityContext, VERSION } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
@Pipe({ @Pipe({
name: 'nl2br', name: 'nl2br',
standalone: false standalone: false,
}) })
export class Nl2BrPipe implements PipeTransform { export class Nl2BrPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {} constructor(private sanitizer: DomSanitizer) {}

View file

@ -2,10 +2,10 @@ import { Component } from '@angular/core';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
@Component({ @Component({
selector: 'app-profile', selector: 'app-profile',
templateUrl: './profile.component.html', templateUrl: './profile.component.html',
styleUrls: ['./profile.component.scss'], styleUrls: ['./profile.component.scss'],
standalone: false standalone: false,
}) })
export class ProfileComponent { export class ProfileComponent {
constructor(private authService: AuthService) {} constructor(private authService: AuthService) {}

View file

@ -4,6 +4,7 @@ import { Observable } from 'rxjs';
import { Profile } from './models/profile.model'; import { Profile } from './models/profile.model';
import { Setting } from './models/setting.model'; import { Setting } from './models/setting.model';
import { APP_BASE_HREF } from '@angular/common'; import { APP_BASE_HREF } from '@angular/common';
import { Version } from './models/version.model';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -26,6 +27,10 @@ export class SettingsService {
return this.http.get<Profile>(`${this.baseHref}Api/Settings/Profile`); return this.http.get<Profile>(`${this.baseHref}Api/Settings/Profile`);
} }
public getVersion(): Observable<Version> {
return this.http.get<Version>(`${this.baseHref}Api/Settings/Version`);
}
public testPath(path: string): Observable<void> { public testPath(path: string): Observable<void> {
return this.http.post<void>(`${this.baseHref}Api/Settings/TestPath`, { path }); return this.http.post<void>(`${this.baseHref}Api/Settings/TestPath`, { path });
} }

View file

@ -3,10 +3,10 @@ import { SettingsService } from 'src/app/settings.service';
import { Setting } from '../models/setting.model'; import { Setting } from '../models/setting.model';
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'], styleUrls: ['./settings.component.scss'],
standalone: false standalone: false,
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
public activeTab = 0; public activeTab = 0;

View file

@ -3,10 +3,10 @@ import { Router } from '@angular/router';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
@Component({ @Component({
selector: 'app-setup', selector: 'app-setup',
templateUrl: './setup.component.html', templateUrl: './setup.component.html',
styleUrls: ['./setup.component.scss'], styleUrls: ['./setup.component.scss'],
standalone: false standalone: false,
}) })
export class SetupComponent { export class SetupComponent {
public userName: string; public userName: string;

View file

@ -1,8 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ @Pipe({
name: 'sort', name: 'sort',
standalone: false standalone: false,
}) })
export class SortPipe implements PipeTransform { export class SortPipe implements PipeTransform {
transform(array: any[], field: string, order: 'asc' | 'desc' = 'asc'): any[] { transform(array: any[], field: string, order: 'asc' | 'desc' = 'asc'): any[] {

View file

@ -1,10 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { FileSizePipe } from 'ngx-filesize';
import { RealDebridStatus, Torrent } from './models/torrent.model'; import { RealDebridStatus, Torrent } from './models/torrent.model';
import { FileSizePipe } from './filesize.pipe';
@Pipe({ @Pipe({
name: 'status', name: 'status',
standalone: false standalone: false,
}) })
export class TorrentStatusPipe implements PipeTransform { export class TorrentStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {} constructor(private pipe: FileSizePipe) {}

View file

@ -33,7 +33,7 @@
[checked]="selectedTorrents.contains(torrent.torrentId)" [checked]="selectedTorrents.contains(torrent.torrentId)"
/> />
</td> </td>
<td (click)="openTorrent(torrent.torrentId)"> <td (click)="openTorrent(torrent.torrentId)" class="break-all">
{{ torrent.rdName }} {{ torrent.rdName }}
</td> </td>
<td> <td>

View file

@ -1,6 +1,9 @@
table { table {
tr { tr {
cursor: pointer; cursor: pointer;
td.break-all {
word-break: break-all;
}
} }
} }

View file

@ -5,10 +5,10 @@ import { TorrentService } from '../torrent.service';
import { forkJoin, Observable } from 'rxjs'; import { forkJoin, Observable } from 'rxjs';
@Component({ @Component({
selector: 'app-torrent-table', selector: 'app-torrent-table',
templateUrl: './torrent-table.component.html', templateUrl: './torrent-table.component.html',
styleUrls: ['./torrent-table.component.scss'], styleUrls: ['./torrent-table.component.scss'],
standalone: false standalone: false,
}) })
export class TorrentTableComponent implements OnInit { export class TorrentTableComponent implements OnInit {
public torrents: Torrent[] = []; public torrents: Torrent[] = [];

View file

@ -252,7 +252,7 @@
<td style="width: 35px"></td> <td style="width: 35px"></td>
<td colspan="5"> <td colspan="5">
<div class="flex-container"> <div class="flex-container">
<div style="flex: 1 1 0;"> <div style="flex: 1 1 0">
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<button class="button is-primary" (click)="showDownloadRetryModal(download.downloadId)"> <button class="button is-primary" (click)="showDownloadRetryModal(download.downloadId)">
@ -285,7 +285,7 @@
{{ download.retryCount }} / {{ torrent.downloadRetryAttempts }} {{ download.retryCount }} / {{ torrent.downloadRetryAttempts }}
</div> </div>
</div> </div>
<div style="flex: 1 1 0;"> <div style="flex: 1 1 0">
<div class="field"> <div class="field">
<label class="label">Added</label> <label class="label">Added</label>
<ng-container *ngIf="download.added"> <ng-container *ngIf="download.added">

View file

@ -5,10 +5,10 @@ import { Torrent } from '../models/torrent.model';
import { TorrentService } from '../torrent.service'; import { TorrentService } from '../torrent.service';
@Component({ @Component({
selector: 'app-torrent', selector: 'app-torrent',
templateUrl: './torrent.component.html', templateUrl: './torrent.component.html',
styleUrls: ['./torrent.component.scss'], styleUrls: ['./torrent.component.scss'],
standalone: false standalone: false,
}) })
export class TorrentComponent implements OnInit { export class TorrentComponent implements OnInit {
public torrent: Torrent; public torrent: Torrent;

View file

@ -1,2 +1,2 @@
@forward "../node_modules/bulma/bulma.sass"; @forward "../node_modules/bulma/bulma.scss";
@forward "../node_modules/@fortawesome/fontawesome-free/css/all.css"; @forward "../node_modules/@fortawesome/fontawesome-free/css/all.css";

4611
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
{
"name": "rdt-client",
"version": "2.0.102",
"description": "This is a web interface to manage your torrents on Real-Debrid.",
"main": "index.js",
"dependencies": {
"gh-release": "^7.0.2"
},
"devDependencies": {},
"repository": {
"type": "git",
"url": "git+https://github.com/rogerfar/rdt-client.git"
},
"author": "Roger Far",
"license": "MIT",
"bugs": {
"url": "https://github.com/rogerfar/rdt-client/issues"
},
"homepage": "https://github.com/rogerfar/rdt-client#readme"
}

View file

@ -74,6 +74,10 @@ Supports the following parameters:
[DisplayName("Copy added torrent files")] [DisplayName("Copy added torrent files")]
[Description("When a torrent file or magnet is added, create a copy in this directory.")] [Description("When a torrent file or magnet is added, create a copy in this directory.")]
public String? CopyAddedTorrents { get; set; } = null; public String? CopyAddedTorrents { get; set; } = null;
[DisplayName("Disable update notifications")]
[Description("Ignore update notifications. You will still be notified if the version you are running has a security vulnerability.")]
public Boolean DisableUpdateNotifications { get; set; } = false;
} }
public class DbSettingsDownloadClient public class DbSettingsDownloadClient
@ -111,8 +115,8 @@ public class DbSettingsDownloadClient
[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;
[DisplayName("Proxy Server (only used for the Internal Downloader)")] [DisplayName("Proxy Server (only used for the Bezzad Downloader)")]
[Description("Address of a proxy server to download through (only used for the Internal Downloader).")] [Description("Address of a proxy server to download through (only used for the Bezzad Downloader).")]
public String? ProxyServer { get; set; } = null; public String? ProxyServer { get; set; } = null;
[DisplayName("Aria2c URL (only used for the Aria2c Downloader)")] [DisplayName("Aria2c URL (only used for the Aria2c Downloader)")]

View file

@ -7,4 +7,6 @@ public class Profile
public DateTimeOffset? Expiration { get; set; } public DateTimeOffset? Expiration { get; set; }
public String? CurrentVersion { get; set; } public String? CurrentVersion { get; set; }
public String? LatestVersion { get; set; } public String? LatestVersion { get; set; }
public Boolean? IsInsecure { get; set; }
public Boolean? DisableUpdateNotification { get; set; }
} }

View file

@ -8,11 +8,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.2" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.3">
<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

@ -10,16 +10,16 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AllDebrid.NET" Version="1.0.17" /> <PackageReference Include="AllDebrid.NET" Version="1.0.18" />
<PackageReference Include="coverlet.collector" Version="6.0.4"> <PackageReference Include="coverlet.collector" Version="6.0.4">
<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.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.11" /> <PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.12" />
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.0.11" /> <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.0.12" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.11" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.12" />
<PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2"> <PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View file

@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using RdtClient.Data.Enums;
using RdtClient.Service.Services; using RdtClient.Service.Services;
namespace RdtClient.Service.BackgroundServices; namespace RdtClient.Service.BackgroundServices;
@ -26,8 +27,8 @@ public class ProviderUpdater(ILogger<TaskRunner> logger, IServiceProvider servic
try try
{ {
var torrents = await torrentService.Get(); var torrents = await torrentService.Get();
if (_nextUpdate < DateTime.UtcNow && ((torrents.Count > 0 && !Settings.Get.Provider.AutoImport) || Settings.Get.Provider.AutoImport)) if (_nextUpdate < DateTime.UtcNow && (Settings.Get.Provider.AutoImport || torrents.Any(t => t.RdStatus != TorrentStatus.Finished)))
{ {
logger.LogDebug($"Updating torrent info from debrid provider"); logger.LogDebug($"Updating torrent info from debrid provider");

View file

@ -9,6 +9,11 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
{ {
public static String? CurrentVersion { get; private set; } public static String? CurrentVersion { get; private set; }
public static String? LatestVersion { get; private set; } public static String? LatestVersion { get; private set; }
public static Boolean? IsInsecure { get; private set; }
private static readonly List<String> KnownGhsaIds = [
];
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
@ -34,18 +39,9 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
{ {
try try
{ {
var httpClient = new HttpClient(); var gitHubReleases = await GitHubRequest<List<GitHubReleasesResponse>>("/repos/rogerfar/rdt-client/tags?per_page=1", stoppingToken);
httpClient.DefaultRequestHeaders.UserAgent.Add(new("RdtClient", CurrentVersion));
var response = await httpClient.GetStringAsync($"https://api.github.com/repos/rogerfar/rdt-client/tags?per_page=1", stoppingToken);
var gitHubReleases = JsonConvert.DeserializeObject<List<GitHubReleasesResponse>>(response); var latestRelease = gitHubReleases?.FirstOrDefault(m => m.Name != null)?.Name;
if (gitHubReleases == null || gitHubReleases.Count == 0)
{
return;
}
var latestRelease = gitHubReleases.FirstOrDefault(m => m.Name != null)?.Name;
if (latestRelease == null) if (latestRelease == null)
{ {
@ -59,6 +55,18 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
} }
LatestVersion = latestRelease; LatestVersion = latestRelease;
var gitHubSecurityAdvisories = await GitHubRequest<List<GitHubSecurityAdvisoriesResponse>>("/repos/rogerfar/rdt-client/security-advisories", stoppingToken);
var unseenGhsaIds = gitHubSecurityAdvisories?.Where(advisory => !KnownGhsaIds.Contains(advisory.GhsaId));
if (unseenGhsaIds == null)
{
logger.LogWarning($"Unable to find security advisories on GitHub");
return;
}
IsInsecure = unseenGhsaIds.Any();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -70,10 +78,25 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
logger.LogInformation("UpdateChecker stopped."); logger.LogInformation("UpdateChecker stopped.");
} }
private async Task<T?> GitHubRequest<T>(String endpoint, CancellationToken cancellationToken)
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.UserAgent.Add(new("RdtClient", CurrentVersion));
var response = await httpClient.GetStringAsync($"https://api.github.com{endpoint}", cancellationToken);
return JsonConvert.DeserializeObject<T>(response);
}
} }
public class GitHubReleasesResponse public class GitHubReleasesResponse
{ {
[JsonProperty("name")] [JsonProperty("name")]
public String? Name { get; set; } public String? Name { get; set; }
}
public class GitHubSecurityAdvisoriesResponse
{
[JsonProperty("ghsa_id")]
public required String GhsaId { get; set; }
} }

View file

@ -9,23 +9,23 @@
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="AllDebrid.NET" Version="1.0.17" /> <PackageReference Include="AllDebrid.NET" Version="1.0.18" />
<PackageReference Include="Aria2.NET" Version="1.0.5" /> <PackageReference Include="Aria2.NET" Version="1.0.6" />
<PackageReference Include="DebridLinkFr.NET" Version="1.0.4" /> <PackageReference Include="DebridLinkFr.NET" Version="1.0.4" />
<PackageReference Include="Downloader" Version="3.3.3" /> <PackageReference Include="Downloader" Version="3.3.4" />
<PackageReference Include="Downloader.NET" Version="1.0.14" /> <PackageReference Include="Downloader.NET" Version="1.0.15" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.2.0" /> <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.3.0" />
<PackageReference Include="MonoTorrent" Version="3.0.2" /> <PackageReference Include="MonoTorrent" Version="3.0.2" />
<PackageReference Include="Polly" Version="8.5.2" /> <PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="Premiumize.NET" Version="1.0.9" /> <PackageReference Include="Premiumize.NET" Version="1.0.10" />
<PackageReference Include="RD.NET" Version="2.1.7" /> <PackageReference Include="RD.NET" Version="2.1.11" />
<PackageReference Include="Serilog" Version="4.2.0" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SharpCompress" Version="0.39.0" /> <PackageReference Include="SharpCompress" Version="0.39.0" />
<PackageReference Include="Synology.Api.Client" Version="0.3.91" /> <PackageReference Include="Synology.Api.Client" Version="0.3.93" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.11" /> <PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.12" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.11" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.12" />
<PackageReference Include="TorBox.NET" Version="1.5.0" /> <PackageReference Include="TorBox.NET" Version="1.5.0" />
</ItemGroup> </ItemGroup>

View file

@ -147,7 +147,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
public async Task SelectFiles(Data.Models.Data.Torrent torrent) public async Task SelectFiles(Data.Models.Data.Torrent torrent)
{ {
IList<TorrentClientFile> files; List<TorrentClientFile> files;
Log("Seleting files", torrent); Log("Seleting files", torrent);
@ -158,32 +158,17 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
} }
else else
{ {
Log("Selecting all files", torrent); Log("Selecting files", torrent);
files = [.. torrent.Files]; files = [.. torrent.Files];
} }
Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList(); files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList();
if (files.Count == 0) Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
{
Log($"Filtered all files out! Downloading ALL files instead!", torrent);
files = torrent.Files;
}
var fileIds = files.Select(m => m.Id.ToString()).ToArray(); var fileIds = files.Select(m => m.Id.ToString()).ToArray();
Log($"Selecting files:");
foreach (var file in files)
{
Log($"{file.Id}: {file.Path} ({file.Bytes}b)");
}
Log("", torrent);
await GetClient().Torrents.SelectFilesAsync(torrent.RdId!, [.. fileIds]); await GetClient().Torrents.SelectFilesAsync(torrent.RdId!, [.. fileIds]);
} }

View file

@ -1,9 +1,11 @@
using System.Reflection;
using System.Diagnostics; using System.Diagnostics;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using TorBoxNET; using TorBoxNET;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.TorrentClient;
using System.Web;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
@ -24,7 +26,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
} }
var httpClient = httpClientFactory.CreateClient(); var httpClient = httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.Add("User-Agent", "rdt-client"); httpClient.DefaultRequestHeaders.Add("User-Agent", $"rdt-client {Assembly.GetEntryAssembly()?.GetName().Version}");
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout); httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var torBoxNetClient = new TorBoxNetClient(null, httpClient, 5); var torBoxNetClient = new TorBoxNetClient(null, httpClient, 5);

View file

@ -417,7 +417,9 @@ public class Torrents(
UserName = user.Username, UserName = user.Username,
Expiration = user.Expiration, Expiration = user.Expiration,
CurrentVersion = UpdateChecker.CurrentVersion, CurrentVersion = UpdateChecker.CurrentVersion,
LatestVersion = UpdateChecker.LatestVersion LatestVersion = UpdateChecker.LatestVersion,
IsInsecure = UpdateChecker.IsInsecure,
DisableUpdateNotification = Settings.Get.General.DisableUpdateNotifications
}; };
return profile; return profile;

View file

@ -67,7 +67,7 @@ public class AuthController(Authentication authentication, Settings settings) :
return Ok(); return Ok();
} }
[AllowAnonymous] [Authorize(Policy = "AuthSetting")]
[Route("SetupProvider")] [Route("SetupProvider")]
[HttpPost] [HttpPost]
public async Task<ActionResult> SetupProvider([FromBody] AuthControllerSetupProviderRequest? request) public async Task<ActionResult> SetupProvider([FromBody] AuthControllerSetupProviderRequest? request)
@ -82,13 +82,6 @@ public class AuthController(Authentication authentication, Settings settings) :
return StatusCode(401); return StatusCode(401);
} }
var user = await authentication.GetUser();
if (user != null)
{
return StatusCode(401);
}
await settings.Update("Provider:Provider", request.Provider); await settings.Update("Provider:Provider", request.Provider);
await settings.Update("Provider:ApiKey", request.Token); await settings.Update("Provider:ApiKey", request.Token);

View file

@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Reflection;
using Aria2NET; using Aria2NET;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -44,6 +45,18 @@ public class SettingsController(Settings settings, Torrents torrents) : Controll
var profile = await torrents.GetProfile(); var profile = await torrents.GetProfile();
return Ok(profile); return Ok(profile);
} }
[HttpGet]
[Route("Version")]
public ActionResult<Version> Version()
{
var version = Assembly.GetExecutingAssembly().GetName().Version!;
return Ok(new
{
Version = version
});
}
[HttpPost] [HttpPost]
[Route("TestPath")] [Route("TestPath")]

View file

@ -29,15 +29,15 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.2" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.3">
<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="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.3" />
<PackageReference Include="Serilog" Version="4.2.0" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />

0
docker-build-dev.ps1 → tools/docker-build-dev.ps1 Executable file → Normal file
View file

0
docker-build.ps1 → tools/docker-build.ps1 Executable file → Normal file
View file

0
docker-publish.ps1 → tools/docker-publish.ps1 Executable file → Normal file
View file

View file

@ -3,14 +3,10 @@ $utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
$version = (Get-Content "package.json" | ConvertFrom-Json).version $version = (Get-Content "package.json" | ConvertFrom-Json).version
$csProj = "$pwd\server\RdtClient.Web\RdtClient.Web.csproj" $csProj = "$pwd\server\RdtClient.Web\RdtClient.Web.csproj"
$navbar = "$pwd\client\src\app\navbar\navbar.component.html";
$newCsProj = (Get-Content $csProj) -replace '<Version>.*?<\/Version>', "<Version>$version</Version>" $newCsProj = (Get-Content $csProj) -replace '<Version>.*?<\/Version>', "<Version>$version</Version>"
[System.IO.File]::WriteAllLines($csProj, $newCsProj, $utf8NoBomEncoding) [System.IO.File]::WriteAllLines($csProj, $newCsProj, $utf8NoBomEncoding)
$newNavbar = (Get-Content $navbar) -replace 'Version .*?<', "Version $version<"
[System.IO.File]::WriteAllLines($navbar, $newNavbar, $utf8NoBomEncoding)
Write-Output "Commit and push now, press any key to continue" Write-Output "Commit and push now, press any key to continue"
[Console]::ReadKey() [Console]::ReadKey()