Merge branch 'main' into feat/github-action

This commit is contained in:
Cucumberrbob 2025-04-13 15:07:11 +01:00
commit 15d7e7b0cc
No known key found for this signature in database
GPG key ID: 2B935C47401C3614
73 changed files with 2444 additions and 13107 deletions

View file

@ -9,7 +9,7 @@ assignees: ''
**What version are you using?**
**Wat OS are you running?**
**What OS are you running?**
**Are you using Docker or as a service?**

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

@ -0,0 +1,141 @@
name: Release
on:
push:
tags:
- v*
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 }}

View file

@ -4,6 +4,26 @@ 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).
## [Unreleased]
## [v2.0.104] - 2025-04-12
### Fixed
- Update the version number
## [v2.0.103] - 2025-04-12
### Added
- Button to select all options when deleting a torrent, thanks @EugeneKallis
- Add setting to ignore update notifications. A notification will appear regardless of this setting if any GitHub Security Advisories are published in this repo.
### Changed
- Download .zip of torrent files from TorBox when possible, thanks @asylumexp
- Users of AllDebrid and RealDebrid will now have no files downloaded when all files are excluded by filters. Before, if all files were excluded, rdt-client would download all the files in the torrent.
- Reduce number of calls to debrid provider API when no torrents need updating
### Fixed
- The dropdown navigation menu on mobile will now close when you navigate to another page
- Long torrent names without spaces will now wrap across lines
### Security
- Require auth to change debrid api key
## [2.0.102] - 2025-03-07
### Changed
- Fixed Angular build for Docker.

View file

