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?**
|
**What version are you using?**
|
||||||
|
|
||||||
**Wat OS are you running?**
|
**What OS are you running?**
|
||||||
|
|
||||||
**Are you using Docker or as a service?**
|
**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/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [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
|
## [2.0.102] - 2025-03-07
|
||||||
### Changed
|
### Changed
|
||||||
- Fixed Angular build for Docker.
|
- Fixed Angular build for Docker.
|
||||||
|
|
|
||||||
13
README.md
13
README.md
|
|
@ -157,7 +157,7 @@ Required configuration:
|
||||||
Suggested configuration:
|
Suggested configuration:
|
||||||
- Automatic retry downloads > 3
|
- 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.
|
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 stop: `docker stop rdtclient`
|
||||||
1. To remove: `docker rm rdtclient`
|
1. To remove: `docker rm rdtclient`
|
||||||
1. Or use `docker-build.bat`
|
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": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "500kb",
|
"maximumWarning": "2mb",
|
||||||
"maximumError": "1mb"
|
"maximumError": "2mb"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"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",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"update": "ng update --force --allow-dirty @angular/cli @angular/core @angular/cdk @angular/flex-layout",
|
"update": "ng update --force --allow-dirty @angular/cli @angular/core @angular/cdk",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"prettier": "prettier --write \"./**/*.{ts,html,json}\""
|
"prettier": "prettier --write \"./**/*.{ts,html,json}\""
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^19.2.0",
|
"@angular/animations": "^19.2.6",
|
||||||
"@angular/cdk": "^19.2.1",
|
"@angular/cdk": "^19.2.9",
|
||||||
"@angular/common": "^19.2.0",
|
"@angular/common": "^19.2.6",
|
||||||
"@angular/compiler": "^19.2.0",
|
"@angular/compiler": "^19.2.6",
|
||||||
"@angular/core": "^19.2.0",
|
"@angular/core": "^19.2.6",
|
||||||
"@angular/forms": "^19.2.0",
|
"@angular/forms": "^19.2.6",
|
||||||
"@angular/platform-browser": "^19.2.0",
|
"@angular/platform-browser": "^19.2.6",
|
||||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
"@angular/platform-browser-dynamic": "^19.2.6",
|
||||||
"@angular/router": "^19.2.0",
|
"@angular/router": "^19.2.6",
|
||||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@microsoft/signalr": "^6.0.21",
|
"@microsoft/signalr": "^8.0.7",
|
||||||
"bulma": "^0.9.4",
|
"bulma": "^1.0.3",
|
||||||
"curray": "^1.0.11",
|
"curray": "^1.0.12",
|
||||||
"file-saver-es": "^2.0.5",
|
"file-saver-es": "^2.0.5",
|
||||||
"ngx-filesize": "^3.0.5",
|
"filesize": "^10.1.6",
|
||||||
"rxjs": "~7.8.1",
|
"rxjs": "^7.8.2",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.8.1",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "^0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-eslint/builder": "19.1.0",
|
"@angular-eslint/builder": "19.3.0",
|
||||||
"@angular-eslint/eslint-plugin": "19.1.0",
|
"@angular-eslint/eslint-plugin": "19.3.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "19.1.0",
|
"@angular-eslint/eslint-plugin-template": "19.3.0",
|
||||||
"@angular-eslint/schematics": "19.1.0",
|
"@angular-eslint/schematics": "19.3.0",
|
||||||
"@angular-eslint/template-parser": "19.1.0",
|
"@angular-eslint/template-parser": "19.3.0",
|
||||||
"@angular/build": "^19.2.0",
|
"@angular/build": "^19.2.7",
|
||||||
"@angular/cli": "^19.2.0",
|
"@angular/cli": "^19.2.7",
|
||||||
"@angular/compiler-cli": "^19.2.0",
|
"@angular/compiler-cli": "^19.2.6",
|
||||||
"@angular/language-service": "^19.2.0",
|
"@angular/language-service": "^19.2.6",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/file-saver-es": "^2.0.1",
|
"@types/file-saver-es": "^2.0.3",
|
||||||
"@types/node": "^20.5.8",
|
"@types/node": "^22.14.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
||||||
"@typescript-eslint/parser": "^7.2.0",
|
"@typescript-eslint/parser": "^8.29.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^9.24.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.5.3",
|
||||||
"typescript": "5.7.3"
|
"typescript": "5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ import { Torrent, TorrentFileAvailability } from '../models/torrent.model';
|
||||||
import { SettingsService } from '../settings.service';
|
import { SettingsService } from '../settings.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-add-new-torrent',
|
selector: 'app-add-new-torrent',
|
||||||
templateUrl: './add-new-torrent.component.html',
|
templateUrl: './add-new-torrent.component.html',
|
||||||
styleUrls: ['./add-new-torrent.component.scss'],
|
styleUrls: ['./add-new-torrent.component.scss'],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class AddNewTorrentComponent implements OnInit {
|
export class AddNewTorrentComponent implements OnInit {
|
||||||
public fileName: string;
|
public fileName: string;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
template: '<router-outlet></router-outlet>',
|
template: '<router-outlet></router-outlet>',
|
||||||
styles: [],
|
styles: [],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class AppComponent {}
|
export class AppComponent {}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
|
import 'curray';
|
||||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { curray } from 'curray';
|
|
||||||
import { FileSizePipe, NgxFilesizeModule } from 'ngx-filesize';
|
|
||||||
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
|
import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.component';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
|
@ -23,8 +22,7 @@ import { TorrentStatusPipe } from './torrent-status.pipe';
|
||||||
import { TorrentTableComponent } from './torrent-table/torrent-table.component';
|
import { TorrentTableComponent } from './torrent-table/torrent-table.component';
|
||||||
import { TorrentComponent } from './torrent/torrent.component';
|
import { TorrentComponent } from './torrent/torrent.component';
|
||||||
import { SortPipe } from './sort.pipe';
|
import { SortPipe } from './sort.pipe';
|
||||||
|
import { FileSizePipe } from './filesize.pipe';
|
||||||
curray();
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
|
@ -43,9 +41,10 @@ curray();
|
||||||
ProfileComponent,
|
ProfileComponent,
|
||||||
Nl2BrPipe,
|
Nl2BrPipe,
|
||||||
SortPipe,
|
SortPipe,
|
||||||
|
FileSizePipe,
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
imports: [BrowserModule, AppRoutingModule, FormsModule, NgxFilesizeModule, ClipboardModule],
|
imports: [BrowserModule, AppRoutingModule, FormsModule, ClipboardModule],
|
||||||
providers: [
|
providers: [
|
||||||
FileSizePipe,
|
FileSizePipe,
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'decodeURI',
|
name: 'decodeURI',
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class DecodeURIPipe implements PipeTransform {
|
export class DecodeURIPipe implements PipeTransform {
|
||||||
transform(input: any) {
|
transform(input: any) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
import { FileSizePipe } from 'ngx-filesize';
|
|
||||||
import { Download } from './models/download.model';
|
import { Download } from './models/download.model';
|
||||||
|
import { FileSizePipe } from './filesize.pipe';
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'downloadStatus',
|
name: 'downloadStatus',
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class DownloadStatusPipe implements PipeTransform {
|
export class DownloadStatusPipe implements PipeTransform {
|
||||||
constructor(private pipe: FileSizePipe) {}
|
constructor(private pipe: FileSizePipe) {}
|
||||||
|
|
|
||||||
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';
|
import { AuthService } from '../auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrls: ['./login.component.scss'],
|
styleUrls: ['./login.component.scss'],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class LoginComponent {
|
export class LoginComponent {
|
||||||
public userName: string;
|
public userName: string;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-main-layout',
|
selector: 'app-main-layout',
|
||||||
templateUrl: './main-layout.component.html',
|
templateUrl: './main-layout.component.html',
|
||||||
styleUrls: ['./main-layout.component.scss'],
|
styleUrls: ['./main-layout.component.scss'],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class MainLayoutComponent {
|
export class MainLayoutComponent {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,6 @@ export class Profile {
|
||||||
public expiration: Date;
|
public expiration: Date;
|
||||||
public currentVersion: string;
|
public currentVersion: string;
|
||||||
public latestVersion: string;
|
public latestVersion: string;
|
||||||
|
public isInsecure: boolean;
|
||||||
|
public disableUpdateNotification: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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" routerLink="profile"> Profile </a>
|
||||||
<a class="navbar-item" (click)="logout()"> Logout </a>
|
<a class="navbar-item" (click)="logout()"> Logout </a>
|
||||||
<hr class="navbar-divider" />
|
<hr class="navbar-divider" />
|
||||||
<a href="https://github.com/rogerfar/rdt-client" target="_blank" class="navbar-item">Version 2.0.102</a>
|
<a href="https://github.com/rogerfar/rdt-client" target="_blank" class="navbar-item">Version {{ version }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div
|
<div
|
||||||
class="notification is-warning"
|
class="notification"
|
||||||
*ngIf="profile && profile.latestVersion && profile.currentVersion !== profile.latestVersion"
|
[ngClass]="profile.isInsecure ? 'is-danger' : 'is-warning'"
|
||||||
|
*ngIf="
|
||||||
|
profile &&
|
||||||
|
(!profile.disableUpdateNotification || profile.isInsecure) &&
|
||||||
|
profile.latestVersion &&
|
||||||
|
profile.currentVersion !== profile.latestVersion
|
||||||
|
"
|
||||||
>
|
>
|
||||||
Version {{ profile.latestVersion }} of RealDebrid Client was found. You are currently on version
|
<span *ngIf="profile.isInsecure"> Your current version is insecure. </span>
|
||||||
{{ profile.currentVersion }}.
|
<span>
|
||||||
|
Version {{ profile.latestVersion }} of RealDebrid Client was found. You are currently on version
|
||||||
|
{{ profile.currentVersion }}.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,33 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
import { Profile } from '../models/profile.model';
|
import { Profile } from '../models/profile.model';
|
||||||
import { SettingsService } from '../settings.service';
|
import { SettingsService } from '../settings.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-navbar',
|
||||||
templateUrl: './navbar.component.html',
|
templateUrl: './navbar.component.html',
|
||||||
styleUrls: ['./navbar.component.scss'],
|
styleUrls: ['./navbar.component.scss'],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class NavbarComponent implements OnInit {
|
export class NavbarComponent implements OnInit {
|
||||||
public showMobileMenu = false;
|
public showMobileMenu = false;
|
||||||
|
|
||||||
public profile: Profile;
|
public profile: Profile;
|
||||||
public providerLink: string;
|
public providerLink: string;
|
||||||
|
public version: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
) {}
|
) {
|
||||||
|
this.router.events.subscribe((event) => {
|
||||||
|
if (event instanceof NavigationEnd) {
|
||||||
|
this.showMobileMenu = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.settingsService.getProfile().subscribe((result) => {
|
this.settingsService.getProfile().subscribe((result) => {
|
||||||
|
|
@ -44,6 +51,10 @@ export class NavbarComponent implements OnInit {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.settingsService.getVersion().subscribe((result) => {
|
||||||
|
this.version = result.version;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public logout(): void {
|
public logout(): void {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { Pipe, PipeTransform, SecurityContext, VERSION } from '@angular/core';
|
||||||
import { DomSanitizer } from '@angular/platform-browser';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'nl2br',
|
name: 'nl2br',
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class Nl2BrPipe implements PipeTransform {
|
export class Nl2BrPipe implements PipeTransform {
|
||||||
constructor(private sanitizer: DomSanitizer) {}
|
constructor(private sanitizer: DomSanitizer) {}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import { Component } from '@angular/core';
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-profile',
|
selector: 'app-profile',
|
||||||
templateUrl: './profile.component.html',
|
templateUrl: './profile.component.html',
|
||||||
styleUrls: ['./profile.component.scss'],
|
styleUrls: ['./profile.component.scss'],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class ProfileComponent {
|
export class ProfileComponent {
|
||||||
constructor(private authService: AuthService) {}
|
constructor(private authService: AuthService) {}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Observable } from 'rxjs';
|
||||||
import { Profile } from './models/profile.model';
|
import { Profile } from './models/profile.model';
|
||||||
import { Setting } from './models/setting.model';
|
import { Setting } from './models/setting.model';
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
|
import { Version } from './models/version.model';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
|
|
@ -26,6 +27,10 @@ export class SettingsService {
|
||||||
return this.http.get<Profile>(`${this.baseHref}Api/Settings/Profile`);
|
return this.http.get<Profile>(`${this.baseHref}Api/Settings/Profile`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getVersion(): Observable<Version> {
|
||||||
|
return this.http.get<Version>(`${this.baseHref}Api/Settings/Version`);
|
||||||
|
}
|
||||||
|
|
||||||
public testPath(path: string): Observable<void> {
|
public testPath(path: string): Observable<void> {
|
||||||
return this.http.post<void>(`${this.baseHref}Api/Settings/TestPath`, { path });
|
return this.http.post<void>(`${this.baseHref}Api/Settings/TestPath`, { path });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ import { SettingsService } from 'src/app/settings.service';
|
||||||
import { Setting } from '../models/setting.model';
|
import { Setting } from '../models/setting.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
templateUrl: './settings.component.html',
|
templateUrl: './settings.component.html',
|
||||||
styleUrls: ['./settings.component.scss'],
|
styleUrls: ['./settings.component.scss'],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class SettingsComponent implements OnInit {
|
export class SettingsComponent implements OnInit {
|
||||||
public activeTab = 0;
|
public activeTab = 0;
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ import { Router } from '@angular/router';
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-setup',
|
selector: 'app-setup',
|
||||||
templateUrl: './setup.component.html',
|
templateUrl: './setup.component.html',
|
||||||
styleUrls: ['./setup.component.scss'],
|
styleUrls: ['./setup.component.scss'],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class SetupComponent {
|
export class SetupComponent {
|
||||||
public userName: string;
|
public userName: string;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'sort',
|
name: 'sort',
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class SortPipe implements PipeTransform {
|
export class SortPipe implements PipeTransform {
|
||||||
transform(array: any[], field: string, order: 'asc' | 'desc' = 'asc'): any[] {
|
transform(array: any[], field: string, order: 'asc' | 'desc' = 'asc'): any[] {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
import { FileSizePipe } from 'ngx-filesize';
|
|
||||||
import { RealDebridStatus, Torrent } from './models/torrent.model';
|
import { RealDebridStatus, Torrent } from './models/torrent.model';
|
||||||
|
import { FileSizePipe } from './filesize.pipe';
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'status',
|
name: 'status',
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class TorrentStatusPipe implements PipeTransform {
|
export class TorrentStatusPipe implements PipeTransform {
|
||||||
constructor(private pipe: FileSizePipe) {}
|
constructor(private pipe: FileSizePipe) {}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<th>
|
<th>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
(click)="toggleSelectAll($event)"
|
(click)="toggleDeleteSelectAll($event)"
|
||||||
[checked]="selectedTorrents.length > 0 && selectedTorrents.length === torrents.length"
|
[checked]="selectedTorrents.length > 0 && selectedTorrents.length === torrents.length"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
[checked]="selectedTorrents.contains(torrent.torrentId)"
|
[checked]="selectedTorrents.contains(torrent.torrentId)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td (click)="openTorrent(torrent.torrentId)">
|
<td (click)="openTorrent(torrent.torrentId)" class="break-all">
|
||||||
{{ torrent.rdName }}
|
{{ torrent.rdName }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -107,19 +107,25 @@
|
||||||
<label class="label"></label>
|
<label class="label"></label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="deleteData" />
|
<input type="checkbox" [(ngModel)]="deleteData" (change)="updateDeleteSelectAll()" />
|
||||||
Delete Torrents from client
|
Delete Torrents from client
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
|
<input type="checkbox" [(ngModel)]="deleteRdTorrent" (change)="updateDeleteSelectAll()" />
|
||||||
Delete Torrents from provider
|
Delete Torrents from provider
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="deleteLocalFiles" />
|
<input type="checkbox" [(ngModel)]="deleteLocalFiles" (change)="updateDeleteSelectAll()" />
|
||||||
Delete local files
|
Delete local files
|
||||||
</label>
|
</label>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" [(ngModel)]="deleteSelectAll" (change)="toggleDeleteSelectAllOptions()" />
|
||||||
|
Select All
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="notification is-primary">
|
<div class="notification is-primary">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
table {
|
table {
|
||||||
tr {
|
tr {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
td.break-all {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ import { TorrentService } from '../torrent.service';
|
||||||
import { forkJoin, Observable } from 'rxjs';
|
import { forkJoin, Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-torrent-table',
|
selector: 'app-torrent-table',
|
||||||
templateUrl: './torrent-table.component.html',
|
templateUrl: './torrent-table.component.html',
|
||||||
styleUrls: ['./torrent-table.component.scss'],
|
styleUrls: ['./torrent-table.component.scss'],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class TorrentTableComponent implements OnInit {
|
export class TorrentTableComponent implements OnInit {
|
||||||
public torrents: Torrent[] = [];
|
public torrents: Torrent[] = [];
|
||||||
|
|
@ -20,6 +20,7 @@ export class TorrentTableComponent implements OnInit {
|
||||||
public isDeleteModalActive: boolean;
|
public isDeleteModalActive: boolean;
|
||||||
public deleteError: string;
|
public deleteError: string;
|
||||||
public deleting: boolean;
|
public deleting: boolean;
|
||||||
|
public deleteSelectAll: boolean;
|
||||||
public deleteData: boolean;
|
public deleteData: boolean;
|
||||||
public deleteRdTorrent: boolean;
|
public deleteRdTorrent: boolean;
|
||||||
public deleteLocalFiles: boolean;
|
public deleteLocalFiles: boolean;
|
||||||
|
|
@ -74,7 +75,7 @@ export class TorrentTableComponent implements OnInit {
|
||||||
return el.torrentId;
|
return el.torrentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleSelectAll(event: any) {
|
public toggleDeleteSelectAll(event: any) {
|
||||||
this.selectedTorrents = [];
|
this.selectedTorrents = [];
|
||||||
|
|
||||||
if (event.target.checked) {
|
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 style="width: 35px"></td>
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
<div style="flex: 1 1 0;">
|
<div style="flex: 1 1 0">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button is-primary" (click)="showDownloadRetryModal(download.downloadId)">
|
<button class="button is-primary" (click)="showDownloadRetryModal(download.downloadId)">
|
||||||
|
|
@ -285,7 +285,7 @@
|
||||||
{{ download.retryCount }} / {{ torrent.downloadRetryAttempts }}
|
{{ download.retryCount }} / {{ torrent.downloadRetryAttempts }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1 1 0;">
|
<div style="flex: 1 1 0">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Added</label>
|
<label class="label">Added</label>
|
||||||
<ng-container *ngIf="download.added">
|
<ng-container *ngIf="download.added">
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ import { Torrent } from '../models/torrent.model';
|
||||||
import { TorrentService } from '../torrent.service';
|
import { TorrentService } from '../torrent.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-torrent',
|
selector: 'app-torrent',
|
||||||
templateUrl: './torrent.component.html',
|
templateUrl: './torrent.component.html',
|
||||||
styleUrls: ['./torrent.component.scss'],
|
styleUrls: ['./torrent.component.scss'],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class TorrentComponent implements OnInit {
|
export class TorrentComponent implements OnInit {
|
||||||
public torrent: Torrent;
|
public torrent: Torrent;
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
@forward "../node_modules/bulma/bulma.sass";
|
@forward "../node_modules/bulma/bulma.scss";
|
||||||
@forward "../node_modules/@fortawesome/fontawesome-free/css/all.css";
|
@forward "../node_modules/@fortawesome/fontawesome-free/css/all.css";
|
||||||
|
|
|
||||||
4611
package-lock.json
generated
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 Microsoft.EntityFrameworkCore;
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
using Download = RdtClient.Data.Models.Data.Download;
|
using Download = RdtClient.Data.Models.Data.Download;
|
||||||
|
|
||||||
namespace RdtClient.Data.Data;
|
namespace RdtClient.Data.Data;
|
||||||
|
|
@ -29,13 +30,14 @@ public class DownloadData(DataContext dataContext)
|
||||||
.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
|
.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
|
var download = new Download
|
||||||
{
|
{
|
||||||
DownloadId = Guid.NewGuid(),
|
DownloadId = Guid.NewGuid(),
|
||||||
TorrentId = torrentId,
|
TorrentId = torrentId,
|
||||||
Path = path,
|
FileName = downloadInfo.FileName,
|
||||||
|
Path = downloadInfo.RestrictedLink,
|
||||||
Added = DateTimeOffset.UtcNow,
|
Added = DateTimeOffset.UtcNow,
|
||||||
DownloadQueued = DateTimeOffset.UtcNow,
|
DownloadQueued = DateTimeOffset.UtcNow,
|
||||||
RetryCount = 0
|
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")]
|
||||||
|
|
@ -42,3 +42,19 @@ public class Download
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public Int64 Speed { get; set; }
|
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")]
|
[DisplayName("Copy added torrent files")]
|
||||||
[Description("When a torrent file or magnet is added, create a copy in this directory.")]
|
[Description("When a torrent file or magnet is added, create a copy in this directory.")]
|
||||||
public String? CopyAddedTorrents { get; set; } = null;
|
public String? CopyAddedTorrents { get; set; } = null;
|
||||||
|
|
||||||
|
[DisplayName("Disable update notifications")]
|
||||||
|
[Description("Ignore update notifications. You will still be notified if the version you are running has a security vulnerability.")]
|
||||||
|
public Boolean DisableUpdateNotifications { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DbSettingsDownloadClient
|
public class DbSettingsDownloadClient
|
||||||
|
|
@ -111,8 +115,8 @@ public class DbSettingsDownloadClient
|
||||||
[Description("Timeout in milliseconds before the downloader times out.")]
|
[Description("Timeout in milliseconds before the downloader times out.")]
|
||||||
public Int32 Timeout { get; set; } = 5000;
|
public Int32 Timeout { get; set; } = 5000;
|
||||||
|
|
||||||
[DisplayName("Proxy Server (only used for the Internal Downloader)")]
|
[DisplayName("Proxy Server (only used for the Bezzad Downloader)")]
|
||||||
[Description("Address of a proxy server to download through (only used for the Internal Downloader).")]
|
[Description("Address of a proxy server to download through (only used for the Bezzad Downloader).")]
|
||||||
public String? ProxyServer { get; set; } = null;
|
public String? ProxyServer { get; set; } = null;
|
||||||
|
|
||||||
[DisplayName("Aria2c URL (only used for the Aria2c Downloader)")]
|
[DisplayName("Aria2c URL (only used for the Aria2c Downloader)")]
|
||||||
|
|
@ -177,6 +181,13 @@ or
|
||||||
<a href=""https://debrid-link.com/webapp/apikey"" target=""_blank"" rel=""noopener"">https://debrid-link.com/webapp/apikey</a>")]
|
<a href=""https://debrid-link.com/webapp/apikey"" target=""_blank"" rel=""noopener"">https://debrid-link.com/webapp/apikey</a>")]
|
||||||
public String ApiKey { get; set; } = "";
|
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")]
|
[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.")]
|
[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;
|
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.")]
|
[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;
|
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")]
|
[DisplayName("Auto Import Defaults")]
|
||||||
public DbSettingsDefaultsWithCategory Default { get; set; } = new();
|
public DbSettingsDefaultsWithCategory Default { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,6 @@ public class Profile
|
||||||
public DateTimeOffset? Expiration { get; set; }
|
public DateTimeOffset? Expiration { get; set; }
|
||||||
public String? CurrentVersion { get; set; }
|
public String? CurrentVersion { get; set; }
|
||||||
public String? LatestVersion { get; set; }
|
public String? LatestVersion { get; set; }
|
||||||
|
public Boolean? IsInsecure { get; set; }
|
||||||
|
public Boolean? DisableUpdateNotification { get; set; }
|
||||||
}
|
}
|
||||||
|
|
@ -8,11 +8,11 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,16 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AllDebrid.NET" Version="1.0.17" />
|
<PackageReference Include="AllDebrid.NET" Version="1.0.18" />
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.11" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.13" />
|
||||||
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.0.11" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.11" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.13" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|
|
||||||
|
|
@ -405,7 +405,7 @@ public class AllDebridTorrentClientTest
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetDownloadLinks_WhenTorrentRdIdNull_ReturnsNull()
|
public async Task GetDownloadInfos_WhenTorrentRdIdNull_ReturnsNull()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var mocks = new Mocks();
|
var mocks = new Mocks();
|
||||||
|
|
@ -418,7 +418,7 @@ public class AllDebridTorrentClientTest
|
||||||
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
|
var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
|
|
@ -426,7 +426,7 @@ public class AllDebridTorrentClientTest
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetDownloadLinks_UsesFileFilter()
|
public async Task GetDownloadInfos_UsesFileFilter()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var mocks = new Mocks();
|
var mocks = new Mocks();
|
||||||
|
|
@ -467,16 +467,16 @@ public class AllDebridTorrentClientTest
|
||||||
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
|
var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.Single(result);
|
Assert.Single(result);
|
||||||
Assert.Contains("https://fake.url/file1.txt", result);
|
Assert.Single(result, info => info.RestrictedLink == "https://fake.url/file1.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetDownloadLinks_WhenAllFilesExcluded_ReturnsAllFiles()
|
public async Task GetDownloadInfos_WhenAllFilesExcluded_ReturnsEmptyList()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var mocks = new Mocks();
|
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.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);
|
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);
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
|
var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
var linksSet = new HashSet<String>(result);
|
Assert.Empty(result);
|
||||||
Assert.Equal(expectedLinksSet, linksSet);
|
|
||||||
|
|
||||||
mocks.FileFilterMock.Verify(f => f.IsDownloadable(torrent, "file-1.txt", 100));
|
mocks.FileFilterMock.Verify(f => f.IsDownloadable(torrent, "file-1.txt", 100));
|
||||||
mocks.FileFilterMock.Verify(f => f.IsDownloadable(torrent, "file-2.txt", 100));
|
mocks.FileFilterMock.Verify(f => f.IsDownloadable(torrent, "file-2.txt", 100));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using RdtClient.Data.Enums;
|
||||||
using RdtClient.Service.Services;
|
using RdtClient.Service.Services;
|
||||||
|
|
||||||
namespace RdtClient.Service.BackgroundServices;
|
namespace RdtClient.Service.BackgroundServices;
|
||||||
|
|
@ -27,7 +28,7 @@ public class ProviderUpdater(ILogger<TaskRunner> logger, IServiceProvider servic
|
||||||
{
|
{
|
||||||
var torrents = await torrentService.Get();
|
var torrents = await torrentService.Get();
|
||||||
|
|
||||||
if (_nextUpdate < DateTime.UtcNow && ((torrents.Count > 0 && !Settings.Get.Provider.AutoImport) || Settings.Get.Provider.AutoImport))
|
if (_nextUpdate < DateTime.UtcNow && (Settings.Get.Provider.AutoImport || torrents.Any(t => t.RdStatus != TorrentStatus.Finished)))
|
||||||
{
|
{
|
||||||
logger.LogDebug($"Updating torrent info from debrid provider");
|
logger.LogDebug($"Updating torrent info from debrid provider");
|
||||||
|
|
||||||
|
|
@ -52,12 +53,12 @@ public class ProviderUpdater(ILogger<TaskRunner> logger, IServiceProvider servic
|
||||||
|
|
||||||
await torrentService.UpdateRdData();
|
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)
|
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);
|
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ public class Startup(IServiceProvider serviceProvider) : IHostedService
|
||||||
using var scope = serviceProvider.CreateScope();
|
using var scope = serviceProvider.CreateScope();
|
||||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Startup>>();
|
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>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
|
||||||
public static String? CurrentVersion { get; private set; }
|
public static String? CurrentVersion { get; private set; }
|
||||||
public static String? LatestVersion { get; private set; }
|
public static String? LatestVersion { get; private set; }
|
||||||
|
|
||||||
|
public static Boolean? IsInsecure { get; private set; }
|
||||||
|
|
||||||
|
private static readonly List<String> KnownGhsaIds = [
|
||||||
|
];
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
while (!Startup.Ready)
|
while (!Startup.Ready)
|
||||||
|
|
@ -34,18 +39,9 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var httpClient = new HttpClient();
|
var gitHubReleases = await GitHubRequest<List<GitHubReleasesResponse>>("/repos/rogerfar/rdt-client/tags?per_page=1", stoppingToken);
|
||||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new("RdtClient", CurrentVersion));
|
|
||||||
var response = await httpClient.GetStringAsync($"https://api.github.com/repos/rogerfar/rdt-client/tags?per_page=1", stoppingToken);
|
|
||||||
|
|
||||||
var gitHubReleases = JsonConvert.DeserializeObject<List<GitHubReleasesResponse>>(response);
|
var latestRelease = gitHubReleases?.FirstOrDefault(m => m.Name != null)?.Name;
|
||||||
|
|
||||||
if (gitHubReleases == null || gitHubReleases.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var latestRelease = gitHubReleases.FirstOrDefault(m => m.Name != null)?.Name;
|
|
||||||
|
|
||||||
if (latestRelease == null)
|
if (latestRelease == null)
|
||||||
{
|
{
|
||||||
|
|
@ -59,6 +55,18 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
|
||||||
}
|
}
|
||||||
|
|
||||||
LatestVersion = latestRelease;
|
LatestVersion = latestRelease;
|
||||||
|
|
||||||
|
var gitHubSecurityAdvisories = await GitHubRequest<List<GitHubSecurityAdvisoriesResponse>>("/repos/rogerfar/rdt-client/security-advisories", stoppingToken);
|
||||||
|
|
||||||
|
var unseenGhsaIds = gitHubSecurityAdvisories?.Where(advisory => !KnownGhsaIds.Contains(advisory.GhsaId));
|
||||||
|
|
||||||
|
if (unseenGhsaIds == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"Unable to find security advisories on GitHub");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsInsecure = unseenGhsaIds.Any();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -70,6 +78,15 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
|
||||||
|
|
||||||
logger.LogInformation("UpdateChecker stopped.");
|
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
|
public class GitHubReleasesResponse
|
||||||
|
|
@ -77,3 +94,9 @@ public class GitHubReleasesResponse
|
||||||
[JsonProperty("name")]
|
[JsonProperty("name")]
|
||||||
public String? Name { get; set; }
|
public String? Name { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class GitHubSecurityAdvisoriesResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("ghsa_id")]
|
||||||
|
public required String GhsaId { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -6,3 +6,4 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
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("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>
|
<ItemGroup>
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
<PackageReference Include="AllDebrid.NET" Version="1.0.17" />
|
<PackageReference Include="AllDebrid.NET" Version="1.0.18" />
|
||||||
<PackageReference Include="Aria2.NET" Version="1.0.5" />
|
<PackageReference Include="Aria2.NET" Version="1.0.6" />
|
||||||
<PackageReference Include="DebridLinkFr.NET" Version="1.0.4" />
|
<PackageReference Include="DebridLinkFr.NET" Version="1.0.4" />
|
||||||
<PackageReference Include="Downloader" Version="3.3.3" />
|
<PackageReference Include="Downloader" Version="3.3.4" />
|
||||||
<PackageReference Include="Downloader.NET" Version="1.0.14" />
|
<PackageReference Include="Downloader.NET" Version="1.0.15" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.2.0" />
|
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.4.0" />
|
||||||
<PackageReference Include="MonoTorrent" Version="3.0.2" />
|
<PackageReference Include="MonoTorrent" Version="3.0.2" />
|
||||||
<PackageReference Include="Polly" Version="8.5.2" />
|
<PackageReference Include="Polly" Version="8.5.2" />
|
||||||
<PackageReference Include="Premiumize.NET" Version="1.0.9" />
|
<PackageReference Include="Premiumize.NET" Version="1.0.10" />
|
||||||
<PackageReference Include="RD.NET" Version="2.1.7" />
|
<PackageReference Include="RD.NET" Version="2.1.11" />
|
||||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
||||||
<PackageReference Include="Synology.Api.Client" Version="0.3.91" />
|
<PackageReference Include="Synology.Api.Client" Version="0.3.93" />
|
||||||
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.11" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.13" />
|
||||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.11" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.13" />
|
||||||
<PackageReference Include="TorBox.NET" Version="1.5.0" />
|
<PackageReference Include="TorBox.NET" Version="1.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using RdtClient.Data.Data;
|
using RdtClient.Data.Data;
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
using Download = RdtClient.Data.Models.Data.Download;
|
using Download = RdtClient.Data.Models.Data.Download;
|
||||||
|
|
||||||
namespace RdtClient.Service.Services;
|
namespace RdtClient.Service.Services;
|
||||||
|
|
@ -20,9 +21,9 @@ public class Downloads(DownloadData downloadData) : IDownloads
|
||||||
return await downloadData.Get(torrentId, path);
|
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)
|
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ public interface IDownloads
|
||||||
Task<List<Download>> GetForTorrent(Guid torrentId);
|
Task<List<Download>> GetForTorrent(Guid torrentId);
|
||||||
Task<Download?> GetById(Guid downloadId);
|
Task<Download?> GetById(Guid downloadId);
|
||||||
Task<Download?> Get(Guid torrentId, String path);
|
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 UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink);
|
||||||
Task UpdateFileName(Guid downloadId, String fileName);
|
Task UpdateFileName(Guid downloadId, String fileName);
|
||||||
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);
|
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);
|
||||||
|
|
|
||||||
|
|
@ -411,11 +411,11 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
||||||
{
|
{
|
||||||
if (deleteFiles)
|
if (deleteFiles)
|
||||||
{
|
{
|
||||||
logger.LogDebug($"Delete {hash}, with files");
|
logger.LogDebug("Delete {hash}, with files", hash);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.LogDebug($"Delete {hash}, no files");
|
logger.LogDebug("Delete {hash}, no files", hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
var torrent = await torrents.GetByHash(hash);
|
var torrent = await torrents.GetByHash(hash);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
using AllDebridNET;
|
using System.Diagnostics;
|
||||||
|
using AllDebridNET;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RdtClient.Data.Enums;
|
using RdtClient.Data.Enums;
|
||||||
using RdtClient.Data.Models.TorrentClient;
|
using RdtClient.Data.Models.TorrentClient;
|
||||||
using RdtClient.Service.Helpers;
|
using RdtClient.Service.Helpers;
|
||||||
using System.Web;
|
|
||||||
using RdtClient.Data.Models.Data;
|
using RdtClient.Data.Models.Data;
|
||||||
using File = AllDebridNET.File;
|
using File = AllDebridNET.File;
|
||||||
using Torrent = RdtClient.Data.Models.Data.Torrent;
|
using Torrent = RdtClient.Data.Models.Data.Torrent;
|
||||||
|
|
@ -155,9 +155,10 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
|
||||||
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
|
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)
|
public async Task Delete(String torrentId)
|
||||||
|
|
@ -198,7 +199,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
|
||||||
torrent.RdSize = torrentClientTorrent.Bytes;
|
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);
|
torrent.RdFiles = JsonConvert.SerializeObject(torrentClientTorrent.Files);
|
||||||
}
|
}
|
||||||
|
|
@ -245,48 +246,33 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
|
||||||
return torrent;
|
return torrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<String>?> GetDownloadLinks(Torrent torrent)
|
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent)
|
||||||
{
|
{
|
||||||
if (torrent.RdId == null)
|
if (torrent.RdId == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log($"Getting download links", torrent);
|
||||||
|
|
||||||
var allFiles = await allDebridNetClientFactory.GetClient().Magnet.FilesAsync(Int64.Parse(torrent.RdId));
|
var allFiles = await allDebridNetClientFactory.GetClient().Magnet.FilesAsync(Int64.Parse(torrent.RdId));
|
||||||
|
|
||||||
var files = GetFiles(allFiles);
|
var files = GetFiles(allFiles);
|
||||||
|
|
||||||
files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList();
|
return files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes) && f.DownloadLink != null).Select(f => new DownloadInfo
|
||||||
|
|
||||||
Log($"Getting download links", torrent);
|
|
||||||
|
|
||||||
if (files.Count == 0)
|
|
||||||
{
|
{
|
||||||
Log($"Filtered all files out! Downloading ALL files instead!", torrent);
|
FileName = Path.GetFileName(f.Path),
|
||||||
|
RestrictedLink = f.DownloadLink!
|
||||||
files = GetFiles(allFiles);
|
}).ToList();
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<String> GetFileName(String downloadUrl)
|
/// <inheritdoc />
|
||||||
|
public Task<String> GetFileName(Download download)
|
||||||
{
|
{
|
||||||
if (String.IsNullOrWhiteSpace(downloadUrl))
|
// FileName is set in GetDownlaadInfos
|
||||||
{
|
Debug.Assert(download.FileName != null);
|
||||||
return Task.FromResult("");
|
|
||||||
}
|
|
||||||
|
|
||||||
var uri = new Uri(downloadUrl);
|
return Task.FromResult(download.FileName);
|
||||||
|
|
||||||
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TorrentClientTorrent> GetInfo(String torrentId)
|
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 Newtonsoft.Json;
|
||||||
using DebridLinkFrNET;
|
using DebridLinkFrNET;
|
||||||
using RdtClient.Data.Enums;
|
using RdtClient.Data.Enums;
|
||||||
using RdtClient.Data.Models.TorrentClient;
|
using RdtClient.Data.Models.TorrentClient;
|
||||||
using RdtClient.Service.Helpers;
|
using RdtClient.Service.Helpers;
|
||||||
using DebridLinkFrNET.Models;
|
using RdtClient.Data.Models.Data;
|
||||||
using System.Web;
|
|
||||||
using Download = RdtClient.Data.Models.Data.Download;
|
using Download = RdtClient.Data.Models.Data.Download;
|
||||||
|
using Torrent = DebridLinkFrNET.Models.Torrent;
|
||||||
|
|
||||||
namespace RdtClient.Service.Services.TorrentClients;
|
namespace RdtClient.Service.Services.TorrentClients;
|
||||||
|
|
||||||
|
|
@ -136,9 +137,10 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
|
||||||
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
|
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)
|
public async Task Delete(String torrentId)
|
||||||
|
|
@ -234,7 +236,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
|
||||||
return torrent;
|
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)
|
if (torrent.RdId == null)
|
||||||
{
|
{
|
||||||
|
|
@ -248,19 +250,14 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadLinks = rdTorrent.Files?
|
return rdTorrent.Files?
|
||||||
.Where(m => fileFilter.IsDownloadable(torrent, m.Path, m.Bytes) && m.DownloadLink != null)
|
.Where(m => fileFilter.IsDownloadable(torrent, m.Path, m.Bytes) && m.DownloadLink != null)
|
||||||
.Select(m => m.DownloadLink!)
|
.Select(m => new DownloadInfo
|
||||||
.ToList() ?? [];
|
{
|
||||||
|
RestrictedLink = m.DownloadLink!,
|
||||||
Log($"Found {downloadLinks.Count} links", torrent);
|
FileName = Path.GetFileName(m.Path)
|
||||||
|
})
|
||||||
foreach (var link in downloadLinks)
|
.ToList();
|
||||||
{
|
|
||||||
Log($"{link}", torrent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return downloadLinks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TorrentClientTorrent> GetInfo(String torrentId)
|
private async Task<TorrentClientTorrent> GetInfo(String torrentId)
|
||||||
|
|
@ -280,16 +277,13 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
|
||||||
logger.LogDebug(message);
|
logger.LogDebug(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<String> GetFileName(String downloadUrl)
|
/// <inheritdoc />
|
||||||
|
public Task<String> GetFileName(Download download)
|
||||||
{
|
{
|
||||||
if (String.IsNullOrWhiteSpace(downloadUrl))
|
// FileName is set in GetDownlaadInfos
|
||||||
{
|
Debug.Assert(download.FileName != null);
|
||||||
return Task.FromResult("");
|
|
||||||
}
|
|
||||||
|
|
||||||
var uri = new Uri(downloadUrl);
|
return Task.FromResult(download.FileName);
|
||||||
|
|
||||||
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String? GetSymlinkPath(Data.Models.Data.Torrent torrent, Download download)
|
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> AddMagnet(String magnetLink);
|
||||||
Task<String> AddFile(Byte[] bytes);
|
Task<String> AddFile(Byte[] bytes);
|
||||||
Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash);
|
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 Delete(String torrentId);
|
||||||
Task<String> Unrestrict(String link);
|
Task<String> Unrestrict(String link);
|
||||||
Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent);
|
Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent);
|
||||||
Task<IList<String>?> GetDownloadLinks(Torrent torrent);
|
Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent);
|
||||||
Task<String> GetFileName(String downloadUrl);
|
/// <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 Newtonsoft.Json;
|
||||||
using PremiumizeNET;
|
using PremiumizeNET;
|
||||||
using RdtClient.Data.Enums;
|
using RdtClient.Data.Enums;
|
||||||
using RdtClient.Data.Models.TorrentClient;
|
using RdtClient.Data.Models.TorrentClient;
|
||||||
using RdtClient.Service.Helpers;
|
using RdtClient.Service.Helpers;
|
||||||
using System.Web;
|
using RdtClient.Data.Models.Data;
|
||||||
using Torrent = RdtClient.Data.Models.Data.Torrent;
|
using Torrent = RdtClient.Data.Models.Data.Torrent;
|
||||||
|
|
||||||
namespace RdtClient.Service.Services.TorrentClients;
|
namespace RdtClient.Service.Services.TorrentClients;
|
||||||
|
|
@ -121,9 +122,12 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
|
||||||
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
|
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)
|
public async Task Delete(String id)
|
||||||
|
|
@ -197,7 +201,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
|
||||||
return torrent;
|
return torrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<String>?> GetDownloadLinks(Torrent torrent)
|
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent)
|
||||||
{
|
{
|
||||||
if (torrent.RdId == null)
|
if (torrent.RdId == null)
|
||||||
{
|
{
|
||||||
|
|
@ -212,7 +216,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
|
||||||
|
|
||||||
Log($"Found transfer {transfer.Name} ({transfer.Id})", torrent);
|
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))
|
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);
|
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);
|
Log($"Found {info.RestrictedLink}", torrent);
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var link in downloadLinks)
|
return downloadInfos;
|
||||||
{
|
|
||||||
Log($"Found {link}", torrent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return downloadLinks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<String> GetFileName(String downloadUrl)
|
/// <inheritdoc />
|
||||||
|
public Task<String> GetFileName(Download download)
|
||||||
{
|
{
|
||||||
if (String.IsNullOrWhiteSpace(downloadUrl))
|
// FileName is set in GetDownlaadInfos
|
||||||
{
|
Debug.Assert(download.FileName != null);
|
||||||
return Task.FromResult("");
|
|
||||||
}
|
|
||||||
|
|
||||||
var uri = new Uri(downloadUrl);
|
return Task.FromResult(download.FileName);
|
||||||
|
|
||||||
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TorrentClientTorrent> GetInfo(String id)
|
private async Task<TorrentClientTorrent> GetInfo(String id)
|
||||||
|
|
@ -263,13 +257,12 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
|
||||||
return Map(result);
|
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))
|
if (String.IsNullOrWhiteSpace(folderId))
|
||||||
{
|
{
|
||||||
return downloadLinks;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var folder = await GetClient().Folder.ListAsync(folderId);
|
var folder = await GetClient().Folder.ListAsync(folderId);
|
||||||
|
|
@ -277,11 +270,13 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
|
||||||
if (folder.Content == null)
|
if (folder.Content == null)
|
||||||
{
|
{
|
||||||
Log($"Found no items in folder {folder.Name} ({folderId})", torrent);
|
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);
|
Log($"Found {folder.Content.Count} items in folder {folder.Name} ({folderId})", torrent);
|
||||||
|
|
||||||
|
var downloadInfos = new List<DownloadInfo>();
|
||||||
|
|
||||||
foreach (var item in folder.Content)
|
foreach (var item in folder.Content)
|
||||||
{
|
{
|
||||||
if (item.Type == "file")
|
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);
|
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")
|
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);
|
Log($"Found subfolder {item.Name} in {folder.Name} ({folderId}), searching subfolder for files", torrent);
|
||||||
var subDownloadLinks = await GetAllDownloadLinks(torrent, item.Id);
|
var subDownloadLinks = await GetAllDownloadInfos(torrent, item.Id);
|
||||||
downloadLinks.AddRange(subDownloadLinks);
|
downloadInfos.AddRange(subDownloadLinks);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -320,7 +315,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return downloadLinks;
|
return downloadInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Log(String message, Torrent? torrent = null)
|
private void Log(String message, Torrent? torrent = null)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@ using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RDNET;
|
using RDNET;
|
||||||
using RdtClient.Data.Enums;
|
using RdtClient.Data.Enums;
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
using RdtClient.Data.Models.TorrentClient;
|
using RdtClient.Data.Models.TorrentClient;
|
||||||
using RdtClient.Service.Helpers;
|
using RdtClient.Service.Helpers;
|
||||||
|
using Download = RdtClient.Data.Models.Data.Download;
|
||||||
|
using Torrent = RDNET.Torrent;
|
||||||
|
|
||||||
namespace RdtClient.Service.Services.TorrentClients;
|
namespace RdtClient.Service.Services.TorrentClients;
|
||||||
|
|
||||||
|
|
@ -26,7 +29,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
||||||
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
|
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
|
||||||
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
|
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);
|
rdtNetClient.UseApiAuthentication(apiKey);
|
||||||
|
|
||||||
// Get the server time to fix up the timezones on results
|
// 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>>([]);
|
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);
|
Log("Seleting files", torrent);
|
||||||
|
|
||||||
|
|
@ -155,33 +159,25 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log("Selecting all files", torrent);
|
Log("Selecting files", torrent);
|
||||||
files = [.. torrent.Files];
|
files = [.. torrent.Files];
|
||||||
}
|
}
|
||||||
|
|
||||||
Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
|
|
||||||
|
|
||||||
files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList();
|
files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList();
|
||||||
|
|
||||||
if (files.Count == 0)
|
Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
|
||||||
{
|
|
||||||
Log($"Filtered all files out! Downloading ALL files instead!", torrent);
|
|
||||||
|
|
||||||
files = torrent.Files;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileIds = files.Select(m => m.Id.ToString()).ToArray();
|
var fileIds = files.Select(m => m.Id.ToString()).ToArray();
|
||||||
|
|
||||||
Log($"Selecting files:");
|
if (fileIds.Length == 0)
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
{
|
||||||
Log($"{file.Id}: {file.Path} ({file.Bytes}b)");
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log("", torrent);
|
|
||||||
|
|
||||||
await GetClient().Torrents.SelectFilesAsync(torrent.RdId!, [.. fileIds]);
|
await GetClient().Torrents.SelectFilesAsync(torrent.RdId!, [.. fileIds]);
|
||||||
|
|
||||||
|
return fileIds.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Delete(String torrentId)
|
public async Task Delete(String torrentId)
|
||||||
|
|
@ -280,7 +276,7 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
||||||
return torrent;
|
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)
|
if (torrent.RdId == null)
|
||||||
{
|
{
|
||||||
|
|
@ -294,7 +290,14 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
||||||
return null;
|
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);
|
Log($"Found {downloadLinks.Count} links", torrent);
|
||||||
|
|
||||||
|
|
@ -341,14 +344,15 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
|
||||||
return null;
|
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("");
|
return Task.FromResult("");
|
||||||
}
|
}
|
||||||
|
|
||||||
var uri = new Uri(downloadUrl);
|
var uri = new Uri(download.Link);
|
||||||
|
|
||||||
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
|
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Diagnostics;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using TorBoxNET;
|
using TorBoxNET;
|
||||||
using RdtClient.Data.Enums;
|
using RdtClient.Data.Enums;
|
||||||
using RdtClient.Data.Models.TorrentClient;
|
using RdtClient.Data.Models.TorrentClient;
|
||||||
using System.Web;
|
|
||||||
using RdtClient.Data.Models.Data;
|
using RdtClient.Data.Models.Data;
|
||||||
using RdtClient.Service.Helpers;
|
using RdtClient.Service.Helpers;
|
||||||
|
|
||||||
|
|
@ -24,7 +25,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpClient = httpClientFactory.CreateClient();
|
var httpClient = httpClientFactory.CreateClient();
|
||||||
httpClient.DefaultRequestHeaders.Add("User-Agent", "rdt-client");
|
httpClient.DefaultRequestHeaders.Add("User-Agent", $"rdt-client {Assembly.GetEntryAssembly()?.GetName().Version}");
|
||||||
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
|
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
|
||||||
|
|
||||||
var torBoxNetClient = new TorBoxNetClient(null, httpClient, 5);
|
var torBoxNetClient = new TorBoxNetClient(null, httpClient, 5);
|
||||||
|
|
@ -132,10 +133,8 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
||||||
var magnetLinkInfo = MonoTorrent.MagnetLink.Parse(magnetLink);
|
var magnetLinkInfo = MonoTorrent.MagnetLink.Parse(magnetLink);
|
||||||
return magnetLinkInfo.InfoHashes.V1!.ToHex().ToLowerInvariant();
|
return magnetLinkInfo.InfoHashes.V1!.ToHex().ToLowerInvariant();
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
return result.Data!.Hash!;
|
||||||
return result.Data!.Hash!;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<String> AddFile(Byte[] bytes)
|
public async Task<String> AddFile(Byte[] bytes)
|
||||||
|
|
@ -170,9 +169,10 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
||||||
return [];
|
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)
|
public async Task Delete(String torrentId)
|
||||||
|
|
@ -206,7 +206,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
||||||
return torrent;
|
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))
|
if (!String.IsNullOrWhiteSpace(rdTorrent.Filename))
|
||||||
{
|
{
|
||||||
|
|
@ -284,39 +284,42 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
||||||
return torrent;
|
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 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))
|
if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && Settings.Get.Provider.PreferZippedDownloads)
|
||||||
.Select(file => $"https://torbox.app/fakedl/{torrentId?.Id}/{file.Id}")
|
{
|
||||||
.ToList();
|
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))
|
// FileName is set in GetDownlaadInfos
|
||||||
{
|
Debug.Assert(download.FileName != null);
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
var uri = new Uri(downloadUrl);
|
return Task.FromResult(download.FileName);
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
|
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
|
||||||
|
|
@ -336,6 +339,43 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
|
||||||
return Map(result!);
|
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)
|
private void Log(String message, Torrent? torrent = null)
|
||||||
{
|
{
|
||||||
if (torrent != null)
|
if (torrent != null)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ using RdtClient.Service.Services.Downloaders;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Web;
|
|
||||||
|
|
||||||
namespace RdtClient.Service.Services;
|
namespace RdtClient.Service.Services;
|
||||||
|
|
||||||
|
|
@ -363,8 +362,11 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
||||||
var downloadLink = await torrents.UnrestrictLink(download.DownloadId);
|
var downloadLink = await torrents.UnrestrictLink(download.DownloadId);
|
||||||
download.Link = downloadLink;
|
download.Link = downloadLink;
|
||||||
|
|
||||||
var fileName = await torrents.RetrieveFileName(download.DownloadId);
|
if (download.FileName == null)
|
||||||
download.FileName = fileName;
|
{
|
||||||
|
var fileName = await torrents.RetrieveFileName(download.DownloadId);
|
||||||
|
download.FileName = fileName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -447,13 +449,8 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
|
||||||
|
|
||||||
// Check if the unpacking process is even needed
|
// Check if the unpacking process is even needed
|
||||||
var uri = new Uri(download.Link);
|
var uri = new Uri(download.Link);
|
||||||
var fileName = uri.Segments.Last();
|
|
||||||
|
|
||||||
fileName = HttpUtility.UrlDecode(fileName);
|
var extension = Path.GetExtension(download.FileName);
|
||||||
|
|
||||||
Log($"Found file name {fileName}", download, torrent);
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(fileName);
|
|
||||||
|
|
||||||
if ((extension != ".rar" && extension != ".zip") ||
|
if ((extension != ".rar" && extension != ".zip") ||
|
||||||
torrent.DownloadClient == Data.Enums.DownloadClient.Symlink)
|
torrent.DownloadClient == Data.Enums.DownloadClient.Symlink)
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,12 @@ public class Torrents(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await TorrentClient.SelectFiles(torrent);
|
var selected = await TorrentClient.SelectFiles(torrent);
|
||||||
|
|
||||||
|
if (selected == 0)
|
||||||
|
{
|
||||||
|
await MarkAllFilesExcluded(torrent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateDownloads(Guid torrentId)
|
public async Task CreateDownloads(Guid torrentId)
|
||||||
|
|
@ -235,39 +240,48 @@ public class Torrents(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadLinks = await TorrentClient.GetDownloadLinks(torrent);
|
var downloadInfos = await TorrentClient.GetDownloadInfos(torrent);
|
||||||
|
|
||||||
if (downloadLinks == null)
|
if (downloadInfos == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadLinks.Count == 0)
|
if (downloadInfos.Count == 0)
|
||||||
{
|
{
|
||||||
logger.LogInformation("All files excluded by filters (IncludeRegex: {includeRegex}, ExcludeRegex: {excludeRegex}, DownloadMinSize: {downloadMinSize} {torrentInfo}",
|
await MarkAllFilesExcluded(torrent);
|
||||||
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);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var downloadLink in downloadLinks)
|
foreach (var downloadInfo in downloadInfos)
|
||||||
{
|
{
|
||||||
// Make sure downloads don't get added multiple times
|
// 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)
|
public async Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles)
|
||||||
{
|
{
|
||||||
var torrent = await GetById(torrentId);
|
var torrent = await GetById(torrentId);
|
||||||
|
|
@ -390,13 +404,17 @@ public class Torrents(
|
||||||
return unrestrictedLink;
|
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)
|
public async Task<String> RetrieveFileName(Guid downloadId)
|
||||||
{
|
{
|
||||||
var download = await downloads.GetById(downloadId) ?? throw new($"Download with ID {downloadId} not found");
|
var download = await downloads.GetById(downloadId) ?? throw new($"Download with ID {downloadId} not found");
|
||||||
|
|
||||||
Log($"Retrieving filename for", download, download.Torrent);
|
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);
|
await downloads.UpdateFileName(downloadId, fileName);
|
||||||
|
|
||||||
|
|
@ -413,7 +431,9 @@ public class Torrents(
|
||||||
UserName = user.Username,
|
UserName = user.Username,
|
||||||
Expiration = user.Expiration,
|
Expiration = user.Expiration,
|
||||||
CurrentVersion = UpdateChecker.CurrentVersion,
|
CurrentVersion = UpdateChecker.CurrentVersion,
|
||||||
LatestVersion = UpdateChecker.LatestVersion
|
LatestVersion = UpdateChecker.LatestVersion,
|
||||||
|
IsInsecure = UpdateChecker.IsInsecure,
|
||||||
|
DisableUpdateNotification = Settings.Get.General.DisableUpdateNotifications
|
||||||
};
|
};
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using RdtClient.Data.Models.Data;
|
using RdtClient.Data.Models.Data;
|
||||||
using RdtClient.Service.Helpers;
|
using RdtClient.Service.Helpers;
|
||||||
|
using RdtClient.Service.Services.TorrentClients;
|
||||||
using SharpCompress.Archives;
|
using SharpCompress.Archives;
|
||||||
using SharpCompress.Archives.Rar;
|
using SharpCompress.Archives.Rar;
|
||||||
using SharpCompress.Archives.Zip;
|
using SharpCompress.Archives.Zip;
|
||||||
|
|
@ -102,6 +103,11 @@ public class UnpackClient(Download download, String destinationPath)
|
||||||
|
|
||||||
await FileHelper.Delete(filePath);
|
await FileHelper.Delete(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_torrent.ClientKind == Data.Enums.Provider.TorBox)
|
||||||
|
{
|
||||||
|
TorBoxTorrentClient.MoveHashDirContents(extractPath, _torrent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -113,6 +119,7 @@ public class UnpackClient(Download download, String destinationPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static async Task<IList<String>> GetArchiveFiles(String filePath)
|
private static async Task<IList<String>> GetArchiveFiles(String filePath)
|
||||||
{
|
{
|
||||||
await using Stream stream = File.OpenRead(filePath);
|
await using Stream stream = File.OpenRead(filePath);
|
||||||
|
|
@ -161,7 +168,7 @@ public class UnpackClient(Download download, String destinationPath)
|
||||||
d =>
|
d =>
|
||||||
{
|
{
|
||||||
Debug.WriteLine(d);
|
Debug.WriteLine(d);
|
||||||
Progess = (Int32) Math.Round(d);
|
Progess = (Int32)Math.Round(d);
|
||||||
},
|
},
|
||||||
cancellationToken: cancellationToken);
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ public class Process : IProcess
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_process.Dispose();
|
_process.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BeginOutputReadLine()
|
public void BeginOutputReadLine()
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ public class AuthController(Authentication authentication, Settings settings) :
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[Authorize(Policy = "AuthSetting")]
|
||||||
[Route("SetupProvider")]
|
[Route("SetupProvider")]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ActionResult> SetupProvider([FromBody] AuthControllerSetupProviderRequest? request)
|
public async Task<ActionResult> SetupProvider([FromBody] AuthControllerSetupProviderRequest? request)
|
||||||
|
|
@ -82,13 +82,6 @@ public class AuthController(Authentication authentication, Settings settings) :
|
||||||
return StatusCode(401);
|
return StatusCode(401);
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await authentication.GetUser();
|
|
||||||
|
|
||||||
if (user != null)
|
|
||||||
{
|
|
||||||
return StatusCode(401);
|
|
||||||
}
|
|
||||||
|
|
||||||
await settings.Update("Provider:Provider", request.Provider);
|
await settings.Update("Provider:Provider", request.Provider);
|
||||||
await settings.Update("Provider:ApiKey", request.Token);
|
await settings.Update("Provider:ApiKey", request.Token);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
using Aria2NET;
|
using Aria2NET;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
@ -45,6 +46,18 @@ public class SettingsController(Settings settings, Torrents torrents) : Controll
|
||||||
return Ok(profile);
|
return Ok(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("Version")]
|
||||||
|
public ActionResult<Version> Version()
|
||||||
|
{
|
||||||
|
var version = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
Version = version
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("TestPath")]
|
[Route("TestPath")]
|
||||||
public async Task<ActionResult> TestPath([FromBody] SettingsControllerTestPathRequest? request)
|
public async Task<ActionResult> TestPath([FromBody] SettingsControllerTestPathRequest? request)
|
||||||
|
|
|
||||||
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>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId>
|
<UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId>
|
||||||
<Version>2.0.102</Version>
|
<Version>2.0.104</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
|
|
@ -29,15 +29,15 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.4" />
|
||||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
|
|
|
||||||
0
docker-build-dev.ps1 → tools/docker-build-dev.ps1
Executable file → Normal file
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
|
$version = (Get-Content "package.json" | ConvertFrom-Json).version
|
||||||
|
|
||||||
$csProj = "$pwd\server\RdtClient.Web\RdtClient.Web.csproj"
|
$csProj = "$pwd\server\RdtClient.Web\RdtClient.Web.csproj"
|
||||||
$navbar = "$pwd\client\src\app\navbar\navbar.component.html";
|
|
||||||
|
|
||||||
$newCsProj = (Get-Content $csProj) -replace '<Version>.*?<\/Version>', "<Version>$version</Version>"
|
$newCsProj = (Get-Content $csProj) -replace '<Version>.*?<\/Version>', "<Version>$version</Version>"
|
||||||
[System.IO.File]::WriteAllLines($csProj, $newCsProj, $utf8NoBomEncoding)
|
[System.IO.File]::WriteAllLines($csProj, $newCsProj, $utf8NoBomEncoding)
|
||||||
|
|
||||||
$newNavbar = (Get-Content $navbar) -replace 'Version .*?<', "Version $version<"
|
|
||||||
[System.IO.File]::WriteAllLines($navbar, $newNavbar, $utf8NoBomEncoding)
|
|
||||||
|
|
||||||
Write-Output "Commit and push now, press any key to continue"
|
Write-Output "Commit and push now, press any key to continue"
|
||||||
|
|
||||||
[Console]::ReadKey()
|
[Console]::ReadKey()
|
||||||
Loading…
Reference in a new issue