From fc15569e1b51c1f5142cceb93cee990981e6da43 Mon Sep 17 00:00:00 2001 From: Roger Far Date: Fri, 13 May 2022 14:39:52 -0600 Subject: [PATCH] Rewrote the setting store, retrieve and displaying on the settings page to make maintenance a lot easier. Added settings to set defaults for the provider import, sonarr, gui and watch folders. --- .../add-new-torrent.component.ts | 22 +- client/src/app/app.module.ts | 6 +- client/src/app/auth.service.ts | 7 + client/src/app/models/setting.model.ts | 9 +- client/src/app/nl2br.pipe.ts | 27 ++ client/src/app/settings.service.ts | 2 +- .../src/app/settings/settings.component.html | 450 ++--------------- client/src/app/settings/settings.component.ts | 211 +------- client/src/app/setup/setup.component.html | 10 +- client/src/app/setup/setup.component.ts | 16 +- server/RdtClient.Data/Data/DataContext.cs | 204 -------- server/RdtClient.Data/Data/SettingData.cs | 250 ++++++---- server/RdtClient.Data/Enums/DownloadClient.cs | 8 + server/RdtClient.Data/Enums/LogLevel.cs | 18 + server/RdtClient.Data/Enums/Provider.cs | 12 + .../Enums/TorrentDownloadAction.cs | 9 +- .../Enums/TorrentFinishedAction.cs | 9 +- ...513164723_Settings_Remove_Type.Designer.cs | 458 ++++++++++++++++++ .../20220513164723_Settings_Remove_Type.cs | 25 + ...0220513175547_Settings_Migrate.Designer.cs | 458 ++++++++++++++++++ .../20220513175547_Settings_Migrate.cs | 54 +++ .../Migrations/DataContextModelSnapshot.cs | 5 +- server/RdtClient.Data/Models/Data/Setting.cs | 2 - .../Models/Internal/DbSettings.cs | 234 +++++++-- .../Models/Internal/SettingProperty.cs | 11 + server/RdtClient.Data/RdtClient.Data.csproj | 4 + .../BackgroundServices/ProviderUpdater.cs | 11 +- .../BackgroundServices/Startup.cs | 31 +- .../BackgroundServices/TaskRunner.cs | 5 +- .../BackgroundServices/UpdateChecker.cs | 5 + .../BackgroundServices/WatchFolderChecker.cs | 112 +++++ .../Services/DownloadClient.cs | 12 +- .../Services/Downloaders/Aria2cDownloader.cs | 12 +- .../Services/Downloaders/MultiDownloader.cs | 8 +- .../Services/Downloaders/SimpleDownloader.cs | 10 +- .../RdtClient.Service/Services/QBittorrent.cs | 120 ++--- server/RdtClient.Service/Services/Settings.cs | 53 +- .../TorrentClients/AllDebridTorrentClient.cs | 2 +- .../TorrentClients/RealDebridTorrentClient.cs | 8 +- .../Services/TorrentRunner.cs | 16 +- server/RdtClient.Service/Services/Torrents.cs | 38 +- .../Controllers/AuthController.cs | 30 +- .../Controllers/SettingsController.cs | 24 +- 43 files changed, 1901 insertions(+), 1117 deletions(-) create mode 100644 client/src/app/nl2br.pipe.ts create mode 100644 server/RdtClient.Data/Enums/DownloadClient.cs create mode 100644 server/RdtClient.Data/Enums/LogLevel.cs create mode 100644 server/RdtClient.Data/Enums/Provider.cs create mode 100644 server/RdtClient.Data/Migrations/20220513164723_Settings_Remove_Type.Designer.cs create mode 100644 server/RdtClient.Data/Migrations/20220513164723_Settings_Remove_Type.cs create mode 100644 server/RdtClient.Data/Migrations/20220513175547_Settings_Migrate.Designer.cs create mode 100644 server/RdtClient.Data/Migrations/20220513175547_Settings_Migrate.cs create mode 100644 server/RdtClient.Data/Models/Internal/SettingProperty.cs create mode 100644 server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs diff --git a/client/src/app/add-new-torrent/add-new-torrent.component.ts b/client/src/app/add-new-torrent/add-new-torrent.component.ts index 08d6193..62ae063 100644 --- a/client/src/app/add-new-torrent/add-new-torrent.component.ts +++ b/client/src/app/add-new-torrent/add-new-torrent.component.ts @@ -17,17 +17,14 @@ export class AddNewTorrentComponent implements OnInit { public provider: string; public category: string; - public priority: number; - public downloadAction: number = 0; public finishedAction: number = 0; - public downloadMinSize: number = 0; - - public downloadRetryAttempts: number = 3; public torrentRetryAttempts: number = 1; + public downloadRetryAttempts: number = 3; public torrentDeleteOnError: number = 0; public torrentLifetime: number = 0; + public priority: number; public availableFiles: TorrentFileAvailability[]; public downloadFiles: { [key: string]: boolean } = {}; @@ -44,7 +41,20 @@ export class AddNewTorrentComponent implements OnInit { private settingsService: SettingsService ) { this.settingsService.get().subscribe((settings) => { - this.provider = settings.firstOrDefault((m) => m.settingId === 'Provider')?.value; + const providerSetting = settings.first((m) => m.key === 'Provider:Provider'); + this.provider = providerSetting.enumValues[providerSetting.value as number]; + + this.category = settings.first((m) => m.key === 'Gui:Default:Category')?.value as string; + this.downloadAction = + settings.first((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0; + this.finishedAction = settings.first((m) => m.key === 'Gui:Default:FinishedAction')?.value as number; + this.downloadMinSize = settings.first((m) => m.key === 'Gui:Default:MinFileSize')?.value as number; + this.torrentRetryAttempts = settings.first((m) => m.key === 'Gui:Default:TorrentRetryAttempts')?.value as number; + this.downloadRetryAttempts = settings.first((m) => m.key === 'Gui:Default:DownloadRetryAttempts') + ?.value as number; + this.torrentDeleteOnError = settings.first((m) => m.key === 'Gui:Default:DeleteOnError')?.value as number; + this.torrentLifetime = settings.first((m) => m.key === 'Gui:Default:TorrentLifetime')?.value as number; + this.priority = settings.first((m) => m.key === 'Gui:Default:Priority')?.value as number; }); } diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 633d6d0..4c22a23 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -10,17 +10,18 @@ import { AddNewTorrentComponent } from './add-new-torrent/add-new-torrent.compon import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { AuthInterceptor } from './auth.interceptor'; +import { DecodeURIPipe } from './decode-uri.pipe'; import { DownloadStatusPipe } from './download-status.pipe'; import { LoginComponent } from './login/login.component'; import { MainLayoutComponent } from './main-layout/main-layout.component'; import { NavbarComponent } from './navbar/navbar.component'; +import { Nl2BrPipe } from './nl2br.pipe'; +import { ProfileComponent } from './profile/profile.component'; import { SettingsComponent } from './settings/settings.component'; import { SetupComponent } from './setup/setup.component'; import { TorrentStatusPipe } from './torrent-status.pipe'; import { TorrentTableComponent } from './torrent-table/torrent-table.component'; import { TorrentComponent } from './torrent/torrent.component'; -import { DecodeURIPipe } from './decode-uri.pipe'; -import { ProfileComponent } from './profile/profile.component'; curray(); @@ -39,6 +40,7 @@ curray(); TorrentComponent, DecodeURIPipe, ProfileComponent, + Nl2BrPipe, ], imports: [ BrowserModule, diff --git a/client/src/app/auth.service.ts b/client/src/app/auth.service.ts index 4e55561..4ce50b0 100644 --- a/client/src/app/auth.service.ts +++ b/client/src/app/auth.service.ts @@ -19,6 +19,13 @@ export class AuthService { }); } + public setupProvider(provider: string, token: string): Observable { + return this.http.post(`/Api/Authentication/SetupProvider`, { + provider, + token, + }); + } + public login(userName: string, password: string): Observable { return this.http.post(`/Api/Authentication/Login`, { userName, diff --git a/client/src/app/models/setting.model.ts b/client/src/app/models/setting.model.ts index 7e8c8d9..0dc5c62 100644 --- a/client/src/app/models/setting.model.ts +++ b/client/src/app/models/setting.model.ts @@ -1,4 +1,9 @@ export class Setting { - public settingId: string; - public value: string; + key: string; + value: boolean | number | null | string; + displayName: null | string; + description: null | string; + type: string; + settings: Setting[]; + enumValues: { [key: string]: string }; } diff --git a/client/src/app/nl2br.pipe.ts b/client/src/app/nl2br.pipe.ts new file mode 100644 index 0000000..48efbeb --- /dev/null +++ b/client/src/app/nl2br.pipe.ts @@ -0,0 +1,27 @@ +import { Pipe, PipeTransform, SecurityContext, VERSION } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +@Pipe({ + name: 'nl2br', +}) +export class Nl2BrPipe implements PipeTransform { + constructor(private sanitizer: DomSanitizer) {} + + transform(value: string, sanitizeBeforehand?: boolean): string { + if (typeof value !== 'string') { + return value; + } + let result: any; + const textParsed = value.replace(/(?:\r\n|\r|\n)/g, '
'); + + if (!VERSION || VERSION.major === '2') { + result = this.sanitizer.bypassSecurityTrustHtml(textParsed); + } else if (sanitizeBeforehand) { + result = this.sanitizer.sanitize(SecurityContext.HTML, textParsed); + } else { + result = textParsed; + } + + return result; + } +} diff --git a/client/src/app/settings.service.ts b/client/src/app/settings.service.ts index 106fa81..8ba08c0 100644 --- a/client/src/app/settings.service.ts +++ b/client/src/app/settings.service.ts @@ -15,7 +15,7 @@ export class SettingsService { } public update(settings: Setting[]): Observable { - return this.http.put(`/Api/Settings`, { settings }); + return this.http.put(`/Api/Settings`, settings); } public getProfile(): Observable { diff --git a/client/src/app/settings/settings.component.html b/client/src/app/settings/settings.component.html index 7657d21..0128937 100644 --- a/client/src/app/settings/settings.component.html +++ b/client/src/app/settings/settings.component.html @@ -1,373 +1,67 @@ -
-
- -
- -
-

- The following 2 providers are supported: -
- https://real-debrid.com -
- https://alldebrid.com -
- At this point only 1 provider can be used at the time. -

-
-
- -
- -
-

- You can find your API key here: - https://real-debrid.com/apitoken. -

-
-
- -
- -
-

- You can find your API key here: - https://alldebrid.com/apikeys/. -

-
-
-
- -
- When selected, import downloads that are not added through RealDebridClient but have been directly added to - Real-Debrid or AllDebrid. -
-
-
-
- -
- -
-

When a torrent is imported assign it this category.

-
-
-
- -
- When selected, cancel and delete downloads that have been removed from Real-Debrid or AllDebrid. -
-
-
-
- -
- -
-

- The timeout to use to connect to the provider in seconds, with a minimum of 5 seconds. Increase if you experience - timeout errors in the log. This setting is only used when there is a user connected to the web interface or if - AutoImport/AutoDelete has been enabled. -

-
-
- -
- -
-

- This interval is used to check Real-Debrid or AllDebrid for updates. This setting is only used when there are - active downloads. If there are no active downloads it will use this setting * 3, with a minimum of 30 seconds. -

-
-
- -
-
- -
- -
-

Recommended level is Warning, set to Debug to get the most info.

-
-
- -
- -
-

Path in the docker container to download files to (i.e. /data/downloads).

-
-
- -
- -
-

- Path where files are downloaded to on your host (i.e. D:\Downloads). This path is used for Radarr and Sonarr to - find your downloads. -

-
- -
- -
- -
-

Maximum amount of downloads that get unpacked on your host at the same time.

-
- -
- -
- -
-

- Path to the executable to run when the torrent and all downloads are finished. No arguments should be passed here. -
- When running in Docker, this command will run on your docker instance! -

-
- -
- -
- -
-

- When the executable above is executed, use these parameters. -
- Supports the following parameters: -

-
    -
  • %N: Torrent name
  • -
  • %L: Category
  • -
  • %F: Content path (same as root path for multifile torrent)
  • -
  • %R: Root path (first torrent subdirectory path)
  • -
  • %D: Save path
  • -
  • %C: Number of files
  • -
  • %Z: Torrent size (bytes)
  • -
  • %I: Info hash
  • -
-
-
- -
-
- -
- -
-

- Select which download client to use, see the - README for the various options. -

-
- -
- -
- -
-

- Path in the docker container to temporarily download to (i.e. /data/temp). Make sure the docker container has - enough disk space if using a path inside the container. -

-
- -
- -
- -
-

Maximum amount of torrents that get downloaded to your host at the same time.

-
- -
- -
- -
-

- Maximum amount of parallel threads that are used to download a single torrent to your host. If set to 1 no - parallel downloading will be done. -

-
- -
- -
- -
-

Maximum download speed in Megabytes per second. When set to 0 unlimited speed is used.

-
- -
- -
- -
-

Address of a proxy server.

-
- -
- -
- -
-

- This is the URL to your Aria2c instance. It must end in /jsonrpc. A common URL is - http://192.168.10.2:6800/jsonrpc. -

-
- -
- -
- -
-

The secret of your Aria2c instance. Optional.

-
- - - -
- Could connect to Aria2 client
- {{ testAria2cConnectionError }} -
- -
- Found Aria2 client version {{ testAria2cConnectionSuccess }} -
-
- -
-

- The following settings only apply when a torrent gets added through the qbittorrent API, usually Radarr or Sonarr. -

-
- -
-
-
- +
+

{{ tab.description }}

+ +

{{ setting.displayName }}

+
+ + +
+
-
- MB +
+
-
-
- Files that are smaller than this setting are skipped and not downloaded. When set to 0 all files are downloaded. - When downloading from Radarr or Sonarr it's recommended to keep this setting at atleast a few MB to avoid - Real-Debrid having to re-download the torrent. -
-
-
+ +
+ +
+ +
Invalid setting type {{ setting.type }}
+ -
-
- -
- When selected, it will only download files in the torrent that have been download by Real-Debrid. You can use - this in combination with the Min File size setting above. -
-
-
+

-
- -
- -
-

When a single download fails it will retry it this many times before marking it as failed.

-
+ + +
+ Could connect to Aria2 client
+ {{ testAria2cConnectionError }} +
-
- -
- +
+ Found Aria2 client version {{ testAria2cConnectionSuccess }} +
+
-

- When a single download has failed multiple times (see setting above) or when the torrent itself received an error - it will retry the full torrent this many times before marking it failed. -

-
- -
- -
- -
-

- When a download has been in error for this many minutes, delete it from the provider and the client. 0 to disable. -

-
- -
- -
- -
-

- The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the torrent - is completed and has downloads, the lifetime setting will not apply. 0 to disable. -

-
+
-
+
@@ -441,57 +135,13 @@
-
-
- -
- -
-

- Watch this path for .torrent or .magnet files. When a file is found it will be automatically imported. -

-
-
-
- -
- When selected, cancel and delete downloads that have been removed from Real-Debrid or AllDebrid. -
-
-
-
- -
- -
-

- The timeout to use to connect to the provider in seconds, with a minimum of 5 seconds. Increase if you experience - timeout errors in the log. This setting is only used when there is a user connected to the web interface or if - AutoImport/AutoDelete has been enabled. -

-
-
- -
- -
-

- This interval is used to check Real-Debrid or AllDebrid for updates. This setting is only used when there are - active downloads. If there are no active downloads it will use this setting * 3, with a minimum of 30 seconds. -

-
-
-
Error saving settings: {{ error }}
-
+
diff --git a/client/src/app/setup/setup.component.ts b/client/src/app/setup/setup.component.ts index 86e07f1..c87cd8b 100644 --- a/client/src/app/setup/setup.component.ts +++ b/client/src/app/setup/setup.component.ts @@ -1,8 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from '../auth.service'; -import { Setting } from '../models/setting.model'; -import { SettingsService } from '../settings.service'; @Component({ selector: 'app-setup', @@ -20,7 +18,7 @@ export class SetupComponent implements OnInit { public step: number = 1; - constructor(private authService: AuthService, private settingsService: SettingsService, private router: Router) {} + constructor(private authService: AuthService, private router: Router) {} ngOnInit(): void {} @@ -41,20 +39,12 @@ export class SetupComponent implements OnInit { } public setToken(): void { - const settingToken = new Setting(); - settingToken.settingId = 'RealDebridApiKey'; - settingToken.value = this.token; - - const settingProvider = new Setting(); - settingProvider.settingId = 'Provider'; - settingProvider.value = this.provider; - - this.settingsService.update([settingToken, settingProvider]).subscribe( + this.authService.setupProvider(this.provider, this.token).subscribe( () => { this.step = 3; this.working = false; }, - (err) => { + (err: any) => { this.working = false; this.error = err.error; } diff --git a/server/RdtClient.Data/Data/DataContext.cs b/server/RdtClient.Data/Data/DataContext.cs index 41cf1d0..3b7f817 100644 --- a/server/RdtClient.Data/Data/DataContext.cs +++ b/server/RdtClient.Data/Data/DataContext.cs @@ -14,11 +14,6 @@ public class DataContext : IdentityDbContext public DbSet Settings { get; set; } public DbSet Torrents { get; set; } - protected override void OnConfiguring(DbContextOptionsBuilder options) - { - - } - protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); @@ -32,203 +27,4 @@ public class DataContext : IdentityDbContext fk.DeleteBehavior = DeleteBehavior.Restrict; } } - - public async Task Seed() - { - var seedSettings = new List - { - new Setting - { - SettingId = "Provider", - Type = "String", - Value = "RealDebrid" - }, - new Setting - { - SettingId = "ProviderAutoImport", - Type = "Int32", - Value = "0" - }, - new Setting - { - SettingId = "ProviderAutoImportCategory", - Type = "String", - Value = "" - }, - new Setting - { - SettingId = "ProviderAutoDelete", - Type = "Int32", - Value = "0" - }, - new Setting - { - SettingId = "ProviderTimeout", - Type = "Int32", - Value = "10" - }, - new Setting - { - SettingId = "ProviderCheckInterval", - Type = "Int32", - Value = "10" - }, - new Setting - { - SettingId = "DeleteOnError", - Type = "Int32", - Value = "0" - }, - new Setting - { - SettingId = "TorrentLifetime", - Type = "Int32", - Value = "0" - }, - new Setting - { - SettingId = "RealDebridApiKey", - Type = "String", - Value = "" - }, - new Setting - { - SettingId = "DownloadPath", - Type = "String", -#if DEBUG - Value = @"C:\Temp\rdtclient" -#else - Value = "/data/downloads" -#endif - }, - new Setting - { - SettingId = "DownloadClient", - Type = "String", - Value = @"Simple" - }, - new Setting - { - SettingId = "TempPath", - Type = "String", -#if DEBUG - Value = @"C:\Temp\rdtclient" -#else - Value = "/data/downloads" -#endif - }, - new Setting - { - SettingId = "MappedPath", - Type = "String", -#if DEBUG - Value = @"C:\Temp\rdtclient" -#else - Value = @"C:\Downloads" -#endif - }, - new Setting - { - SettingId = "DownloadLimit", - Type = "Int32", - Value = "2" - }, - new Setting - { - SettingId = "UnpackLimit", - Type = "Int32", - Value = "1" - }, - new Setting - { - SettingId = "MinFileSize", - Type = "Int32", - Value = "0" - }, - new Setting - { - SettingId = "OnlyDownloadAvailableFiles", - Type = "Int32", - Value = "1" - }, - new Setting - { - SettingId = "DownloadChunkCount", - Type = "Int32", - Value = "8" - }, - new Setting - { - SettingId = "DownloadMaxSpeed", - Type = "Int32", - Value = "0" - }, - new Setting - { - SettingId = "ProxyServer", - Type = "String", - Value = "" - }, - new Setting - { - SettingId = "LogLevel", - Type = "String", - Value = "Warning" - }, - new Setting - { - SettingId = "Categories", - Type = "String", - Value = "" - }, - new Setting - { - SettingId = "Aria2cUrl", - Type = "String", - Value = "http://127.0.0.1:6800/jsonrpc" - }, - new Setting - { - SettingId = "Aria2cSecret", - Type = "String", - Value = "" - }, - new Setting - { - SettingId = "DownloadRetryAttempts", - Type = "Int32", - Value = "3" - }, - new Setting - { - SettingId = "TorrentRetryAttempts", - Type = "Int32", - Value = "1" - }, - new Setting - { - SettingId = "RunOnTorrentCompleteFileName", - Type = "String", - Value = "" - }, - new Setting - { - SettingId = "RunOnTorrentCompleteArguments", - Type = "String", - Value = "" - } - }; - - var dbSettings = await Settings.ToListAsync(); - foreach (var seedSetting in seedSettings) - { - var dbSetting = dbSettings.FirstOrDefault(m => m.SettingId == seedSetting.SettingId); - - if (dbSetting == null) - { - await Settings.AddAsync(seedSetting); - await SaveChangesAsync(); - } - } - } } \ No newline at end of file diff --git a/server/RdtClient.Data/Data/SettingData.cs b/server/RdtClient.Data/Data/SettingData.cs index f1745d9..e9d3135 100644 --- a/server/RdtClient.Data/Data/SettingData.cs +++ b/server/RdtClient.Data/Data/SettingData.cs @@ -1,4 +1,6 @@ -using Microsoft.EntityFrameworkCore; +using System.ComponentModel; +using System.Reflection; +using Microsoft.EntityFrameworkCore; using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Internal; using Serilog; @@ -7,128 +9,200 @@ namespace RdtClient.Data.Data; public class SettingData { - private static readonly SemaphoreSlim _settingCacheLock = new(1); - private readonly DataContext _dataContext; + public static DbSettings Get { get; } = new DbSettings(); + public SettingData(DataContext dataContext) { _dataContext = dataContext; } - public static DbSettings Get { get; private set; } + public IList GetAll() + { + return GetSettings(Get, null).ToList(); + } + + public async Task Update(IList settings) + { + var dbSettings = await _dataContext.Settings.ToListAsync(); + + foreach (var dbSetting in dbSettings) + { + var setting = settings.FirstOrDefault(m => m.Key == dbSetting.SettingId); + + if (setting != null) + { + dbSetting.Value = setting.Value.ToString(); + } + } + + await _dataContext.SaveChangesAsync(); + + await ResetCache(); + } + + public async Task Update(String settingId, Object value) + { + var dbSetting = await _dataContext.Settings.FirstOrDefaultAsync(m => m.SettingId == settingId); + + if (dbSetting == null) + { + return; + } + + dbSetting.Value = value.ToString(); + + await _dataContext.SaveChangesAsync(); + + await ResetCache(); + } public async Task ResetCache() { - var allSettings = await _dataContext.Settings.AsNoTracking().ToListAsync(); + var settings = await _dataContext.Settings.AsNoTracking().ToListAsync(); - String GetString(String name) + if (settings.Count == 0) { - return allSettings.FirstOrDefault(m => m.SettingId == name)?.Value; + throw new Exception("No settings found, please restart"); } - Int32 GetInt32(String name) - { - var strVal = GetString(name); + SetSettings(settings, Get, null); + } - if (!Int32.TryParse(strVal, out var intVal)) + public async Task Seed() + { + var dbSettings = await _dataContext.Settings.AsNoTracking().ToListAsync(); + + var expectedSettings = GetSettings(Get, null).Where(m => m.Type != "Object").Select(m => new Setting + { + SettingId = m.Key, + Value = m.Value?.ToString() + }).ToList(); + + var newSettings = expectedSettings.Where(m => dbSettings.All(p => p.SettingId != m.SettingId)).ToList(); + + if (newSettings.Any()) + { + await _dataContext.Settings.AddRangeAsync(newSettings); + await _dataContext.SaveChangesAsync(); + } + + var oldSettings = dbSettings.Where(m => expectedSettings.All(p => p.SettingId != m.SettingId)).ToList(); + + if (oldSettings.Any()) + { + _dataContext.Settings.RemoveRange(oldSettings); + await _dataContext.SaveChangesAsync(); + } + } + + private static IEnumerable GetSettings(Object defaultSetting, String parent) + { + var result = new List(); + + var properties = defaultSetting.GetType().GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in properties) + { + var displayName = (DisplayNameAttribute) Attribute.GetCustomAttribute(property, typeof(DisplayNameAttribute)); + var description = (DescriptionAttribute) Attribute.GetCustomAttribute(property, typeof(DescriptionAttribute)); + var propertyName = property.Name; + + if (parent != null) { - Log.Error("Unable to parse setting {name} to Int32", name); - return 0; + propertyName = $"{parent}:{propertyName}"; } - return intVal; + var settingProperty = new SettingProperty + { + Key = propertyName, + DisplayName = displayName?.DisplayName, + Description = description?.Description, + Type = property.PropertyType.Name + }; + if (property.PropertyType.IsEnum || + property.PropertyType.IsValueType || + property.PropertyType == typeof(String)) + { + settingProperty.Value = property.GetValue(defaultSetting); + + if (property.PropertyType.IsEnum) + { + settingProperty.Type = "Enum"; + settingProperty.EnumValues = new Dictionary(); + + foreach (var e in Enum.GetValues(property.PropertyType).Cast()) + { + var enumMember = property.PropertyType.GetMember(e.ToString()).First(); + var enumDescriptionAttribute = enumMember.GetCustomAttribute(); + var enumName = enumDescriptionAttribute?.Description ?? Enum.GetName(property.PropertyType, e); + settingProperty.EnumValues.Add((Int32)(Object)e, enumName); + } + } + + result.Add(settingProperty); + } + else + { + settingProperty.Type = "Object"; + result.Add(settingProperty); + + var childResults = GetSettings(property.GetValue(defaultSetting), propertyName); + result.AddRange(childResults); + } } - Get = new DbSettings - { - Provider = GetString("Provider"), - ProviderAutoImport = GetInt32("ProviderAutoImport"), - ProviderAutoImportCategory = GetString("ProviderAutoImportCategory"), - ProviderAutoDelete = GetInt32("ProviderAutoDelete"), - ProviderTimeout = GetInt32("ProviderTimeout"), - ProviderCheckInterval = GetInt32("ProviderCheckInterval"), - RealDebridApiKey = GetString("RealDebridApiKey"), - DownloadPath = GetString("DownloadPath"), - DownloadClient = GetString("DownloadClient"), - TempPath = GetString("TempPath"), - MappedPath = GetString("MappedPath"), - DownloadLimit = GetInt32("DownloadLimit"), - UnpackLimit = GetInt32("UnpackLimit"), - MinFileSize = GetInt32("MinFileSize"), - OnlyDownloadAvailableFiles = GetInt32("OnlyDownloadAvailableFiles"), - DownloadChunkCount = GetInt32("DownloadChunkCount"), - DownloadMaxSpeed = GetInt32("DownloadMaxSpeed"), - ProxyServer = GetString("ProxyServer"), - LogLevel = GetString("LogLevel"), - Categories = GetString("Categories"), - Aria2cUrl = GetString("Aria2cUrl"), - Aria2cSecret = GetString("Aria2cSecret"), - DownloadRetryAttempts = GetInt32("DownloadRetryAttempts"), - TorrentRetryAttempts = GetInt32("TorrentRetryAttempts"), - DeleteOnError = GetInt32("DeleteOnError"), - TorrentLifetime = GetInt32("TorrentLifetime"), - RunOnTorrentCompleteFileName = GetString("RunOnTorrentCompleteFileName"), - RunOnTorrentCompleteArguments = GetString("RunOnTorrentCompleteArguments") - }; + return result; } - public async Task> GetAll() + private static void SetSettings(IList settings, Object defaultSetting, String parent) { - return await _dataContext.Settings.AsNoTracking().ToListAsync(); - } + var properties = defaultSetting.GetType().GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance); - public async Task Update(IList settings) - { - await _settingCacheLock.WaitAsync(); - - try + foreach (var property in properties) { - var dbSettings = await _dataContext.Settings.ToListAsync(); + var propertyName = property.Name; - foreach (var dbSetting in dbSettings) + if (parent != null) { - var setting = settings.FirstOrDefault(m => m.SettingId == dbSetting.SettingId); + propertyName = $"{parent}:{propertyName}"; + } + + if (property.PropertyType.IsEnum || + property.PropertyType.IsValueType || + property.PropertyType == typeof(String)) + { + var setting = settings.FirstOrDefault(m => m.SettingId == propertyName); if (setting != null) { - dbSetting.Value = setting.Value; + if (property.PropertyType.IsEnum) + { + var newValue = Enum.Parse(property.PropertyType, setting.Value); + property.SetValue(defaultSetting, newValue); + } + else + { + var converter = TypeDescriptor.GetConverter(property.PropertyType); + + if (converter.IsValid(setting.Value)) + { + var newValue = converter.ConvertFrom(setting.Value); + property.SetValue(defaultSetting, newValue); + } + else + { + Log.Warning($"Invalid value for setting {propertyName}: {setting.Value}"); + } + } } } - - await _dataContext.SaveChangesAsync(); - - await ResetCache(); - } - finally - { - _settingCacheLock.Release(); - } - } - - public async Task UpdateString(String key, String value) - { - await _settingCacheLock.WaitAsync(); - - try - { - var dbSetting = await _dataContext.Settings.FirstOrDefaultAsync(m => m.SettingId == key); - - if (dbSetting == null) + else { - throw new Exception($"Cannot find setting with key {key}"); + SetSettings(settings, property.GetValue(defaultSetting), propertyName); } - - dbSetting.Value = value; - - await _dataContext.SaveChangesAsync(); - - await ResetCache(); - } - finally - { - _settingCacheLock.Release(); } } } \ No newline at end of file diff --git a/server/RdtClient.Data/Enums/DownloadClient.cs b/server/RdtClient.Data/Enums/DownloadClient.cs new file mode 100644 index 0000000..183ae5f --- /dev/null +++ b/server/RdtClient.Data/Enums/DownloadClient.cs @@ -0,0 +1,8 @@ +namespace RdtClient.Data.Enums; + +public enum DownloadClient +{ + Simple, + MultiPart, + Aria2c +} \ No newline at end of file diff --git a/server/RdtClient.Data/Enums/LogLevel.cs b/server/RdtClient.Data/Enums/LogLevel.cs new file mode 100644 index 0000000..fdc43f1 --- /dev/null +++ b/server/RdtClient.Data/Enums/LogLevel.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; + +namespace RdtClient.Data.Enums; + +public enum LogLevel +{ + [Description("Debug")] + Debug, + + [Description("Information")] + Information, + + [Description("Warning")] + Warning, + + [Description("Error")] + Error +} \ No newline at end of file diff --git a/server/RdtClient.Data/Enums/Provider.cs b/server/RdtClient.Data/Enums/Provider.cs new file mode 100644 index 0000000..fd81c22 --- /dev/null +++ b/server/RdtClient.Data/Enums/Provider.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace RdtClient.Data.Enums; + +public enum Provider +{ + [Description("RealDebrid")] + RealDebrid, + + [Description("AllDebrid")] + AllDebrid +} \ No newline at end of file diff --git a/server/RdtClient.Data/Enums/TorrentDownloadAction.cs b/server/RdtClient.Data/Enums/TorrentDownloadAction.cs index 40a4094..c9ff757 100644 --- a/server/RdtClient.Data/Enums/TorrentDownloadAction.cs +++ b/server/RdtClient.Data/Enums/TorrentDownloadAction.cs @@ -1,8 +1,15 @@ -namespace RdtClient.Data.Enums; +using System.ComponentModel; + +namespace RdtClient.Data.Enums; public enum TorrentDownloadAction { + [Description("Download All Files")] DownloadAll = 0, + + [Description("Download All Available Files")] DownloadAvailableFiles = 1, + + [Description("Manually Select Files")] DownloadManual = 2 } \ No newline at end of file diff --git a/server/RdtClient.Data/Enums/TorrentFinishedAction.cs b/server/RdtClient.Data/Enums/TorrentFinishedAction.cs index 777c605..efaf448 100644 --- a/server/RdtClient.Data/Enums/TorrentFinishedAction.cs +++ b/server/RdtClient.Data/Enums/TorrentFinishedAction.cs @@ -1,8 +1,15 @@ -namespace RdtClient.Data.Enums; +using System.ComponentModel; + +namespace RdtClient.Data.Enums; public enum TorrentFinishedAction { + [Description("No Action")] None = 0, + + [Description("Remove Torrent From Client And Provider")] RemoveAllTorrents = 1, + + [Description("Remove Torrent From Provider")] RemoveRealDebrid = 2 } \ No newline at end of file diff --git a/server/RdtClient.Data/Migrations/20220513164723_Settings_Remove_Type.Designer.cs b/server/RdtClient.Data/Migrations/20220513164723_Settings_Remove_Type.Designer.cs new file mode 100644 index 0000000..eb11b76 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20220513164723_Settings_Remove_Type.Designer.cs @@ -0,0 +1,458 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RdtClient.Data.Data; + +#nullable disable + +namespace RdtClient.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20220513164723_Settings_Remove_Type")] + partial class Settings_Remove_Type + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.4"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.Property("DownloadId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DownloadFinished") + .HasColumnType("TEXT"); + + b.Property("DownloadQueued") + .HasColumnType("TEXT"); + + b.Property("DownloadStarted") + .HasColumnType("TEXT"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("RemoteId") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("TorrentId") + .HasColumnType("TEXT"); + + b.Property("UnpackingFinished") + .HasColumnType("TEXT"); + + b.Property("UnpackingQueued") + .HasColumnType("TEXT"); + + b.Property("UnpackingStarted") + .HasColumnType("TEXT"); + + b.HasKey("DownloadId"); + + b.HasIndex("TorrentId"); + + b.ToTable("Downloads"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b => + { + b.Property("SettingId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Property("TorrentId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DeleteOnError") + .HasColumnType("INTEGER"); + + b.Property("DownloadAction") + .HasColumnType("INTEGER"); + + b.Property("DownloadManualFiles") + .HasColumnType("TEXT"); + + b.Property("DownloadMinSize") + .HasColumnType("INTEGER"); + + b.Property("DownloadRetryAttempts") + .HasColumnType("INTEGER"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("FileOrMagnet") + .HasColumnType("TEXT"); + + b.Property("FilesSelected") + .HasColumnType("TEXT"); + + b.Property("FinishedAction") + .HasColumnType("INTEGER"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("IsFile") + .HasColumnType("INTEGER"); + + b.Property("Lifetime") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RdAdded") + .HasColumnType("TEXT"); + + b.Property("RdEnded") + .HasColumnType("TEXT"); + + b.Property("RdFiles") + .HasColumnType("TEXT"); + + b.Property("RdHost") + .HasColumnType("TEXT"); + + b.Property("RdId") + .HasColumnType("TEXT"); + + b.Property("RdName") + .HasColumnType("TEXT"); + + b.Property("RdProgress") + .HasColumnType("INTEGER"); + + b.Property("RdSeeders") + .HasColumnType("INTEGER"); + + b.Property("RdSize") + .HasColumnType("INTEGER"); + + b.Property("RdSpeed") + .HasColumnType("INTEGER"); + + b.Property("RdSplit") + .HasColumnType("INTEGER"); + + b.Property("RdStatus") + .HasColumnType("INTEGER"); + + b.Property("RdStatusRaw") + .HasColumnType("TEXT"); + + b.Property("Retry") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("TorrentRetryAttempts") + .HasColumnType("INTEGER"); + + b.HasKey("TorrentId"); + + b.ToTable("Torrents"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent") + .WithMany("Downloads") + .HasForeignKey("TorrentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Torrent"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Navigation("Downloads"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/RdtClient.Data/Migrations/20220513164723_Settings_Remove_Type.cs b/server/RdtClient.Data/Migrations/20220513164723_Settings_Remove_Type.cs new file mode 100644 index 0000000..3d207ca --- /dev/null +++ b/server/RdtClient.Data/Migrations/20220513164723_Settings_Remove_Type.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RdtClient.Data.Migrations +{ + public partial class Settings_Remove_Type : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Type", + table: "Settings"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Type", + table: "Settings", + type: "TEXT", + nullable: true); + } + } +} diff --git a/server/RdtClient.Data/Migrations/20220513175547_Settings_Migrate.Designer.cs b/server/RdtClient.Data/Migrations/20220513175547_Settings_Migrate.Designer.cs new file mode 100644 index 0000000..31aab7c --- /dev/null +++ b/server/RdtClient.Data/Migrations/20220513175547_Settings_Migrate.Designer.cs @@ -0,0 +1,458 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RdtClient.Data.Data; + +#nullable disable + +namespace RdtClient.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20220513175547_Settings_Migrate")] + partial class Settings_Migrate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.4"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.Property("DownloadId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DownloadFinished") + .HasColumnType("TEXT"); + + b.Property("DownloadQueued") + .HasColumnType("TEXT"); + + b.Property("DownloadStarted") + .HasColumnType("TEXT"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("RemoteId") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("TorrentId") + .HasColumnType("TEXT"); + + b.Property("UnpackingFinished") + .HasColumnType("TEXT"); + + b.Property("UnpackingQueued") + .HasColumnType("TEXT"); + + b.Property("UnpackingStarted") + .HasColumnType("TEXT"); + + b.HasKey("DownloadId"); + + b.HasIndex("TorrentId"); + + b.ToTable("Downloads"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b => + { + b.Property("SettingId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Property("TorrentId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DeleteOnError") + .HasColumnType("INTEGER"); + + b.Property("DownloadAction") + .HasColumnType("INTEGER"); + + b.Property("DownloadManualFiles") + .HasColumnType("TEXT"); + + b.Property("DownloadMinSize") + .HasColumnType("INTEGER"); + + b.Property("DownloadRetryAttempts") + .HasColumnType("INTEGER"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("FileOrMagnet") + .HasColumnType("TEXT"); + + b.Property("FilesSelected") + .HasColumnType("TEXT"); + + b.Property("FinishedAction") + .HasColumnType("INTEGER"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("IsFile") + .HasColumnType("INTEGER"); + + b.Property("Lifetime") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RdAdded") + .HasColumnType("TEXT"); + + b.Property("RdEnded") + .HasColumnType("TEXT"); + + b.Property("RdFiles") + .HasColumnType("TEXT"); + + b.Property("RdHost") + .HasColumnType("TEXT"); + + b.Property("RdId") + .HasColumnType("TEXT"); + + b.Property("RdName") + .HasColumnType("TEXT"); + + b.Property("RdProgress") + .HasColumnType("INTEGER"); + + b.Property("RdSeeders") + .HasColumnType("INTEGER"); + + b.Property("RdSize") + .HasColumnType("INTEGER"); + + b.Property("RdSpeed") + .HasColumnType("INTEGER"); + + b.Property("RdSplit") + .HasColumnType("INTEGER"); + + b.Property("RdStatus") + .HasColumnType("INTEGER"); + + b.Property("RdStatusRaw") + .HasColumnType("TEXT"); + + b.Property("Retry") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("TorrentRetryAttempts") + .HasColumnType("INTEGER"); + + b.HasKey("TorrentId"); + + b.ToTable("Torrents"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent") + .WithMany("Downloads") + .HasForeignKey("TorrentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Torrent"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Navigation("Downloads"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/RdtClient.Data/Migrations/20220513175547_Settings_Migrate.cs b/server/RdtClient.Data/Migrations/20220513175547_Settings_Migrate.cs new file mode 100644 index 0000000..012566d --- /dev/null +++ b/server/RdtClient.Data/Migrations/20220513175547_Settings_Migrate.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RdtClient.Data.Migrations +{ + public partial class Settings_Migrate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'General:LogLevel' WHERE SettingId = 'LogLevel'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'General:DownloadLimit' WHERE SettingId = 'DownloadLimit'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'General:UnpackLimit' WHERE SettingId = 'UnpackLimit'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'General:Categories' WHERE SettingId = 'Categories'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'General:RunOnTorrentCompleteFileName' WHERE SettingId = 'RunOnTorrentCompleteFileName'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'General:RunOnTorrentCompleteArguments' WHERE SettingId = 'RunOnTorrentCompleteArguments'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'DownloadClient:Client' WHERE SettingId = 'DownloadClient'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'DownloadClient:DownloadPath' WHERE SettingId = 'DownloadPath'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'DownloadClient:MappedPath' WHERE SettingId = 'MappedPath'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'DownloadClient:ChunkCount' WHERE SettingId = 'DownloadChunkCount'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'DownloadClient:MaxSpeed' WHERE SettingId = 'DownloadMaxSpeed'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'DownloadClient:Aria2cUrl' WHERE SettingId = 'Aria2cUrl'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'DownloadClient:Aria2cSecret' WHERE SettingId = 'Aria2cSecret'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'DownloadClient:TempPath' WHERE SettingId = 'TempPath'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'DownloadClient:ProxyServer' WHERE SettingId = 'ProxyServer'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:Provider' WHERE SettingId = 'Provider'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:ApiKey' WHERE SettingId = 'RealDebridApiKey'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:AutoImport' WHERE SettingId = 'ProviderAutoImport'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:AutoDelete' WHERE SettingId = 'ProviderAutoDelete'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:Timeout' WHERE SettingId = 'ProviderTimeout'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:CheckInterval' WHERE SettingId = 'ProviderCheckInterval'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:Default:Category' WHERE SettingId = 'ProviderAutoImportCategory'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:Default:OnlyDownloadAvailableFiles' WHERE SettingId = 'OnlyDownloadAvailableFiles'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:Default:MinFileSize' WHERE SettingId = 'MinFileSize'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:Default:TorrentRetryAttempts' WHERE SettingId = 'TorrentRetryAttempts'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:Default:DownloadRetryAttempts' WHERE SettingId = 'DownloadRetryAttempts'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:Default:DeleteOnError' WHERE SettingId = 'ProviderDeleteOnError'"); + migrationBuilder.Sql("UPDATE Settings SET SettingId = 'Provider:Default:TorrentLifetime' WHERE SettingId = 'TorrentLifetime'"); + + migrationBuilder.Sql("UPDATE Settings SET Value = 'True' WHERE SettingId = 'Provider:AutoDelete' AND Value = '1'"); + migrationBuilder.Sql("UPDATE Settings SET Value = 'False' WHERE SettingId = 'Provider:AutoDelete' AND Value = '0'"); + migrationBuilder.Sql("UPDATE Settings SET Value = 'True' WHERE SettingId = 'Provider:AutoImport' AND Value = '1'"); + migrationBuilder.Sql("UPDATE Settings SET Value = 'False' WHERE SettingId = 'Provider:AutoImport' AND Value = '0'"); + migrationBuilder.Sql("UPDATE Settings SET Value = 'True' WHERE SettingId = 'Provider:Default:OnlyDownloadAvailableFiles' AND Value = '1'"); + migrationBuilder.Sql("UPDATE Settings SET Value = 'False' WHERE SettingId = 'Provider:Default:OnlyDownloadAvailableFiles' AND Value = '0'"); + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs index fe8d4ca..ccd5424 100644 --- a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs +++ b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace RdtClient.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.4"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { @@ -269,9 +269,6 @@ namespace RdtClient.Data.Migrations b.Property("SettingId") .HasColumnType("TEXT"); - b.Property("Type") - .HasColumnType("TEXT"); - b.Property("Value") .HasColumnType("TEXT"); diff --git a/server/RdtClient.Data/Models/Data/Setting.cs b/server/RdtClient.Data/Models/Data/Setting.cs index 5f92987..099cec9 100644 --- a/server/RdtClient.Data/Models/Data/Setting.cs +++ b/server/RdtClient.Data/Models/Data/Setting.cs @@ -8,6 +8,4 @@ public class Setting public String SettingId { get; set; } public String Value { get; set; } - - public String Type { get; set; } } \ No newline at end of file diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs index 7a87b8b..fea548e 100644 --- a/server/RdtClient.Data/Models/Internal/DbSettings.cs +++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs @@ -1,33 +1,209 @@ -namespace RdtClient.Data.Models.Internal; +using System.ComponentModel; +using RdtClient.Data.Enums; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global + +namespace RdtClient.Data.Models.Internal; public class DbSettings { - public String Provider { get; set; } - public Int32 ProviderAutoImport { get; set; } - public String ProviderAutoImportCategory { get; set; } - public Int32 ProviderAutoDelete { get; set; } - public Int32 ProviderTimeout { get; set; } - public Int32 ProviderCheckInterval { get; set; } - public String RealDebridApiKey { get; set; } - public String DownloadPath { get; set; } - public String DownloadClient { get; set; } - public String TempPath { get; set; } - public String MappedPath { get; set; } - public Int32 DownloadLimit { get; set; } - public Int32 UnpackLimit { get; set; } - public Int32 MinFileSize { get; set; } - public Int32 OnlyDownloadAvailableFiles { get; set; } - public Int32 DownloadChunkCount { get; set; } - public Int32 DownloadMaxSpeed { get; set; } - public String ProxyServer { get; set; } - public String LogLevel { get; set; } - public String Categories { get; set; } - public String Aria2cUrl { get; set; } - public String Aria2cSecret { get; set; } - public Int32 DownloadRetryAttempts { get; set; } - public Int32 TorrentRetryAttempts { get; set; } - public Int32 DeleteOnError { get; set; } - public Int32 TorrentLifetime { get; set; } - public String RunOnTorrentCompleteFileName { get; set; } - public String RunOnTorrentCompleteArguments { get; set; } + [DisplayName("General")] + [Description("")] + public DbSettingsGeneral General { get; set; } = new DbSettingsGeneral(); + + [DisplayName("Download Client")] + [Description("")] + public DbSettingsDownloadClient DownloadClient { get; set; } = new DbSettingsDownloadClient(); + + [DisplayName("Provider")] + [Description("")] + public DbSettingsProvider Provider { get; set; } = new DbSettingsProvider(); + + [DisplayName("qBittorrent")] + [Description("The following settings only apply when a torrent gets added through the qbittorrent API, usually Radarr or Sonarr.")] + public DbSettingsIntegrations Integrations { get; set; } = new DbSettingsIntegrations(); + + [DisplayName("GUI Defaults")] + [Description("Settings used when adding a torrent through the web interface.")] + public DbSettingsGui Gui { get; set; } = new DbSettingsGui(); + + [DisplayName("Watch")] + [Description("The following settings only apply when a torrent gets through the watch folder.")] + public DbSettingsWatch Watch { get; set; } = new DbSettingsWatch(); +} + +public class DbSettingsGeneral +{ + [DisplayName("Log level")] + [Description("Recommended level is Warning, set to Debug to get the most info.")] + public LogLevel LogLevel { get; set; } = LogLevel.Warning; + + [DisplayName("Maximum parallel downloads")] + [Description("Maximum amount of torrents that get downloaded to your host at the same time.")] + public Int32 DownloadLimit { get; set; } = 2; + + [DisplayName("Maximum unpack processes")] + [Description("Maximum amount of downloads that get unpacked on your host at the same time.")] + public Int32 UnpackLimit { get; set; } = 1; + + [DisplayName("Categories")] + [Description("Expose these categories through the QBittorrent API.")] + public String? Categories { get; set; } = null; + + [DisplayName("Run external program on torrent completion")] + [Description("Path to the executable to run when the torrent and all downloads are finished. No arguments should be passed here.When running in Docker, this command will run on your docker instance!")] + public String? RunOnTorrentCompleteFileName { get; set; } = null; + + [DisplayName("External program arguments")] + [Description(@"When the executable above is executed, use these parameters. +Supports the following parameters: +%N: Torrent name +%L: Category +%F: Content path (same as root path for multifile torrent) +%R: Root path (first torrent subdirectory path) +%D: Save path +%C: Number of files +%Z: Torrent size (bytes) +%I: Info hash")] + public String? RunOnTorrentCompleteArguments { get; set; } = null; +} + +public class DbSettingsDownloadClient +{ + [DisplayName("Download client")] + [Description(@"Select which download client to use, see the +README for the various options.")] + public DownloadClient Client { get; set; } = DownloadClient.Simple; + + [DisplayName("Download path")] + [Description("Path in the docker container to download files to (i.e. /data/downloads), or a local path when using as a service.")] + public String DownloadPath { get; set; } = "/data/downloads"; + + [DisplayName("Mapped path")] + [Description("Path where files are downloaded to on your host (i.e. D:\\Downloads). This path is used for *arr to find your downloads.")] + public String MappedPath { get; set; } = @"C:\Downloads"; + + [DisplayName("Download speed (in MB/s) (only used for the Simple and Multi Downloader)")] + [Description("Maximum download speed in Megabytes per second. When set to 0 unlimited speed is used.")] + public Int32 MaxSpeed { get; set; } = 0; + + [DisplayName("Aria2c URL (only used for the Aria2c Downloader)")] + [Description(@"This is the URL to your Aria2c instance. It must end in /jsonrpc. A common URL is +http://127.0.0.1:6800/jsonrpc.")] + public String Aria2cUrl { get; set; } = "http://127.0.0.1:6800/jsonrpc"; + + [DisplayName("Aria2c Secret (only used for the Aria2c Downloader)")] + [Description("The secret of your Aria2c instance. Optional.")] + public String Aria2cSecret { get; set; } = "mysecret123"; + + [DisplayName("Temp Download path (only used for the Multi Downloader)")] + [Description("Path in the docker container to temporarily download to (i.e. /data/temp). Make sure the docker container has enough disk space if using a path inside the container.")] + public String TempPath { get; set; } = "/data/downloads"; + + [DisplayName("Parallel connections per download (only used for the Multi Downloader)")] + [Description("Maximum amount of parallel threads that are used to download a single torrent to your host. If set to 1 no parallel downloading will be done.")] + public Int32 ChunkCount { get; set; } = 8; + + [DisplayName("Proxy Server")] + [Description("Address of a proxy server to download through (only used for the Multi Downloader).")] + public String? ProxyServer { get; set; } = null; +} + +public class DbSettingsProvider +{ + [DisplayName("Provider")] + [Description(@"The following 2 providers are supported: +https://real-debrid.com +https://alldebrid.com +At this point only 1 provider can be used at the time.")] + public Provider Provider { get; set; } = Provider.RealDebrid; + + [DisplayName("API Key")] + [Description(@"You can find your API key here: +https://real-debrid.com/apitoken +or +https://alldebrid.com/apikeys/.")] + public String ApiKey { get; set; } = ""; + + [DisplayName("Automatically import and process torrents added to provider")] + [Description("When selected, import downloads that are not added through RealDebridClient but have been directly added to Real-Debrid or AllDebrid.")] + public Boolean AutoImport { get; set; } = false; + + [DisplayName("Automatically delete downloads removed from provider")] + [Description("When selected, cancel and delete downloads that have been removed from Real-Debrid or AllDebrid.")] + public Boolean AutoDelete { get; set; } = false; + + [DisplayName("Connection Timeout")] + [Description("Timeout in seconds to make a connection to the provider. Increase if you experience timeouts in the logs.")] + public Int32 Timeout { get; set; } = 10; + + [DisplayName("Check Interval")] + [Description("The interval to check the torrents info on the providers API. Minumum is 3 seconds. When there are no active downloads this limit is increased * 3.")] + public Int32 CheckInterval { get; set; } = 10; + + [DisplayName("Auto Import Defaults")] + public DbSettingsDefaults Default { get; set; } = new DbSettingsDefaults(); +} + +public class DbSettingsIntegrations +{ + public DbSettingsDefaults Default { get; set; } = new DbSettingsDefaults(); +} + +public class DbSettingsGui +{ + public DbSettingsDefaults Default { get; set; } = new DbSettingsDefaults(); +} + +public class DbSettingsWatch +{ + [DisplayName("Watch Path")] + [Description("Watch this path for .torrent or .magnet files. When a file is found it will be automatically imported.")] + public String? Path { get; set; } = null; + + [DisplayName("Check Interval")] + [Description("Time in seconds to check the folder for new files.")] + public Int32 Interval { get; set; } = 60; + + [DisplayName("Import Defaults")] + public DbSettingsDefaults Default { get; set; } = new DbSettingsDefaults(); +} + +public class DbSettingsDefaults +{ + [DisplayName("Category")] + [Description("When a torrent is imported assign it this category.")] + public String? Category { get; set; } = null; + + [DisplayName("Only download available files on debrid provider")] + [Description("When selected, it will only download files in the torrent that have been download by Real-Debrid. You can use this in combination with the Min File size setting above.")] + public Boolean OnlyDownloadAvailableFiles { get; set; } = true; + + [DisplayName("Finished Action")] + [Description("When a torrent is finished, perform this action.")] + public TorrentFinishedAction FinishedAction { get; set; } = TorrentFinishedAction.RemoveAllTorrents; + + [DisplayName("Minimum file size to download")] + [Description("Files that are smaller than this setting are skipped and not downloaded. When set to 0 all files are downloaded. When downloading from Radarr or Sonarr it's recommended to keep this setting at atleast a few MB to avoid the debrid provider having to re-download the torrent.")] + public Int32 MinFileSize { get; set; } = 0; + + [DisplayName("Automatic retry torrent")] + [Description("When a single download has failed multiple times (see setting above) or when the torrent itself received an error it will retry the full torrent this many times before marking it failed.")] + public Int32 TorrentRetryAttempts { get; set; } = 1; + + [DisplayName("Automatic retry downloads")] + [Description("When a single download fails it will retry it this many times before marking it as failed.")] + public Int32 DownloadRetryAttempts { get; set; } = 3; + + [DisplayName("Delete download when in error")] + [Description("When a download has been in error for this many minutes, delete it from the provider and the client. 0 to disable.")] + public Int32 DeleteOnError { get; set; } = 0; + + [DisplayName("Torrent maximum lifetime")] + [Description("The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the torrent is completed and has downloads, the lifetime setting will not apply. 0 to disable.")] + public Int32 TorrentLifetime { get; set; } = 0; + + [DisplayName("Priority")] + [Description("Set the priority of a torrent, 1 = highest, 0 = disabled.")] + public Int32 Priority { get; set; } = 0; } \ No newline at end of file diff --git a/server/RdtClient.Data/Models/Internal/SettingProperty.cs b/server/RdtClient.Data/Models/Internal/SettingProperty.cs new file mode 100644 index 0000000..13930a3 --- /dev/null +++ b/server/RdtClient.Data/Models/Internal/SettingProperty.cs @@ -0,0 +1,11 @@ +namespace RdtClient.Data.Models.Internal; + +public class SettingProperty +{ + public String Key { get; set; } + public Object Value { get; set; } + public String DisplayName { get; set; } + public String Description { get; set; } + public String Type { get; set; } + public Dictionary EnumValues { get; set; } +} \ No newline at end of file diff --git a/server/RdtClient.Data/RdtClient.Data.csproj b/server/RdtClient.Data/RdtClient.Data.csproj index 6d777a1..920b04b 100644 --- a/server/RdtClient.Data/RdtClient.Data.csproj +++ b/server/RdtClient.Data/RdtClient.Data.csproj @@ -19,4 +19,8 @@ + + + + diff --git a/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs b/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs index b8b9a9d..1446f05 100644 --- a/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs +++ b/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs @@ -20,7 +20,10 @@ public class ProviderUpdater : BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + while (!Startup.Ready) + { + await Task.Delay(1000, stoppingToken); + } using var scope = _serviceProvider.CreateScope(); var torrentService = scope.ServiceProvider.GetRequiredService(); @@ -33,11 +36,11 @@ public class ProviderUpdater : BackgroundService { var torrents = await torrentService.Get(); - if (_nextUpdate < DateTime.UtcNow && ((torrents.Count > 0 && Settings.Get.ProviderAutoImport == 0) || Settings.Get.ProviderAutoImport == 1)) + if (_nextUpdate < DateTime.UtcNow && ((torrents.Count > 0 && Settings.Get.Provider.AutoImport) || Settings.Get.Provider.AutoImport)) { _logger.LogDebug($"Updating torrent info from Real-Debrid"); - var updateTime = Settings.Get.ProviderCheckInterval * 3; + var updateTime = Settings.Get.Provider.CheckInterval * 3; if (updateTime < 30) { @@ -46,7 +49,7 @@ public class ProviderUpdater : BackgroundService if (RdtHub.HasConnections) { - updateTime = Settings.Get.ProviderCheckInterval; + updateTime = Settings.Get.Provider.CheckInterval; if (updateTime < 5) { diff --git a/server/RdtClient.Service/BackgroundServices/Startup.cs b/server/RdtClient.Service/BackgroundServices/Startup.cs index 4bffd7f..b978336 100644 --- a/server/RdtClient.Service/BackgroundServices/Startup.cs +++ b/server/RdtClient.Service/BackgroundServices/Startup.cs @@ -5,12 +5,13 @@ using Microsoft.Extensions.Hosting; using RdtClient.Data.Data; using RdtClient.Service.Services; using Serilog; -using Serilog.Events; namespace RdtClient.Service.BackgroundServices; public class Startup : IHostedService { + public static Boolean Ready { get; private set; } + private readonly IServiceProvider _serviceProvider; public Startup(IServiceProvider serviceProvider) @@ -20,36 +21,20 @@ public class Startup : IHostedService public async Task StartAsync(CancellationToken cancellationToken) { + var version = Assembly.GetEntryAssembly()?.GetName().Version; + Log.Warning($"Starting host on version {version}"); + using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.MigrateAsync(cancellationToken); - await dbContext.Seed(); - - var logLevelSettingDb = await dbContext.Settings.FirstOrDefaultAsync(m => m.SettingId == "LogLevel", cancellationToken); - - var logLevelSetting = "Warning"; - - if (logLevelSettingDb != null) - { - logLevelSetting = logLevelSettingDb.Value; - } - - if (!Enum.TryParse(logLevelSetting, out var logLevel)) - { - logLevel = LogEventLevel.Warning; - } - - Settings.LoggingLevelSwitch.MinimumLevel = logLevel; - - var version = Assembly.GetEntryAssembly()?.GetName().Version; - Log.Warning($"Starting host on version {version}"); var settings = scope.ServiceProvider.GetRequiredService(); - + await settings.Seed(); await settings.ResetCache(); - await settings.Clean(); + + Ready = true; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/server/RdtClient.Service/BackgroundServices/TaskRunner.cs b/server/RdtClient.Service/BackgroundServices/TaskRunner.cs index 8bb361b..1b14e3e 100644 --- a/server/RdtClient.Service/BackgroundServices/TaskRunner.cs +++ b/server/RdtClient.Service/BackgroundServices/TaskRunner.cs @@ -18,7 +18,10 @@ public class TaskRunner : BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + while (!Startup.Ready) + { + await Task.Delay(1000, stoppingToken); + } using var scope = _serviceProvider.CreateScope(); var torrentRunner = scope.ServiceProvider.GetRequiredService(); diff --git a/server/RdtClient.Service/BackgroundServices/UpdateChecker.cs b/server/RdtClient.Service/BackgroundServices/UpdateChecker.cs index 12eff0a..e084b18 100644 --- a/server/RdtClient.Service/BackgroundServices/UpdateChecker.cs +++ b/server/RdtClient.Service/BackgroundServices/UpdateChecker.cs @@ -20,6 +20,11 @@ public class UpdateChecker : BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + while (!Startup.Ready) + { + await Task.Delay(1000, stoppingToken); + } + var version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString(); if (String.IsNullOrWhiteSpace(version)) diff --git a/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs b/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs new file mode 100644 index 0000000..6bb447e --- /dev/null +++ b/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs @@ -0,0 +1,112 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using RdtClient.Data.Enums; +using RdtClient.Data.Models.Data; +using RdtClient.Service.Services; + +namespace RdtClient.Service.BackgroundServices; + +public class WatchFolderChecker : BackgroundService +{ + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + + private DateTime _prevCheck = DateTime.MinValue; + + public WatchFolderChecker(ILogger logger, IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!Startup.Ready) + { + await Task.Delay(1000, stoppingToken); + } + + using var scope = _serviceProvider.CreateScope(); + var torrentService = scope.ServiceProvider.GetRequiredService(); + + _logger.LogInformation("ProviderUpdater started."); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await Task.Delay(1000, stoppingToken); + + if (String.IsNullOrWhiteSpace(Settings.Get.Watch.Path)) + { + continue; + } + + var nextCheck = _prevCheck.AddSeconds(Settings.Get.Watch.Interval); + + if (DateTime.UtcNow < nextCheck) + { + continue; + } + + _prevCheck = DateTime.UtcNow; + + var torrentFiles = Directory.GetFiles(Settings.Get.Watch.Path, "*.torrent,*.magnet", SearchOption.TopDirectoryOnly); + + foreach (var torrentFile in torrentFiles) + { + var fileInfo = new FileInfo(torrentFile); + + if (IsFileLocked(fileInfo)) + { + continue; + } + + var torrent = new Torrent + { + Category = Settings.Get.Watch.Default.Category, + DownloadAction = Settings.Get.Watch.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, + FinishedAction = Settings.Get.Watch.Default.FinishedAction, + DownloadMinSize = Settings.Get.Watch.Default.MinFileSize, + TorrentRetryAttempts = Settings.Get.Watch.Default.TorrentRetryAttempts, + DownloadRetryAttempts = Settings.Get.Watch.Default.DownloadRetryAttempts, + DeleteOnError = Settings.Get.Watch.Default.DeleteOnError, + Lifetime = Settings.Get.Watch.Default.TorrentLifetime, + Priority = Settings.Get.Watch.Default.Priority > 0 ? Settings.Get.Watch.Default.Priority : null + }; + + if (fileInfo.Extension == ".torrent") + { + var torrentFileContents = await File.ReadAllBytesAsync(torrentFile, stoppingToken); + await torrentService.UploadFile(torrentFileContents, torrent); + } + else if (fileInfo.Extension == ".magnet") + { + var magnetLink = await File.ReadAllTextAsync(torrentFile, stoppingToken); + await torrentService.UploadMagnet(magnetLink, torrent); + } + } + + } + catch (Exception ex) + { + _logger.LogError(ex, $"Unexpected error occurred in ProviderUpdater: {ex.Message}"); + } + } + } + + private static Boolean IsFileLocked(FileInfo file) + { + try + { + using var stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None); + stream.Close(); + } + catch (IOException e) when ((e.HResult & 0x0000FFFF) == 32) + { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs index 0f351dc..e310cea 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -21,7 +21,7 @@ public class DownloadClient _destinationPath = destinationPath; } - public String Type { get; set; } + public Data.Enums.DownloadClient Type { get; set; } public Boolean Finished { get; private set; } @@ -48,13 +48,13 @@ public class DownloadClient await FileHelper.Delete(filePath); - Type = settings.DownloadClient; + Type = settings.DownloadClient.Client; - Downloader = settings.DownloadClient switch + Downloader = settings.DownloadClient.Client switch { - "Simple" => new SimpleDownloader(_download.Link, filePath), - "MultiPart" => new MultiDownloader(_download.Link, filePath, settings), - "Aria2c" => new Aria2cDownloader(_download.RemoteId, _download.Link, filePath, settings), + Data.Enums.DownloadClient.Simple => new SimpleDownloader(_download.Link, filePath), + Data.Enums.DownloadClient.MultiPart => new MultiDownloader(_download.Link, filePath, settings), + Data.Enums.DownloadClient.Aria2c => new Aria2cDownloader(_download.RemoteId, _download.Link, filePath, settings), _ => throw new Exception($"Unknown download client {settings.DownloadClient}") }; diff --git a/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs b/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs index 3859d8d..c5482ef 100644 --- a/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs @@ -31,7 +31,7 @@ public class Aria2cDownloader : IDownloader Timeout = TimeSpan.FromSeconds(10) }; - _aria2NetClient = new Aria2NetClient(settings.Aria2cUrl, settings.Aria2cSecret, httpClient, 10); + _aria2NetClient = new Aria2NetClient(settings.DownloadClient.Aria2cUrl, settings.DownloadClient.Aria2cSecret, httpClient, 10); } public async Task Download() @@ -162,7 +162,7 @@ public class Aria2cDownloader : IDownloader if (download == null) { - DownloadComplete.Invoke(this, new DownloadCompleteEventArgs + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs { Error = $"Download was not found in Aria2" }); @@ -172,7 +172,7 @@ public class Aria2cDownloader : IDownloader if (!String.IsNullOrWhiteSpace(download.ErrorMessage) || download.Status == "error") { await Remove(); - DownloadComplete.Invoke(this, new DownloadCompleteEventArgs + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs { Error = $"{download.ErrorCode}: {download.ErrorMessage}" }); @@ -190,7 +190,7 @@ public class Aria2cDownloader : IDownloader { if (retryCount >= 10) { - DownloadComplete.Invoke(this, new DownloadCompleteEventArgs + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs { Error = $"File not found at {_filePath}" }); @@ -199,7 +199,7 @@ public class Aria2cDownloader : IDownloader if (File.Exists(_filePath)) { - DownloadComplete.Invoke(this, new DownloadCompleteEventArgs()); + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs()); break; } @@ -209,7 +209,7 @@ public class Aria2cDownloader : IDownloader return; } - DownloadProgress.Invoke(this, new DownloadProgressEventArgs + DownloadProgress?.Invoke(this, new DownloadProgressEventArgs { BytesDone = download.CompletedLength, BytesTotal = download.TotalLength, diff --git a/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs b/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs index 3ed1ec2..0eea16e 100644 --- a/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs @@ -23,21 +23,21 @@ public class MultiDownloader : IDownloader _uri = uri; _filePath = filePath; - var settingTempPath = settings.TempPath; + var settingTempPath = settings.DownloadClient.TempPath; if (String.IsNullOrWhiteSpace(settingTempPath)) { settingTempPath = Path.GetTempPath(); } - var settingDownloadChunkCount = settings.DownloadChunkCount; + var settingDownloadChunkCount = settings.DownloadClient.ChunkCount; if (settingDownloadChunkCount <= 0) { settingDownloadChunkCount = 1; } - var settingDownloadMaxSpeed = settings.DownloadMaxSpeed; + var settingDownloadMaxSpeed = settings.DownloadClient.MaxSpeed; if (settingDownloadMaxSpeed <= 0) { @@ -46,7 +46,7 @@ public class MultiDownloader : IDownloader settingDownloadMaxSpeed = settingDownloadMaxSpeed * 1024 * 1024; - var settingProxyServer = settings.ProxyServer; + var settingProxyServer = settings.DownloadClient.ProxyServer; var downloadOpt = new DownloadConfiguration { diff --git a/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs b/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs index 07da967..e50f269 100644 --- a/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs @@ -74,7 +74,7 @@ public class SimpleDownloader : IDownloader throw new IOException("No stream"); } - var speedLimit = Settings.Get.DownloadMaxSpeed; + var speedLimit = Settings.Get.DownloadClient.MaxSpeed; await using var destinationStream = new ThrottledStream(responseStream, speedLimit * 1000L * 1000L); @@ -108,9 +108,9 @@ public class SimpleDownloader : IDownloader BytesTotal = _bytesTotal }); - if (Settings.Get.DownloadMaxSpeed != speedLimit) + if (Settings.Get.DownloadClient.MaxSpeed != speedLimit) { - speedLimit = Settings.Get.DownloadMaxSpeed; + speedLimit = Settings.Get.DownloadClient.MaxSpeed; destinationStream.BandwidthLimit = speedLimit * 1000L * 1000L; } } @@ -139,11 +139,11 @@ public class SimpleDownloader : IDownloader throw new Exception($"Download timed out"); } - DownloadComplete.Invoke(this, new DownloadCompleteEventArgs()); + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs()); } catch (Exception ex) { - DownloadComplete.Invoke(this, new DownloadCompleteEventArgs + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs { Error = ex.Message }); diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index 5e42893..e97c559 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -1,5 +1,6 @@ using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; +using RdtClient.Data.Models.Internal; using RdtClient.Data.Models.QBittorrent; namespace RdtClient.Service.Services; @@ -194,7 +195,7 @@ public class QBittorrent public String AppDefaultSavePath() { - var downloadPath = Settings.Get.MappedPath; + var downloadPath = Settings.Get.DownloadClient.MappedPath; downloadPath = downloadPath.TrimEnd('\\') .TrimEnd('/'); @@ -431,15 +432,15 @@ public class QBittorrent { var torrent = new Torrent { - Category = category, - DownloadAction = Settings.Get.OnlyDownloadAvailableFiles == 1 ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, - FinishedAction = TorrentFinishedAction.None, - DownloadMinSize = Settings.Get.MinFileSize, - TorrentRetryAttempts = Settings.Get.TorrentRetryAttempts, - DownloadRetryAttempts = Settings.Get.DownloadRetryAttempts, - DeleteOnError = Settings.Get.DeleteOnError, - Lifetime = Settings.Get.TorrentLifetime, - Priority = priority + Category = category ?? Settings.Get.Integrations.Default.Category, + DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, + FinishedAction = Settings.Get.Integrations.Default.FinishedAction, + DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize, + TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts, + DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts, + DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError, + Lifetime = Settings.Get.Integrations.Default.TorrentLifetime, + Priority = priority ?? (Settings.Get.Integrations.Default.Priority > 0 ? Settings.Get.Integrations.Default.Priority : null) }; await _torrents.UploadMagnet(magnetLink, torrent); @@ -449,15 +450,15 @@ public class QBittorrent { var torrent = new Torrent { - Category = category, - DownloadAction = Settings.Get.OnlyDownloadAvailableFiles == 1 ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, - FinishedAction = TorrentFinishedAction.None, - DownloadMinSize = Settings.Get.MinFileSize, - TorrentRetryAttempts = Settings.Get.TorrentRetryAttempts, - DownloadRetryAttempts = Settings.Get.DownloadRetryAttempts, - DeleteOnError = Settings.Get.DeleteOnError, - Lifetime = Settings.Get.TorrentLifetime, - Priority = priority + Category = category ?? Settings.Get.Integrations.Default.Category, + DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, + FinishedAction = Settings.Get.Integrations.Default.FinishedAction, + DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize, + TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts, + DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts, + DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError, + Lifetime = Settings.Get.Integrations.Default.TorrentLifetime, + Priority = priority ?? (Settings.Get.Integrations.Default.Priority > 0 ? Settings.Get.Integrations.Default.Priority : null) }; await _torrents.UploadFile(fileBytes, torrent); @@ -476,11 +477,13 @@ public class QBittorrent .Select(m => m.Category.ToLower()) .ToList(); - var categories = Settings.Get - .Categories - .Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var categoryList = (Settings.Get.General.Categories ?? "") + .Split(",", StringSplitOptions.RemoveEmptyEntries) + .Distinct(StringComparer.CurrentCultureIgnoreCase) + .Select(m => m.Trim()) + .ToList(); - torrentsToGroup.AddRange(categories); + torrentsToGroup.AddRange(categoryList); var results = new Dictionary(); @@ -500,48 +503,53 @@ public class QBittorrent public async Task CategoryCreate(String category) { - var categoriesSetting = Settings.Get.Categories; - - if (String.IsNullOrWhiteSpace(categoriesSetting)) - { - categoriesSetting = category; - } - else - { - var categoryList = categoriesSetting - .Split(",", StringSplitOptions.RemoveEmptyEntries) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .ToList(); - - if (!categoryList.Contains(category)) - { - categoryList.Add(category); - } - - categoriesSetting = String.Join(",", categoryList); - } - - await _settings.UpdateString("Categories", categoriesSetting); - } - - public async Task CategoryRemove(String category) - { - var categoriesSetting = Settings.Get.Categories; - - if (String.IsNullOrWhiteSpace(categoriesSetting)) + if (category == null) { return; } - var categoryList = categoriesSetting.Split(",", StringSplitOptions.RemoveEmptyEntries) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .ToList(); + category = category.Trim(); + + var categoriesSetting = Settings.Get.General.Categories; + + var categoryList = (categoriesSetting ?? "") + .Split(",", StringSplitOptions.RemoveEmptyEntries) + .Distinct(StringComparer.CurrentCultureIgnoreCase) + .Select(m => m.Trim()) + .ToList(); + + if (!categoryList.Contains(category)) + { + categoryList.Add(category); + } + + categoriesSetting = String.Join(",", categoryList); + + await _settings.Update("General:Categories", categoriesSetting); + } + + public async Task CategoryRemove(String category) + { + if (category == null) + { + return; + } + + category = category.Trim(); + + var categoriesSetting = Settings.Get.General.Categories; + + var categoryList = (categoriesSetting ?? "") + .Split(",", StringSplitOptions.RemoveEmptyEntries) + .Distinct(StringComparer.CurrentCultureIgnoreCase) + .Select(m => m.Trim()) + .ToList(); categoryList = categoryList.Where(m => m != category).ToList(); categoriesSetting = String.Join(",", categoryList); - await _settings.UpdateString("Categories", categoriesSetting); + await _settings.Update("General:Categories", categoriesSetting); } public async Task TorrentsTopPrio(String hash) diff --git a/server/RdtClient.Service/Services/Settings.cs b/server/RdtClient.Service/Services/Settings.cs index e618588..a4bf65a 100644 --- a/server/RdtClient.Service/Services/Settings.cs +++ b/server/RdtClient.Service/Services/Settings.cs @@ -1,9 +1,11 @@ using System.Diagnostics; using Aria2NET; using RdtClient.Data.Data; +using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Internal; using RdtClient.Service.Helpers; +using RdtClient.Service.Services.Downloaders; using Serilog.Core; using Serilog.Events; @@ -22,19 +24,19 @@ public class Settings public static DbSettings Get => SettingData.Get; - public async Task> GetAll() + public IList GetAll() { - return await _settingData.GetAll(); + return _settingData.GetAll(); } - public async Task Update(IList settings) + public async Task Update(IList settings) { await _settingData.Update(settings); } - public async Task UpdateString(String key, String value) + public async Task Update(String settingId, Object value) { - await _settingData.UpdateString(key, value); + await _settingData.Update(settingId, value); } public async Task TestPath(String path) @@ -60,7 +62,7 @@ public class Settings public async Task TestDownloadSpeed(CancellationToken cancellationToken) { - var downloadPath = Get.DownloadPath; + var downloadPath = Get.DownloadClient.DownloadPath; var testFilePath = Path.Combine(downloadPath, "testDefault.rar"); @@ -79,18 +81,29 @@ public class Settings await downloadClient.Start(Get); + var httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(10) + }; + while (!downloadClient.Finished) { -#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods that take one - // ReSharper disable once MethodSupportsCancellation - await Task.Delay(10); -#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods that take one - + await Task.Delay(1000, CancellationToken.None); + if (cancellationToken.IsCancellationRequested) { await downloadClient.Cancel(); } + if (downloadClient.Downloader is Aria2cDownloader aria2Downloader) + { + var aria2NetClient = new Aria2NetClient(Settings.Get.DownloadClient.Aria2cUrl, Settings.Get.DownloadClient.Aria2cSecret, httpClient, 1); + + var allDownloads = await aria2NetClient.TellAll(cancellationToken); + + await aria2Downloader.Update(allDownloads); + } + if (downloadClient.BytesDone > 1024 * 1024 * 50) { await downloadClient.Cancel(); @@ -108,7 +121,7 @@ public class Settings public async Task TestWriteSpeed() { - var downloadPath = Get.DownloadPath; + var downloadPath = Get.DownloadClient.DownloadPath; var testFilePath = Path.Combine(downloadPath, "test.tmp"); @@ -155,7 +168,7 @@ public class Settings { try { - var tempPath = Get.TempPath; + var tempPath = Get.DownloadClient.TempPath; if (!String.IsNullOrWhiteSpace(tempPath)) { @@ -173,8 +186,22 @@ public class Settings } } + public async Task Seed() + { + await _settingData.Seed(); + } + public async Task ResetCache() { await _settingData.ResetCache(); + + LoggingLevelSwitch.MinimumLevel = Settings.Get.General.LogLevel switch + { + LogLevel.Debug => LogEventLevel.Debug, + LogLevel.Information => LogEventLevel.Information, + LogLevel.Warning => LogEventLevel.Warning, + LogLevel.Error => LogEventLevel.Error, + _ => LogEventLevel.Warning + }; } } \ No newline at end of file diff --git a/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs index 6ecb511..0b7a20a 100644 --- a/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs @@ -26,7 +26,7 @@ public class AllDebridTorrentClient : ITorrentClient { try { - var apiKey = Settings.Get.RealDebridApiKey; + var apiKey = Settings.Get.Provider.ApiKey; if (String.IsNullOrWhiteSpace(apiKey)) { diff --git a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs index 50bb2bf..55157ce 100644 --- a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs @@ -11,7 +11,7 @@ public class RealDebridTorrentClient : ITorrentClient { private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; - private TimeSpan? _offset = null; + private TimeSpan? _offset; public RealDebridTorrentClient(ILogger logger, IHttpClientFactory httpClientFactory) { @@ -23,7 +23,7 @@ public class RealDebridTorrentClient : ITorrentClient { try { - var apiKey = Settings.Get.RealDebridApiKey; + var apiKey = Settings.Get.Provider.ApiKey; if (String.IsNullOrWhiteSpace(apiKey)) { @@ -31,7 +31,7 @@ public class RealDebridTorrentClient : ITorrentClient } var httpClient = _httpClientFactory.CreateClient(); - httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.ProviderTimeout); + httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout); var rdtNetClient = new RdNetClient(null, httpClient, 5); rdtNetClient.UseApiAuthentication(apiKey); @@ -73,7 +73,7 @@ public class RealDebridTorrentClient : ITorrentClient Split = torrent.Split, Progress = torrent.Progress, Status = torrent.Status, - Added = ChangeTimeZone(torrent.Added).Value, + Added = ChangeTimeZone(torrent.Added)!.Value, Files = (torrent.Files ?? new List()).Select(m => new TorrentClientFile { Path = m.Path, diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index 65f6ddc..556a2de 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -44,8 +44,8 @@ public class TorrentRunner if (settingsCopy != null) { - settingsCopy.RealDebridApiKey = "*****"; - settingsCopy.Aria2cSecret = "*****"; + settingsCopy.Provider.ApiKey = "*****"; + settingsCopy.DownloadClient.Aria2cSecret = "*****"; Log(JsonSerializer.Serialize(settingsCopy)); } @@ -82,25 +82,25 @@ public class TorrentRunner public async Task Tick() { - if (String.IsNullOrWhiteSpace(Settings.Get.RealDebridApiKey)) + if (String.IsNullOrWhiteSpace(Settings.Get.Provider.ApiKey)) { Log($"No RealDebridApiKey set in settings"); return; } - var settingDownloadLimit = Settings.Get.DownloadLimit; + var settingDownloadLimit = Settings.Get.General.DownloadLimit; if (settingDownloadLimit < 1) { settingDownloadLimit = 1; } - var settingUnpackLimit = Settings.Get.UnpackLimit; + var settingUnpackLimit = Settings.Get.General.UnpackLimit; if (settingUnpackLimit < 1) { settingUnpackLimit = 1; } - var settingDownloadPath = Settings.Get.DownloadPath; + var settingDownloadPath = Settings.Get.DownloadClient.DownloadPath; if (String.IsNullOrWhiteSpace(settingDownloadPath)) { _logger.LogError("No DownloadPath set in settings"); @@ -115,11 +115,11 @@ public class TorrentRunner Log($"TorrentRunner Tick Start, {ActiveDownloadClients.Count} active downloads, {ActiveUnpackClients.Count} active unpacks"); } - if (ActiveDownloadClients.Any(m => m.Value.Type == "Aria2c")) + if (ActiveDownloadClients.Any(m => m.Value.Type == Data.Enums.DownloadClient.Aria2c)) { Log("Updating Aria2 status"); - var aria2NetClient = new Aria2NetClient(Settings.Get.Aria2cUrl, Settings.Get.Aria2cSecret, _httpClient, 1); + var aria2NetClient = new Aria2NetClient(Settings.Get.DownloadClient.Aria2cUrl, Settings.Get.DownloadClient.Aria2cSecret, _httpClient, 1); var allDownloads = await aria2NetClient.TellAll(); diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index c1c14e4..d7d4361 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -38,10 +38,10 @@ public class Torrents _torrentData = torrentData; _downloads = downloads; - _torrentClient = Settings.Get.Provider switch + _torrentClient = Settings.Get.Provider.Provider switch { - "RealDebrid" => realDebridTorrentClient, - "AllDebrid" => allDebridTorrentClient, + Provider.RealDebrid => realDebridTorrentClient, + Provider.AllDebrid => allDebridTorrentClient, _ => null }; } @@ -329,7 +329,7 @@ public class Torrents var profile = new Profile { - Provider = Settings.Get.Provider, + Provider = Enum.GetName(Settings.Get.Provider.Provider), UserName = user.Username, Expiration = user.Expiration, CurrentVersion = UpdateChecker.CurrentVersion, @@ -354,19 +354,19 @@ public class Torrents var torrent = torrents.FirstOrDefault(m => m.RdId == rdTorrent.Id); // Auto import torrents only torrents that have their files selected - if (torrent == null && Settings.Get.ProviderAutoImport == 1) + if (torrent == null && Settings.Get.Provider.AutoImport) { var newTorrent = new Torrent { - Category = Settings.Get.ProviderAutoImportCategory, - DownloadAction = TorrentDownloadAction.DownloadManual, - FinishedAction = TorrentFinishedAction.None, - DownloadMinSize = 0, - TorrentRetryAttempts = 0, - DownloadRetryAttempts = Settings.Get.DownloadRetryAttempts, - DeleteOnError = Settings.Get.DeleteOnError, - Lifetime = Settings.Get.TorrentLifetime, - Priority = 0, + Category = Settings.Get.Provider.Default.Category, + DownloadAction = Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, + FinishedAction = Settings.Get.Provider.Default.FinishedAction, + DownloadMinSize = Settings.Get.Provider.Default.MinFileSize, + TorrentRetryAttempts = Settings.Get.Provider.Default.TorrentRetryAttempts, + DownloadRetryAttempts = Settings.Get.Provider.Default.DownloadRetryAttempts, + DeleteOnError = Settings.Get.Provider.Default.DeleteOnError, + Lifetime = Settings.Get.Provider.Default.TorrentLifetime, + Priority = Settings.Get.Provider.Default.Priority > 0 ? Settings.Get.Provider.Default.Priority : null, RdId = rdTorrent.Id }; @@ -389,7 +389,7 @@ public class Torrents { var rdTorrent = rdTorrents.FirstOrDefault(m => m.Id == torrent.RdId); - if (rdTorrent == null && Settings.Get.ProviderAutoDelete == 1) + if (rdTorrent == null && Settings.Get.Provider.AutoDelete) { await Delete(torrent.TorrentId, true, false, true); } @@ -574,7 +574,7 @@ public class Torrents private static String DownloadPath(Torrent torrent) { - var settingDownloadPath = Settings.Get.DownloadPath; + var settingDownloadPath = Settings.Get.DownloadClient.DownloadPath; if (!String.IsNullOrWhiteSpace(torrent.Category)) { @@ -624,7 +624,7 @@ public class Torrents public async Task RunTorrentComplete(Guid torrentId) { - if (String.IsNullOrWhiteSpace(Settings.Get.RunOnTorrentCompleteFileName)) + if (String.IsNullOrWhiteSpace(Settings.Get.General.RunOnTorrentCompleteFileName)) { return; } @@ -632,8 +632,8 @@ public class Torrents var torrent = await _torrentData.GetById(torrentId); var downloads = await _downloads.GetForTorrent(torrentId); - var fileName = Settings.Get.RunOnTorrentCompleteFileName; - var arguments = Settings.Get.RunOnTorrentCompleteArguments ?? ""; + var fileName = Settings.Get.General.RunOnTorrentCompleteFileName; + var arguments = Settings.Get.General.RunOnTorrentCompleteArguments ?? ""; Log($"Parsing external program {fileName} with arguments {arguments}", torrent); diff --git a/server/RdtClient.Web/Controllers/AuthController.cs b/server/RdtClient.Web/Controllers/AuthController.cs index 7b75ac9..09eb5fc 100644 --- a/server/RdtClient.Web/Controllers/AuthController.cs +++ b/server/RdtClient.Web/Controllers/AuthController.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using RdtClient.Data.Models.Data; +using RdtClient.Data.Models.Internal; using RdtClient.Service.Services; namespace RdtClient.Web.Controllers; @@ -8,10 +10,12 @@ namespace RdtClient.Web.Controllers; public class AuthController : Controller { private readonly Authentication _authentication; + private readonly Settings _settings; - public AuthController(Authentication authentication) + public AuthController(Authentication authentication, Settings settings) { _authentication = authentication; + _settings = settings; } [AllowAnonymous] @@ -33,7 +37,7 @@ public class AuthController : Controller return Ok(); } - + [AllowAnonymous] [Route("Create")] [HttpPost] @@ -58,6 +62,22 @@ public class AuthController : Controller return Ok(); } + [AllowAnonymous] + [Route("SetupProvider")] + [HttpPost] + public async Task SetupProvider([FromBody] AuthControllerSetupProviderRequest request) + { + if (!String.IsNullOrEmpty(Settings.Get.Provider.ApiKey)) + { + return StatusCode(401); + } + + await _settings.Update("Provider:Provider", request.Provider); + await _settings.Update("Provider:ApiKey", request.Token); + + return Ok(); + } + [AllowAnonymous] [Route("Login")] [HttpPost] @@ -109,6 +129,12 @@ public class AuthControllerLoginRequest public String Password { get; set; } } +public class AuthControllerSetupProviderRequest +{ + public Int32 Provider { get; set; } + public String Token { get; set; } +} + public class AuthControllerUpdateRequest { public String UserName { get; set; } diff --git a/server/RdtClient.Web/Controllers/SettingsController.cs b/server/RdtClient.Web/Controllers/SettingsController.cs index 959dccd..a4aafa1 100644 --- a/server/RdtClient.Web/Controllers/SettingsController.cs +++ b/server/RdtClient.Web/Controllers/SettingsController.cs @@ -1,9 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Internal; using RdtClient.Service.Services; -using Serilog.Events; namespace RdtClient.Web.Controllers; @@ -22,25 +20,18 @@ public class SettingsController : Controller [HttpGet] [Route("")] - public async Task>> Get() + public ActionResult Get() { - var result = await _settings.GetAll(); + var result = _settings.GetAll(); return Ok(result); } [HttpPut] [Route("")] - public async Task Update([FromBody] SettingsControllerUpdateRequest request) + public async Task Update([FromBody] IList settings) { - await _settings.Update(request.Settings); - - if (!Enum.TryParse(Settings.Get.LogLevel, out var logLevel)) - { - logLevel = LogEventLevel.Information; - } - - Settings.LoggingLevelSwitch.MinimumLevel = logLevel; - + await _settings.Update(settings); + return Ok(); } @@ -89,11 +80,6 @@ public class SettingsController : Controller } } -public class SettingsControllerUpdateRequest -{ - public IList Settings { get; set; } -} - public class SettingsControllerTestPathRequest { public String Path { get; set; }