@ -157,7 +157,7 @@ Required configuration:
Suggested configuration:
- Automatic retry downloads > 3
### Synology Download Station
#### Synology Download Station
The Synology Download Station downloader uses an external Download Station server. You will need to set this up yourself.
@ -220,3 +220,14 @@ By default the application runs in the root of your hosted address (i.e. https:/
1. To stop: `docker stop rdtclient`
1. To remove: `docker rm rdtclient`
1. Or use `docker-build.bat`
## Misc Install Notes
### Rootless Podman, Linux Host, and CIFS Connections
RDT Client read and write permission tests fail if the CIFS connection is not setup properly, despite permissions working inspection. In the Web GUI, it will report access denied, and in the log file you will see exceptions like this ([dotnet issue](https://github.com/dotnet/runtime/issues/42790)):
```
System.IO.IOException: Permission denied
```
The **nobrl** has to be specified in your CIFS connection - [man page](https://linux.die.net/man/8/mount.cifs).
Example: ```Options=_netdev,credentials=/etc/samba/credentials/600file,rw,uid=SUBUID,gid=SBUGID,nobrl,file_mode=0770,dir_mode=0770,noperm```

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": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
"maximumWarning": "2mb",
"maximumError": "2mb"
},
{
"type": "anyComponentStyle",

9771
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,48 +6,48 @@
"start": "ng serve",
"build": "ng build",
"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",
"prettier": "prettier --write \"./**/*.{ts,html,json}\""
},
"private": true,
"dependencies": {
"@angular/animations": "^19.2.0",
"@angular/cdk": "^19.2.1",
"@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0",
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0",
"@fortawesome/fontawesome-free": "^6.4.2",
"@microsoft/signalr": "^6.0.21",
"bulma": "^0.9.4",
"curray": "^1.0.11",
"@angular/animations": "^19.2.6",
"@angular/cdk": "^19.2.9",
"@angular/common": "^19.2.6",
"@angular/compiler": "^19.2.6",
"@angular/core": "^19.2.6",
"@angular/forms": "^19.2.6",
"@angular/platform-browser": "^19.2.6",
"@angular/platform-browser-dynamic": "^19.2.6",
"@angular/router": "^19.2.6",
"@fortawesome/fontawesome-free": "^6.7.2",
"@microsoft/signalr": "^8.0.7",
"bulma": "^1.0.3",
"curray": "^1.0.12",
"file-saver-es": "^2.0.5",
"ngx-filesize": "^3.0.5",
"rxjs": "~7.8.1",
"tslib": "^2.6.2",
"zone.js": "~0.15.0"
"filesize": "^10.1.6",
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"zone.js": "^0.15.0"
},
"devDependencies": {
"@angular-eslint/builder": "19.1.0",
"@angular-eslint/eslint-plugin": "19.1.0",
"@angular-eslint/eslint-plugin-template": "19.1.0",
"@angular-eslint/schematics": "19.1.0",
"@angular-eslint/template-parser": "19.1.0",
"@angular/build": "^19.2.0",
"@angular/cli": "^19.2.0",
"@angular/compiler-cli": "^19.2.0",
"@angular/language-service": "^19.2.0",
"@types/file-saver": "^2.0.5",
"@types/file-saver-es": "^2.0.1",
"@types/node": "^20.5.8",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"eslint": "^8.57.0",
"prettier": "^3.0.3",
"typescript": "5.7.3"
"@angular-eslint/builder": "19.3.0",
"@angular-eslint/eslint-plugin": "19.3.0",
"@angular-eslint/eslint-plugin-template": "19.3.0",
"@angular-eslint/schematics": "19.3.0",
"@angular-eslint/template-parser": "19.3.0",
"@angular/build": "^19.2.7",
"@angular/cli": "^19.2.7",
"@angular/compiler-cli": "^19.2.6",
"@angular/language-service": "^19.2.6",
"@types/file-saver": "^2.0.7",
"@types/file-saver-es": "^2.0.3",
"@types/node": "^22.14.1",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"eslint": "^9.24.0",
"prettier": "^3.5.3",
"typescript": "5.8.3"
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';
import { FileSizePipe } from 'ngx-filesize';
import { Download } from './models/download.model';
import { FileSizePipe } from './filesize.pipe';
@Pipe({
name: 'downloadStatus',
standalone: false
name: 'downloadStatus',
standalone: false,
})
export class DownloadStatusPipe implements PipeTransform {
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';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
standalone: false
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
standalone: false,
})
export class LoginComponent {
public userName: string;

View file

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

View file

@ -4,4 +4,6 @@ export class Profile {
public expiration: Date;
public currentVersion: 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" (click)="logout()"> Logout </a>
<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>
</nav>
<div
class="notification is-warning"
*ngIf="profile && profile.latestVersion && profile.currentVersion !== profile.latestVersion"
class="notification"
[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
{{ profile.currentVersion }}.
<span *ngIf="profile.isInsecure"> Your current version is insecure. </span>
<span>
Version {{ profile.latestVersion }} of RealDebrid Client was found. You are currently on version
{{ profile.currentVersion }}.
</span>
</div>

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import { Observable } from 'rxjs';
import { Profile } from './models/profile.model';
import { Setting } from './models/setting.model';
import { APP_BASE_HREF } from '@angular/common';
import { Version } from './models/version.model';
@Injectable({
providedIn: 'root',
@ -26,6 +27,10 @@ export class SettingsService {
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> {
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';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
standalone: false
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
standalone: false,
})
export class SettingsComponent implements OnInit {
public activeTab = 0;

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@
<th>
<input
type="checkbox"
(click)="toggleSelectAll($event)"
(click)="toggleDeleteSelectAll($event)"
[checked]="selectedTorrents.length > 0 && selectedTorrents.length === torrents.length"
/>
</th>
@ -33,7 +33,7 @@
[checked]="selectedTorrents.contains(torrent.torrentId)"
/>
</td>
<td (click)="openTorrent(torrent.torrentId)">
<td (click)="openTorrent(torrent.torrentId)" class="break-all">
{{ torrent.rdName }}
</td>
<td>
@ -107,19 +107,25 @@
<label class="label"></label>
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteData" />
<input type="checkbox" [(ngModel)]="deleteData" (change)="updateDeleteSelectAll()" />
Delete Torrents from client
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
<input type="checkbox" [(ngModel)]="deleteRdTorrent" (change)="updateDeleteSelectAll()" />
Delete Torrents from provider
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteLocalFiles" />
<input type="checkbox" [(ngModel)]="deleteLocalFiles" (change)="updateDeleteSelectAll()" />
Delete local files
</label>
<br />
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteSelectAll" (change)="toggleDeleteSelectAllOptions()" />
Select All
</label>
</div>
</div>
<div class="notification is-primary">

View file

@ -1,6 +1,9 @@
table {
tr {
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';
@Component({
selector: 'app-torrent-table',
templateUrl: './torrent-table.component.html',
styleUrls: ['./torrent-table.component.scss'],
standalone: false
selector: 'app-torrent-table',
templateUrl: './torrent-table.component.html',
styleUrls: ['./torrent-table.component.scss'],
standalone: false,
})
export class TorrentTableComponent implements OnInit {
public torrents: Torrent[] = [];
@ -20,6 +20,7 @@ export class TorrentTableComponent implements OnInit {
public isDeleteModalActive: boolean;
public deleteError: string;
public deleting: boolean;
public deleteSelectAll: boolean;
public deleteData: boolean;
public deleteRdTorrent: boolean;
public deleteLocalFiles: boolean;
@ -74,7 +75,7 @@ export class TorrentTableComponent implements OnInit {
return el.torrentId;
}
public toggleSelectAll(event: any) {
public toggleDeleteSelectAll(event: any) {
this.selectedTorrents = [];
if (event.target.checked) {
@ -247,4 +248,13 @@ export class TorrentTableComponent implements OnInit {
},
});
}
toggleDeleteSelectAllOptions() {
this.deleteData = this.deleteSelectAll;
this.deleteRdTorrent = this.deleteSelectAll;
this.deleteLocalFiles = this.deleteSelectAll;
}
updateDeleteSelectAll() {
this.deleteSelectAll = this.deleteData && this.deleteRdTorrent && this.deleteLocalFiles;
}
}

View file

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

View file

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

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

@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using RdtClient.Data.Models.Data;
using Download = RdtClient.Data.Models.Data.Download;
namespace RdtClient.Data.Data;
@ -29,13 +30,14 @@ public class DownloadData(DataContext dataContext)
.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
}
public async Task<Download> Add(Guid torrentId, String path)
public async Task<Download> Add(Guid torrentId, DownloadInfo downloadInfo)
{
var download = new Download
{
DownloadId = Guid.NewGuid(),
TorrentId = torrentId,
Path = path,
FileName = downloadInfo.FileName,
Path = downloadInfo.RestrictedLink,
Added = DateTimeOffset.UtcNow,
DownloadQueued = DateTimeOffset.UtcNow,
RetryCount = 0

View file

@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Code style", Scope = "NamespaceAndDescendants", Target = "N:RdtClient.Web")]

View file

@ -26,7 +26,7 @@ public class Download
public DateTimeOffset? Completed { get; set; }
public Int32 RetryCount { get; set; }
public String? Error { get; set; }
public String? RemoteId { get; set; }
@ -41,4 +41,20 @@ public class Download
[NotMapped]
public Int64 Speed { get; set; }
}
/// <summary>
/// Used to create <see cref="Download"/>s
/// </summary>
public class DownloadInfo
{
/// <summary>
/// The name of the file. Should not include directory.
/// If the filename is not known, set tn null and `GetFileName` will be called with the unrestricted link.
/// </summary>
public required String? FileName;
/// <summary>
/// The restricted link to download this download. If the debrid serice in question does not have restricted links, use either a fake or the unrestricted link
/// </summary>
public required String RestrictedLink;
}

View file

@ -74,6 +74,10 @@ Supports the following parameters:
[DisplayName("Copy added torrent files")]
[Description("When a torrent file or magnet is added, create a copy in this directory.")]
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
@ -111,8 +115,8 @@ public class DbSettingsDownloadClient
[Description("Timeout in milliseconds before the downloader times out.")]
public Int32 Timeout { get; set; } = 5000;
[DisplayName("Proxy Server (only used for the Internal Downloader)")]
[Description("Address of a proxy server to download through (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 Bezzad Downloader).")]
public String? ProxyServer { get; set; } = null;
[DisplayName("Aria2c URL (only used for the Aria2c Downloader)")]
@ -177,6 +181,13 @@ or
<a href=""https://debrid-link.com/webapp/apikey"" target=""_blank"" rel=""noopener"">https://debrid-link.com/webapp/apikey</a>")]
public String ApiKey { get; set; } = "";
/// <summary>
/// API hostname to use <b>for Real Debrid only</b>
/// </summary>
[DisplayName("API Hostname (RD only)")]
[Description("Use this instead of the normal hostname for Real Debrid API requests. Only used by Real Debrid. Leave blank to use default.")]
public String? ApiHostname { get; set; }
[DisplayName("Automatically import and process torrents added to provider")]
[Description("When selected, import downloads that are not added through RealDebridClient but have been directly added to your debrid provider.")]
public Boolean AutoImport { get; set; } = false;
@ -193,6 +204,10 @@ or
[Description("The interval to check the torrents info on the providers API. Minumum is 3 seconds. When there are no active downloads this limit is increased * 3.")]
public Int32 CheckInterval { get; set; } = 10;
[DisplayName("Prefer zipped downloads")]
[Description("Torbox only. When selected, rdt-client will try to download the entire torrent as a .zip from TorBox and unpack it instead of downloading each file individually.")]
public Boolean PreferZippedDownloads { get; set; } = false;
[DisplayName("Auto Import Defaults")]
public DbSettingsDefaultsWithCategory Default { get; set; } = new();
}

View file

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

View file

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

View file

@ -10,16 +10,16 @@
</PropertyGroup>
<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">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.11" />
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.0.11" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.11" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.13" />
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.13" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>

View file

@ -405,7 +405,7 @@ public class AllDebridTorrentClientTest
}
[Fact]
public async Task GetDownloadLinks_WhenTorrentRdIdNull_ReturnsNull()
public async Task GetDownloadInfos_WhenTorrentRdIdNull_ReturnsNull()
{
// Arrange
var mocks = new Mocks();
@ -418,7 +418,7 @@ public class AllDebridTorrentClientTest
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
// Assert
Assert.Null(result);
@ -426,7 +426,7 @@ public class AllDebridTorrentClientTest
}
[Fact]
public async Task GetDownloadLinks_UsesFileFilter()
public async Task GetDownloadInfos_UsesFileFilter()
{
// Arrange
var mocks = new Mocks();
@ -467,16 +467,16 @@ public class AllDebridTorrentClientTest
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Contains("https://fake.url/file1.txt", result);
Assert.Single(result, info => info.RestrictedLink == "https://fake.url/file1.txt");
}
[Fact]
public async Task GetDownloadLinks_WhenAllFilesExcluded_ReturnsAllFiles()
public async Task GetDownloadInfos_WhenAllFilesExcluded_ReturnsEmptyList()
{
// Arrange
var mocks = new Mocks();
@ -502,19 +502,17 @@ public class AllDebridTorrentClientTest
}
];
var expectedLinksSet = new HashSet<String>(files.Select(n => n.DownloadLink)!);
mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny<CancellationToken>())).ReturnsAsync(files);
mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(false);
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
// Assert
Assert.NotNull(result);
var linksSet = new HashSet<String>(result);
Assert.Equal(expectedLinksSet, linksSet);
Assert.Empty(result);
mocks.FileFilterMock.Verify(f => f.IsDownloadable(torrent, "file-1.txt", 100));
mocks.FileFilterMock.Verify(f => f.IsDownloadable(torrent, "file-2.txt", 100));

View file

@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RdtClient.Data.Enums;
using RdtClient.Service.Services;
namespace RdtClient.Service.BackgroundServices;
@ -26,8 +27,8 @@ public class ProviderUpdater(ILogger<TaskRunner> logger, IServiceProvider servic
try
{
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");
@ -52,12 +53,12 @@ public class ProviderUpdater(ILogger<TaskRunner> logger, IServiceProvider servic
await torrentService.UpdateRdData();
logger.LogDebug($"Finished updating torrent info from debrid provider, next update in {updateTime} seconds");
logger.LogDebug("Finished updating torrent info from debrid provider, next update in {updateTime} seconds", updateTime);
}
}
catch (Exception ex)
{
logger.LogError(ex, $"Unexpected error occurred in ProviderUpdater: {ex.Message}");
logger.LogError(ex, "Unexpected error occurred in ProviderUpdater: {ex.Message}", ex.Message);
}
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);

View file

@ -20,7 +20,7 @@ public class Startup(IServiceProvider serviceProvider) : IHostedService
using var scope = serviceProvider.CreateScope();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Startup>>();
logger.LogWarning($"Starting host on version {version}");
logger.LogWarning("Starting host on version {version}", version);
var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
await dbContext.Database.MigrateAsync(cancellationToken);

View file

@ -9,6 +9,11 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
{
public static String? CurrentVersion { 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)
{
@ -34,18 +39,9 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
{
try
{
var httpClient = new HttpClient();
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 = await GitHubRequest<List<GitHubReleasesResponse>>("/repos/rogerfar/rdt-client/tags?per_page=1", stoppingToken);
var gitHubReleases = JsonConvert.DeserializeObject<List<GitHubReleasesResponse>>(response);
if (gitHubReleases == null || gitHubReleases.Count == 0)
{
return;
}
var latestRelease = gitHubReleases.FirstOrDefault(m => m.Name != null)?.Name;
var latestRelease = gitHubReleases?.FirstOrDefault(m => m.Name != null)?.Name;
if (latestRelease == null)
{
@ -59,6 +55,18 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
}
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)
{
@ -70,10 +78,25 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
logger.LogInformation("UpdateChecker stopped.");
}
private static 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
{
[JsonProperty("name")]
public String? Name { get; set; }
}
public class GitHubSecurityAdvisoriesResponse
{
[JsonProperty("ghsa_id")]
public required String GhsaId { get; set; }
}

View file

@ -6,3 +6,4 @@
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "Bug in Serilog", Scope = "NamespaceAndDescendants", Target = "N:RdtClient.Service")]
[assembly: SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Code style", Scope = "NamespaceAndDescendants", Target = "N:RdtClient.Service")]

View file

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

View file

@ -1,4 +1,5 @@
using RdtClient.Data.Data;
using RdtClient.Data.Models.Data;
using Download = RdtClient.Data.Models.Data.Download;
namespace RdtClient.Service.Services;
@ -20,9 +21,9 @@ public class Downloads(DownloadData downloadData) : IDownloads
return await downloadData.Get(torrentId, path);
}
public async Task<Download> Add(Guid torrentId, String path)
public async Task<Download> Add(Guid torrentId, DownloadInfo downloadInfo)
{
return await downloadData.Add(torrentId, path);
return await downloadData.Add(torrentId, downloadInfo);
}
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)

View file

@ -7,7 +7,7 @@ public interface IDownloads
Task<List<Download>> GetForTorrent(Guid torrentId);
Task<Download?> GetById(Guid downloadId);
Task<Download?> Get(Guid torrentId, String path);
Task<Download> Add(Guid torrentId, String path);
Task<Download> Add(Guid torrentId, DownloadInfo downloadInfo);
Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink);
Task UpdateFileName(Guid downloadId, String fileName);
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);

View file

@ -411,11 +411,11 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
{
if (deleteFiles)
{
logger.LogDebug($"Delete {hash}, with files");
logger.LogDebug("Delete {hash}, with files", hash);
}
else
{
logger.LogDebug($"Delete {hash}, no files");
logger.LogDebug("Delete {hash}, no files", hash);
}
var torrent = await torrents.GetByHash(hash);

View file

@ -1,10 +1,10 @@
using AllDebridNET;
using System.Diagnostics;
using AllDebridNET;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
using System.Web;
using RdtClient.Data.Models.Data;
using File = AllDebridNET.File;
using Torrent = RdtClient.Data.Models.Data.Torrent;
@ -155,9 +155,10 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
}
public Task SelectFiles(Torrent torrent)
/// <inheritdoc />
public Task<Int32?> SelectFiles(Torrent torrent)
{
return Task.CompletedTask;
return Task.FromResult<Int32?>(torrent.Files.Count);
}
public async Task Delete(String torrentId)
@ -198,7 +199,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
torrent.RdSize = torrentClientTorrent.Bytes;
}
if (torrentClientTorrent.Files != null && torrentClientTorrent.Files.Any())
if (torrentClientTorrent.Files != null && torrentClientTorrent.Files.Count != 0)
{
torrent.RdFiles = JsonConvert.SerializeObject(torrentClientTorrent.Files);
}
@ -245,48 +246,33 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
return torrent;
}
public async Task<IList<String>?> GetDownloadLinks(Torrent torrent)
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent)
{
if (torrent.RdId == null)
{
return null;
}
Log($"Getting download links", torrent);
var allFiles = await allDebridNetClientFactory.GetClient().Magnet.FilesAsync(Int64.Parse(torrent.RdId));
var files = GetFiles(allFiles);
files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList();
Log($"Getting download links", torrent);
if (files.Count == 0)
return files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes) && f.DownloadLink != null).Select(f => new DownloadInfo
{
Log($"Filtered all files out! Downloading ALL files instead!", torrent);
files = GetFiles(allFiles);
}
Log($"Selecting links:");
foreach (var file in files)
{
Log($"{file.Path} ({file.Bytes}b) {file.DownloadLink}");
}
return files.Where(m => m.DownloadLink != null).Select(m => m.DownloadLink!.ToString()).ToList();
FileName = Path.GetFileName(f.Path),
RestrictedLink = f.DownloadLink!
}).ToList();
}
public Task<String> GetFileName(String downloadUrl)
/// <inheritdoc />
public Task<String> GetFileName(Download download)
{
if (String.IsNullOrWhiteSpace(downloadUrl))
{
return Task.FromResult("");
}
var uri = new Uri(downloadUrl);
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
// FileName is set in GetDownlaadInfos
Debug.Assert(download.FileName != null);
return Task.FromResult(download.FileName);
}
private async Task<TorrentClientTorrent> GetInfo(String torrentId)

