Merge branch 'main' into feat/github-action
This commit is contained in:
commit
15d7e7b0cc
73 changed files with 2444 additions and 13107 deletions
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -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
141
.github/workflows/build-release.yaml
vendored
Normal 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 }}
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -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.
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -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
14
client/.ncurc.js
Normal 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';
|
||||
}
|
||||
};
|
||||
|
|
@ -57,8 +57,8 @@
|
|||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
|
|
|
|||
9771
client/package-lock.json
generated
9771
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
20
client/src/app/filesize.pipe.ts
Normal file
20
client/src/app/filesize.pipe.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,6 @@ export class Profile {
|
|||
public expiration: Date;
|
||||
public currentVersion: string;
|
||||
public latestVersion: string;
|
||||
public isInsecure: boolean;
|
||||
public disableUpdateNotification: boolean;
|
||||
}
|
||||
|
|
|
|||
3
client/src/app/models/version.model.ts
Normal file
3
client/src/app/models/version.model.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export class Version {
|
||||
public version: string;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
table {
|
||||
tr {
|
||||
cursor: pointer;
|
||||
td.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
4611
package-lock.json
generated
File diff suppressed because it is too large
Load diff
20
package.json
20
package.json
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
8
server/RdtClient.Data/GlobalSuppressions.cs
Normal file
8
server/RdtClient.Data/GlobalSuppressions.cs
Normal 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")]
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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")]
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public class Process : IProcess
|
|||
public void Dispose()
|
||||
{
|
||||
_process.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void BeginOutputReadLine()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
8
server/RdtClient.Web/GlobalSuppressions.cs
Normal file
8
server/RdtClient.Web/GlobalSuppressions.cs
Normal 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")]
|
||||
|
|
@ -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
0
docker-build-dev.ps1 → tools/docker-build-dev.ps1
Executable file → Normal file
0
docker-build.ps1 → tools/docker-build.ps1
Executable file → Normal file
0
docker-build.ps1 → tools/docker-build.ps1
Executable file → Normal file
0
docker-publish.ps1 → tools/docker-publish.ps1
Executable file → Normal file
0
docker-publish.ps1 → tools/docker-publish.ps1
Executable file → Normal 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()
|
||||
Loading…
Reference in a new issue