View file

@ -1,12 +1,13 @@
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using DebridLinkFrNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
using DebridLinkFrNET.Models;
using System.Web;
using RdtClient.Data.Models.Data;
using Download = RdtClient.Data.Models.Data.Download;
using Torrent = DebridLinkFrNET.Models.Torrent;
namespace RdtClient.Service.Services.TorrentClients;
@ -136,9 +137,10 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
}
public Task SelectFiles(Data.Models.Data.Torrent torrent)
/// <inheritdoc />
public Task<Int32?> SelectFiles(Data.Models.Data.Torrent torrent)
{
return Task.CompletedTask;
return Task.FromResult<Int32?>(torrent.Files.Count);
}
public async Task Delete(String torrentId)
@ -234,7 +236,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
return torrent;
}
public async Task<IList<String>?> GetDownloadLinks(Data.Models.Data.Torrent torrent)
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Data.Models.Data.Torrent torrent)
{
if (torrent.RdId == null)
{
@ -248,19 +250,14 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
return null;
}
var downloadLinks = rdTorrent.Files?
.Where(m => fileFilter.IsDownloadable(torrent, m.Path, m.Bytes) && m.DownloadLink != null)
.Select(m => m.DownloadLink!)
.ToList() ?? [];
Log($"Found {downloadLinks.Count} links", torrent);
foreach (var link in downloadLinks)
{
Log($"{link}", torrent);
}
return downloadLinks;
return rdTorrent.Files?
.Where(m => fileFilter.IsDownloadable(torrent, m.Path, m.Bytes) && m.DownloadLink != null)
.Select(m => new DownloadInfo
{
RestrictedLink = m.DownloadLink!,
FileName = Path.GetFileName(m.Path)
})
.ToList();
}
private async Task<TorrentClientTorrent> GetInfo(String torrentId)
@ -280,16 +277,13 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
logger.LogDebug(message);
}
public Task<String> GetFileName(String downloadUrl)
/// <inheritdoc />
public Task<String> GetFileName(Download download)
{
if (String.IsNullOrWhiteSpace(downloadUrl))
{
return Task.FromResult("");
}
var uri = new Uri(downloadUrl);
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
// FileName is set in GetDownlaadInfos
Debug.Assert(download.FileName != null);
return Task.FromResult(download.FileName);
}
public static String? GetSymlinkPath(Data.Models.Data.Torrent torrent, Download download)

View file

@ -10,10 +10,24 @@ public interface ITorrentClient
Task<String> AddMagnet(String magnetLink);
Task<String> AddFile(Byte[] bytes);
Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash);
Task SelectFiles(Torrent torrent);
/// <summary>
/// Tell the debrid provider which files to download.
/// </summary>
/// <remark>
/// Not all providers support this feature.
/// </remark>
/// <param name="torrent">The torrent to select files for</param>
/// <returns>Number of files selected</returns>
Task<Int32?> SelectFiles(Torrent torrent);
Task Delete(String torrentId);
Task<String> Unrestrict(String link);
Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent);
Task<IList<String>?> GetDownloadLinks(Torrent torrent);
Task<String> GetFileName(String downloadUrl);
Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent);
/// <summary>
/// To be called only when <see cref="Data.Models.Data.Download" />.<see cref="Data.Models.Data.Download.FileName" /> is not set by
/// <see cref="GetDownloadInfos" />
/// </summary>
/// <param name="download">The download to get the filename of</param>
/// <returns>The filename of the download</returns>
Task<String> GetFileName(Download download);
}

View file

@ -1,10 +1,11 @@
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using PremiumizeNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
using System.Web;
using RdtClient.Data.Models.Data;
using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services.TorrentClients;
@ -121,9 +122,12 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
}
public Task SelectFiles(Torrent torrent)
/// <inheritdoc />
public Task<Int32?> SelectFiles(Torrent torrent)
{
return Task.CompletedTask;
// torrent.Files is not populated when this function is called
// by returning 1, we ensure no logic for "all files excluded" is followed
return Task.FromResult<Int32?>(1);
}
public async Task Delete(String id)
@ -197,7 +201,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
return torrent;
}
public async Task<IList<String>?> GetDownloadLinks(Torrent torrent)
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent)
{
if (torrent.RdId == null)
{
@ -212,7 +216,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
Log($"Found transfer {transfer.Name} ({transfer.Id})", torrent);
var downloadLinks = await GetAllDownloadLinks(torrent, transfer.FolderId);
var downloadInfos = await GetAllDownloadInfos(torrent, transfer.FolderId);
if (!String.IsNullOrWhiteSpace(transfer.FileId))
{
@ -225,34 +229,24 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
Log($"File {file.Name} ({file.Id}) does not contain a link", torrent);
}
downloadLinks.Add(file.Link);
downloadInfos.Add(new () {RestrictedLink = file.Link, FileName = file.Name });
}
if (downloadLinks.Count == 0)
foreach (var info in downloadInfos)
{
Log($"No download links found for transfer {transfer.Name} ({transfer.Id})", torrent);
return null;
Log($"Found {info.RestrictedLink}", torrent);
}
foreach (var link in downloadLinks)
{
Log($"Found {link}", torrent);
}
return downloadLinks;
return downloadInfos;
}
public Task<String> GetFileName(String downloadUrl)
/// <inheritdoc />
public Task<String> GetFileName(Download download)
{
if (String.IsNullOrWhiteSpace(downloadUrl))
{
return Task.FromResult("");
}
var uri = new Uri(downloadUrl);
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
// FileName is set in GetDownlaadInfos
Debug.Assert(download.FileName != null);
return Task.FromResult(download.FileName);
}
private async Task<TorrentClientTorrent> GetInfo(String id)
@ -263,13 +257,12 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
return Map(result);
}
private async Task<List<String>> GetAllDownloadLinks(Torrent torrent, String folderId)
private async Task<List<DownloadInfo>> GetAllDownloadInfos(Torrent torrent, String folderId)
{
var downloadLinks = new List<String>();
if (String.IsNullOrWhiteSpace(folderId))
{
return downloadLinks;
return [];
}
var folder = await GetClient().Folder.ListAsync(folderId);
@ -277,11 +270,13 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
if (folder.Content == null)
{
Log($"Found no items in folder {folder.Name} ({folderId})", torrent);
return downloadLinks;
return [];
}
Log($"Found {folder.Content.Count} items in folder {folder.Name} ({folderId})", torrent);
var downloadInfos = new List<DownloadInfo>();
foreach (var item in folder.Content)
{
if (item.Type == "file")
@ -300,7 +295,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
Log($"Found item {item.Name} in folder {folder.Name} ({folderId})", torrent);
downloadLinks.Add(item.Link);
downloadInfos.Add(new () { RestrictedLink = item.Link, FileName = item.Name});
}
else if (item.Type == "folder")
{
@ -311,8 +306,8 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
}
Log($"Found subfolder {item.Name} in {folder.Name} ({folderId}), searching subfolder for files", torrent);
var subDownloadLinks = await GetAllDownloadLinks(torrent, item.Id);
downloadLinks.AddRange(subDownloadLinks);
var subDownloadLinks = await GetAllDownloadInfos(torrent, item.Id);
downloadInfos.AddRange(subDownloadLinks);
}
else
{
@ -320,7 +315,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
}
}
return downloadLinks;
return downloadInfos;
}
private void Log(String message, Torrent? torrent = null)

View file

@ -3,8 +3,11 @@ using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using RDNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
using Download = RdtClient.Data.Models.Data.Download;
using Torrent = RDNET.Torrent;
namespace RdtClient.Service.Services.TorrentClients;
@ -26,7 +29,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var rdtNetClient = new RdNetClient(null, httpClient, 5);
var rdtNetClient = new RdNetClient(null, httpClient, 5, Settings.Get.Provider.ApiHostname);
rdtNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
@ -142,9 +145,10 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
}
public async Task SelectFiles(Data.Models.Data.Torrent torrent)
/// <inheritdoc />
public async Task<Int32?> SelectFiles(Data.Models.Data.Torrent torrent)
{
IList<TorrentClientFile> files;
List<TorrentClientFile> files;
Log("Seleting files", torrent);
@ -155,33 +159,25 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
}
else
{
Log("Selecting all files", torrent);
Log("Selecting files", torrent);
files = [.. torrent.Files];
}
Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList();
if (files.Count == 0)
{
Log($"Filtered all files out! Downloading ALL files instead!", torrent);
files = torrent.Files;
}
Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
var fileIds = files.Select(m => m.Id.ToString()).ToArray();
Log($"Selecting files:");
foreach (var file in files)
if (fileIds.Length == 0)
{
Log($"{file.Id}: {file.Path} ({file.Bytes}b)");
return 0;
}
Log("", torrent);
await GetClient().Torrents.SelectFilesAsync(torrent.RdId!, [.. fileIds]);
return fileIds.Length;
}
public async Task Delete(String torrentId)
@ -280,7 +276,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return torrent;
}
public async Task<IList<String>?> GetDownloadLinks(Data.Models.Data.Torrent torrent)
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Data.Models.Data.Torrent torrent)
{
if (torrent.RdId == null)
{
@ -294,7 +290,14 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return null;
}
var downloadLinks = rdTorrent.Links.Where(m => !String.IsNullOrWhiteSpace(m)).ToList();
var downloadLinks = rdTorrent.Links
.Where(m => !String.IsNullOrWhiteSpace(m))
.Select(l => new DownloadInfo
{
RestrictedLink = l,
FileName = null
})
.ToList();
Log($"Found {downloadLinks.Count} links", torrent);
@ -341,14 +344,15 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return null;
}
public Task<String> GetFileName(String downloadUrl)
/// <inheritdoc />
public Task<String> GetFileName(Download download)
{
if (String.IsNullOrWhiteSpace(downloadUrl))
if (String.IsNullOrWhiteSpace(download.Link))
{
return Task.FromResult("");
}
var uri = new Uri(downloadUrl);
var uri = new Uri(download.Link);
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
}

View file

@ -1,9 +1,10 @@
using System.Reflection;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using TorBoxNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
using System.Web;
using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers;
@ -24,7 +25,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
}
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);
var torBoxNetClient = new TorBoxNetClient(null, httpClient, 5);
@ -132,10 +133,8 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
var magnetLinkInfo = MonoTorrent.MagnetLink.Parse(magnetLink);
return magnetLinkInfo.InfoHashes.V1!.ToHex().ToLowerInvariant();
}
else
{
return result.Data!.Hash!;
}
return result.Data!.Hash!;
}
public async Task<String> AddFile(Byte[] bytes)
@ -170,9 +169,10 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return [];
}
public Task SelectFiles(Torrent torrent)
/// <inheritdoc />
public Task<Int32?> SelectFiles(Torrent torrent)
{
return Task.CompletedTask;
return Task.FromResult<Int32?>(torrent.Files.Count);
}
public async Task Delete(String torrentId)
@ -206,7 +206,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return torrent;
}
var rdTorrent = await GetInfo(torrent.Hash) ?? throw new($"Resource not found");
var rdTorrent = torrentClientTorrent ?? await GetInfo(torrent.Hash) ?? throw new($"Resource not found");
if (!String.IsNullOrWhiteSpace(rdTorrent.Filename))
{
@ -284,39 +284,42 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return torrent;
}
public async Task<IList<String>?> GetDownloadLinks(Torrent torrent)
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent)
{
var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, skipCache: true);
var downloadableFiles = torrent.Files.Where(file => fileFilter.IsDownloadable(torrent, file.Path, file.Bytes)).ToList();
return torrent.Files.Where(file => fileFilter.IsDownloadable(torrent, file.Path, file.Bytes))
.Select(file => $"https://torbox.app/fakedl/{torrentId?.Id}/{file.Id}")
.ToList();
if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && Settings.Get.Provider.PreferZippedDownloads)
{
logger.LogDebug("Downloading files from TorBox as a zip.");
return
[
new()
{
RestrictedLink = $"https://torbox.app/fakedl/{torrentId?.Id}/zip",
FileName = $"{torrent.RdName}.zip"
}
];
}
logger.LogDebug("Downloading files from TorBox individually.");
return downloadableFiles.Select(file => new DownloadInfo
{
RestrictedLink = $"https://torbox.app/fakedl/{torrentId?.Id}/{file.Id}",
FileName = Path.GetFileName(file.Path)
})
.ToList();
}
public async Task<String> GetFileName(String downloadUrl)
/// <inheritdoc />
public Task<String> GetFileName(Download download)
{
if (String.IsNullOrWhiteSpace(downloadUrl))
{
return "";
}
var uri = new Uri(downloadUrl);
using (HttpClient client = new())
{
var request = new HttpRequestMessage(HttpMethod.Head, uri);
var response = await client.SendAsync(request);
if (response.Content.Headers.ContentDisposition != null)
{
var fileName = response.Content.Headers.ContentDisposition.FileName;
if (!String.IsNullOrWhiteSpace(fileName))
{
return fileName.Trim('"');
}
}
}
return HttpUtility.UrlDecode(uri.Segments.Last());
// FileName is set in GetDownlaadInfos
Debug.Assert(download.FileName != null);
return Task.FromResult(download.FileName);
}
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
@ -335,7 +338,44 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return Map(result!);
}
public static void MoveHashDirContents(String extractPath, Torrent _torrent)
{
var hashDir = Path.Combine(extractPath, _torrent.Hash);
if (Directory.Exists(hashDir))
{
var innerFolder = Directory.GetDirectories(hashDir)[0];
var moveDir = extractPath;
if (!extractPath.EndsWith(_torrent.RdName!))
{
moveDir = hashDir;
}
foreach (var file in Directory.GetFiles(innerFolder))
{
var destFile = Path.Combine(moveDir, Path.GetFileName(file));
File.Move(file, destFile);
}
foreach (var dir in Directory.GetDirectories(innerFolder))
{
var destDir = Path.Combine(moveDir, Path.GetFileName(dir));
Directory.Move(dir, destDir);
}
if (!extractPath.Contains(_torrent.RdName!))
{
Directory.Delete(innerFolder, true);
}
else
{
Directory.Delete(hashDir, true);
}
}
}
private void Log(String message, Torrent? torrent = null)
{
if (torrent != null)

View file

@ -8,7 +8,6 @@ using RdtClient.Service.Services.Downloaders;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;
using System.Web;
namespace RdtClient.Service.Services;
@ -363,8 +362,11 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
var downloadLink = await torrents.UnrestrictLink(download.DownloadId);
download.Link = downloadLink;
var fileName = await torrents.RetrieveFileName(download.DownloadId);
download.FileName = fileName;
if (download.FileName == null)
{
var fileName = await torrents.RetrieveFileName(download.DownloadId);
download.FileName = fileName;
}
}
}
catch (Exception ex)
@ -447,13 +449,8 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
// Check if the unpacking process is even needed
var uri = new Uri(download.Link);
var fileName = uri.Segments.Last();
fileName = HttpUtility.UrlDecode(fileName);
Log($"Found file name {fileName}", download, torrent);
var extension = Path.GetExtension(fileName);
var extension = Path.GetExtension(download.FileName);
if ((extension != ".rar" && extension != ".zip") ||
torrent.DownloadClient == Data.Enums.DownloadClient.Symlink)

View file

@ -223,7 +223,12 @@ public class Torrents(
return;
}
await TorrentClient.SelectFiles(torrent);
var selected = await TorrentClient.SelectFiles(torrent);
if (selected == 0)
{
await MarkAllFilesExcluded(torrent);
}
}
public async Task CreateDownloads(Guid torrentId)
@ -235,39 +240,48 @@ public class Torrents(
return;
}
var downloadLinks = await TorrentClient.GetDownloadLinks(torrent);
var downloadInfos = await TorrentClient.GetDownloadInfos(torrent);
if (downloadLinks == null)
if (downloadInfos == null)
{
return;
}
if (downloadLinks.Count == 0)
if (downloadInfos.Count == 0)
{
logger.LogInformation("All files excluded by filters (IncludeRegex: {includeRegex}, ExcludeRegex: {excludeRegex}, DownloadMinSize: {downloadMinSize} {torrentInfo}",
torrent.IncludeRegex,
torrent.ExcludeRegex,
torrent.DownloadMinSize,
torrent.ToLog());
await torrentData.UpdateRetry(torrentId, null, torrent.TorrentRetryAttempts);
await torrentData.UpdateComplete(torrentId, "All files excluded", DateTimeOffset.Now, false);
await MarkAllFilesExcluded(torrent);
return;
}
foreach (var downloadLink in downloadLinks)
foreach (var downloadInfo in downloadInfos)
{
// Make sure downloads don't get added multiple times
var downloadExists = await downloads.Get(torrent.TorrentId, downloadLink);
var downloadExists = await downloads.Get(torrent.TorrentId, downloadInfo.RestrictedLink);
if (downloadExists == null && !String.IsNullOrWhiteSpace(downloadLink))
if (downloadExists == null && !String.IsNullOrWhiteSpace(downloadInfo.RestrictedLink))
{
await downloads.Add(torrent.TorrentId, downloadLink);
await downloads.Add(torrent.TorrentId, downloadInfo);
}
}
}
/// <summary>
/// Logs a message to the console, sets the error on the torrent and ensures it is not retried.
/// </summary>
/// <param name="torrent">The torrent to mark as "All files excluded"</param>
private async Task MarkAllFilesExcluded(Torrent torrent)
{
logger.LogInformation("All files excluded by filters (IncludeRegex: {includeRegex}, ExcludeRegex: {excludeRegex}, DownloadMinSize: {downloadMinSize}) {torrentInfo}",
torrent.IncludeRegex,
torrent.ExcludeRegex,
torrent.DownloadMinSize,
torrent.ToLog());
await torrentData.UpdateRetry(torrent.TorrentId, null, torrent.TorrentRetryAttempts);
await torrentData.UpdateComplete(torrent.TorrentId, "All files excluded", DateTimeOffset.Now, false);
}
public async Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles)
{
var torrent = await GetById(torrentId);
@ -390,13 +404,17 @@ public class Torrents(
return unrestrictedLink;
}
/// <summary>
/// To be called only when <see cref="Data.Models.Data.Download" />.<see cref="Data.Models.Data.Download.FileName" /> is not set by
/// <see cref="ITorrentClient.GetDownloadInfos" />
/// </summary>
public async Task<String> RetrieveFileName(Guid downloadId)
{
var download = await downloads.GetById(downloadId) ?? throw new($"Download with ID {downloadId} not found");
Log($"Retrieving filename for", download, download.Torrent);
var fileName = await TorrentClient.GetFileName(download.Link!);
var fileName = await TorrentClient.GetFileName(download!);
await downloads.UpdateFileName(downloadId, fileName);
@ -413,7 +431,9 @@ public class Torrents(
UserName = user.Username,
Expiration = user.Expiration,
CurrentVersion = UpdateChecker.CurrentVersion,
LatestVersion = UpdateChecker.LatestVersion
LatestVersion = UpdateChecker.LatestVersion,
IsInsecure = UpdateChecker.IsInsecure,
DisableUpdateNotification = Settings.Get.General.DisableUpdateNotifications
};
return profile;

View file

@ -1,6 +1,7 @@
using System.Diagnostics;
using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers;
using RdtClient.Service.Services.TorrentClients;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Archives.Zip;
@ -10,13 +11,13 @@ namespace RdtClient.Service.Services;
public class UnpackClient(Download download, String destinationPath)
{
public Boolean Finished { get; private set; }
public String? Error { get; private set; }
public Int32 Progess { get; private set; }
private readonly Torrent _torrent = download.Torrent ?? throw new($"Torrent is null");
private readonly CancellationTokenSource _cancellationTokenSource = new();
public void Start()
@ -69,13 +70,13 @@ public class UnpackClient(Download download, String destinationPath)
if (archiveEntries.Any(m => m.Contains(".r00")))
{
extractPathTemp = Path.Combine(extractPath, Guid.NewGuid().ToString());
if (!Directory.Exists(extractPathTemp))
{
Directory.CreateDirectory(extractPathTemp);
}
}
if (extractPathTemp != null)
{
Extract(filePath, extractPathTemp, cancellationToken);
@ -102,6 +103,11 @@ public class UnpackClient(Download download, String destinationPath)
await FileHelper.Delete(filePath);
}
if (_torrent.ClientKind == Data.Enums.Provider.TorBox)
{
TorBoxTorrentClient.MoveHashDirContents(extractPath, _torrent);
}
}
catch (Exception ex)
{
@ -113,6 +119,7 @@ public class UnpackClient(Download download, String destinationPath)
}
}
private static async Task<IList<String>> GetArchiveFiles(String filePath)
{
await using Stream stream = File.OpenRead(filePath);
@ -161,10 +168,10 @@ public class UnpackClient(Download download, String destinationPath)
d =>
{
Debug.WriteLine(d);
Progess = (Int32) Math.Round(d);
Progess = (Int32)Math.Round(d);
},
cancellationToken: cancellationToken);
archive.Dispose();
GC.Collect();

View file

@ -18,6 +18,7 @@ public class Process : IProcess
public void Dispose()
{
_process.Dispose();
GC.SuppressFinalize(this);
}
public void BeginOutputReadLine()

View file

@ -67,7 +67,7 @@ public class AuthController(Authentication authentication, Settings settings) :
return Ok();
}
[AllowAnonymous]
[Authorize(Policy = "AuthSetting")]
[Route("SetupProvider")]
[HttpPost]
public async Task<ActionResult> SetupProvider([FromBody] AuthControllerSetupProviderRequest? request)
@ -82,13 +82,6 @@ public class AuthController(Authentication authentication, Settings settings) :
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:ApiKey", request.Token);

View file

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

View file

@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Code style", Scope = "NamespaceAndDescendants", Target = "N:RdtClient.Web")]

View file

@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId>
<Version>2.0.102</Version>
<Version>2.0.104</Version>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
@ -29,15 +29,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.4" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.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
$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>"
[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"
[Console]::ReadKey()