From f77c1860def3b5223423b5d5c3945be3b968ea5c Mon Sep 17 00:00:00 2001 From: Roger Far Date: Sat, 4 Apr 2020 15:12:04 -0600 Subject: [PATCH] Added most functionality on the client and serverside --- client/angular-proxy.json | 6 + client/angular.json | 28 +- client/package-lock.json | 23 + client/package.json | 3 + client/src/app/app-routing.module.ts | 17 +- client/src/app/app.component.ts | 4 +- client/src/app/app.module.ts | 39 +- client/src/app/auth.guard.ts | 27 + client/src/app/file-status.pipe.ts | 25 + client/src/app/login/login.component.html | 36 ++ .../login.component.scss} | 0 client/src/app/login/login.component.ts | 15 + .../main-layout/main-layout.component.html | 6 + .../main-layout/main-layout.component.scss | 3 + .../app/main-layout/main-layout.component.ts | 12 + client/src/app/models/download.model.ts | 19 + client/src/app/models/profile.model.ts | 4 + client/src/app/models/setting.model.ts | 4 + client/src/app/models/torrent.model.ts | 41 ++ .../add-new-torrent.component.html | 66 +++ .../add-new-torrent.component.scss | 4 + .../add-new-torrent.component.ts | 91 ++++ client/src/app/navbar/navbar.component.html | 77 +++ .../navbar.component.scss} | 0 client/src/app/navbar/navbar.component.ts | 26 + .../navbar/settings/settings.component.html | 79 +++ .../navbar/settings/settings.component.scss} | 0 .../app/navbar/settings/settings.component.ts | 103 ++++ client/src/app/settings.service.ts | 24 + .../torrent-file/torrent-file.component.html | 10 + .../torrent-file/torrent-file.component.scss | 3 + .../torrent-file/torrent-file.component.ts | 17 + .../torrent-row/torrent-row.component.html | 27 + .../torrent-row/torrent-row.component.scss | 4 + .../app/torrent-row/torrent-row.component.ts | 36 ++ client/src/app/torrent-status.pipe.ts | 28 + .../torrent-table.component.html | 48 ++ .../torrent-table.component.scss | 5 + .../torrent-table/torrent-table.component.ts | 61 +++ client/src/app/torrent.service.ts | 37 ++ client/src/app/users.service.ts | 10 + client/src/assets/logo.png | Bin 0 -> 15087 bytes client/src/styles.scss | 3 +- server/RdtClient.Data/Data/DataContext.cs | 60 ++ server/RdtClient.Data/Data/DownloadData.cs | 71 +++ server/RdtClient.Data/Data/SettingData.cs | 53 ++ server/RdtClient.Data/Data/TorrentData.cs | 101 ++++ .../RdtClient.Data/DataContext/DataContext.cs | 9 - server/RdtClient.Data/DiConfig.cs | 5 +- server/RdtClient.Data/Enums/DownloadStatus.cs | 9 + server/RdtClient.Data/Enums/TorrentStatus.cs | 12 + .../20200403195110_Initial.Designer.cs | 395 ++++++++++++++ .../Migrations/20200403195110_Initial.cs | 305 +++++++++++ .../Migrations/DataContextModelSnapshot.cs | 387 +++++++++++++ server/RdtClient.Data/Models/Data/Download.cs | 27 + server/RdtClient.Data/Models/Data/Setting.cs | 15 + server/RdtClient.Data/Models/Data/Torrent.cs | 60 ++ .../Models/Internal/AppSettings.cs | 10 +- server/RdtClient.Data/RdtClient.Data.csproj | 33 ++ server/RdtClient.Service/DiConfig.cs | 8 +- server/RdtClient.Service/Models/Profile.cs | 10 + .../Properties/launchSettings.json | 27 + .../RdtClient.Service.csproj | 16 +- .../RdtClient.Service.csproj.user | 6 + .../Services/DownloadManager.cs | 147 +++++ .../RdtClient.Service/Services/Downloads.cs | 49 ++ .../RdtClient.Service/Services/Scheduler.cs | 62 +++ server/RdtClient.Service/Services/Settings.cs | 60 ++ server/RdtClient.Service/Services/Torrents.cs | 277 ++++++++++ .../Controllers/SettingsController.cs | 75 +++ .../Controllers/TorrentsController.cs | 143 +++++ server/RdtClient.Web/Program.cs | 14 +- .../Properties/launchSettings.json | 16 +- server/RdtClient.Web/RdtClient.Web.csproj | 5 +- server/RdtClient.Web/Startup.cs | 26 +- server/RdtClient.Web/app.log | 512 ++++++++++++++++++ server/RdtClient.Web/appsettings.json | 14 +- server/libs/RDNET.deps.json | 63 +++ server/libs/RDNET.dll | Bin 0 -> 66560 bytes server/libs/RDNET.pdb | Bin 0 -> 16600 bytes 80 files changed, 4081 insertions(+), 72 deletions(-) create mode 100644 client/angular-proxy.json create mode 100644 client/src/app/auth.guard.ts create mode 100644 client/src/app/file-status.pipe.ts create mode 100644 client/src/app/login/login.component.html rename client/src/app/{app.component.html => login/login.component.scss} (100%) create mode 100644 client/src/app/login/login.component.ts create mode 100644 client/src/app/main-layout/main-layout.component.html create mode 100644 client/src/app/main-layout/main-layout.component.scss create mode 100644 client/src/app/main-layout/main-layout.component.ts create mode 100644 client/src/app/models/download.model.ts create mode 100644 client/src/app/models/profile.model.ts create mode 100644 client/src/app/models/setting.model.ts create mode 100644 client/src/app/models/torrent.model.ts create mode 100644 client/src/app/navbar/add-new-torrent/add-new-torrent.component.html create mode 100644 client/src/app/navbar/add-new-torrent/add-new-torrent.component.scss create mode 100644 client/src/app/navbar/add-new-torrent/add-new-torrent.component.ts create mode 100644 client/src/app/navbar/navbar.component.html rename client/src/app/{app.component.scss => navbar/navbar.component.scss} (100%) create mode 100644 client/src/app/navbar/navbar.component.ts create mode 100644 client/src/app/navbar/settings/settings.component.html rename client/src/{assets/.gitkeep => app/navbar/settings/settings.component.scss} (100%) create mode 100644 client/src/app/navbar/settings/settings.component.ts create mode 100644 client/src/app/settings.service.ts create mode 100644 client/src/app/torrent-file/torrent-file.component.html create mode 100644 client/src/app/torrent-file/torrent-file.component.scss create mode 100644 client/src/app/torrent-file/torrent-file.component.ts create mode 100644 client/src/app/torrent-row/torrent-row.component.html create mode 100644 client/src/app/torrent-row/torrent-row.component.scss create mode 100644 client/src/app/torrent-row/torrent-row.component.ts create mode 100644 client/src/app/torrent-status.pipe.ts create mode 100644 client/src/app/torrent-table/torrent-table.component.html create mode 100644 client/src/app/torrent-table/torrent-table.component.scss create mode 100644 client/src/app/torrent-table/torrent-table.component.ts create mode 100644 client/src/app/torrent.service.ts create mode 100644 client/src/app/users.service.ts create mode 100644 client/src/assets/logo.png create mode 100644 server/RdtClient.Data/Data/DataContext.cs create mode 100644 server/RdtClient.Data/Data/DownloadData.cs create mode 100644 server/RdtClient.Data/Data/SettingData.cs create mode 100644 server/RdtClient.Data/Data/TorrentData.cs delete mode 100644 server/RdtClient.Data/DataContext/DataContext.cs create mode 100644 server/RdtClient.Data/Enums/DownloadStatus.cs create mode 100644 server/RdtClient.Data/Enums/TorrentStatus.cs create mode 100644 server/RdtClient.Data/Migrations/20200403195110_Initial.Designer.cs create mode 100644 server/RdtClient.Data/Migrations/20200403195110_Initial.cs create mode 100644 server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs create mode 100644 server/RdtClient.Data/Models/Data/Download.cs create mode 100644 server/RdtClient.Data/Models/Data/Setting.cs create mode 100644 server/RdtClient.Data/Models/Data/Torrent.cs create mode 100644 server/RdtClient.Service/Models/Profile.cs create mode 100644 server/RdtClient.Service/Properties/launchSettings.json create mode 100644 server/RdtClient.Service/RdtClient.Service.csproj.user create mode 100644 server/RdtClient.Service/Services/DownloadManager.cs create mode 100644 server/RdtClient.Service/Services/Downloads.cs create mode 100644 server/RdtClient.Service/Services/Scheduler.cs create mode 100644 server/RdtClient.Service/Services/Settings.cs create mode 100644 server/RdtClient.Service/Services/Torrents.cs create mode 100644 server/RdtClient.Web/Controllers/SettingsController.cs create mode 100644 server/RdtClient.Web/Controllers/TorrentsController.cs create mode 100644 server/RdtClient.Web/app.log create mode 100644 server/libs/RDNET.deps.json create mode 100644 server/libs/RDNET.dll create mode 100644 server/libs/RDNET.pdb diff --git a/client/angular-proxy.json b/client/angular-proxy.json new file mode 100644 index 0000000..009373c --- /dev/null +++ b/client/angular-proxy.json @@ -0,0 +1,6 @@ +{ + "/Api": { + "target": "http://localhost:4201/", + "secure": false + } +} diff --git a/client/angular.json b/client/angular.json index a521dc8..92c90da 100644 --- a/client/angular.json +++ b/client/angular.json @@ -23,13 +23,8 @@ "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "aot": true, - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.scss" - ], + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], "scripts": [] }, "configurations": { @@ -66,7 +61,8 @@ "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "browserTarget": "rdt-client:build" + "browserTarget": "rdt-client:build", + "proxyConfig": "angular-proxy.json" }, "configurations": { "production": { @@ -87,13 +83,8 @@ "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.scss" - ], + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], "scripts": [] } }, @@ -105,9 +96,7 @@ "tsconfig.spec.json", "e2e/tsconfig.json" ], - "exclude": [ - "**/node_modules/**" - ] + "exclude": ["**/node_modules/**"] } }, "e2e": { @@ -123,6 +112,7 @@ } } } - }}, + } + }, "defaultProject": "rdt-client" } diff --git a/client/package-lock.json b/client/package-lock.json index ae1d2c7..8b3e947 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1371,6 +1371,11 @@ "to-fast-properties": "^2.0.0" } }, + "@fortawesome/fontawesome-free": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz", + "integrity": "sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==" + }, "@istanbuljs/schema": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", @@ -2659,6 +2664,11 @@ "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", "dev": true }, + "bulma": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.8.1.tgz", + "integrity": "sha512-Afi2zv4DKmNSYfmx55V+Mtnt8+WfR8Rs65kWArmzEuWP7vNr7dSAEDI+ORZlgOR1gueNZwpKaPdUi4ZiTNwgPA==" + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -4807,6 +4817,11 @@ "minimatch": "^3.0.3" } }, + "filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -7377,6 +7392,14 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, + "ngx-filesize": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/ngx-filesize/-/ngx-filesize-2.0.13.tgz", + "integrity": "sha512-rdOJMmJTou8tjiMLqxybDD1SWrQETbioXE9InlxABuxGJO0eEWzLfXXxMvNj2o92Wo6YqbkkQwi2KEOk7XvFZw==", + "requires": { + "filesize": ">= 4.0.0" + } + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", diff --git a/client/package.json b/client/package.json index aefddd8..d4695e8 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,9 @@ "@angular/platform-browser": "~9.1.0", "@angular/platform-browser-dynamic": "~9.1.0", "@angular/router": "~9.1.0", + "@fortawesome/fontawesome-free": "^5.13.0", + "bulma": "^0.8.1", + "ngx-filesize": "^2.0.13", "rxjs": "~6.5.4", "tslib": "^1.10.0", "zone.js": "~0.10.2" diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index defa9f2..6dd65c9 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -1,10 +1,23 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { MainLayoutComponent } from './main-layout/main-layout.component'; +import { LoginComponent } from './login/login.component'; +import { AuthGuard } from './auth.guard'; -const routes: Routes = []; +const routes: Routes = [ + { + path: 'login', + component: LoginComponent, + }, + { + path: '', + component: MainLayoutComponent, + canActivate: [AuthGuard], + }, +]; @NgModule({ imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] + exports: [RouterModule], }) export class AppRoutingModule {} diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 07b5f6b..3f25e4c 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core'; @Component({ selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + template: '', + styles: [] }) export class AppComponent {} diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 70dd2dd..ceaad1d 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -1,13 +1,44 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; +import { NgxFilesizeModule, FileSizePipe } from 'ngx-filesize'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; +import { MainLayoutComponent } from './main-layout/main-layout.component'; +import { NavbarComponent } from './navbar/navbar.component'; +import { AddNewTorrentComponent } from './navbar/add-new-torrent/add-new-torrent.component'; +import { TorrentTableComponent } from './torrent-table/torrent-table.component'; +import { TorrentRowComponent } from './torrent-row/torrent-row.component'; +import { TorrentFileComponent } from './torrent-file/torrent-file.component'; +import { SettingsComponent } from './navbar/settings/settings.component'; +import { TorrentStatusPipe } from './torrent-status.pipe'; +import { FileStatusPipe } from './file-status.pipe'; +import { LoginComponent } from './login/login.component'; @NgModule({ - declarations: [AppComponent], - imports: [BrowserModule, AppRoutingModule], - providers: [], - bootstrap: [AppComponent] + declarations: [ + AppComponent, + MainLayoutComponent, + NavbarComponent, + AddNewTorrentComponent, + TorrentTableComponent, + TorrentRowComponent, + TorrentFileComponent, + SettingsComponent, + TorrentStatusPipe, + FileStatusPipe, + LoginComponent, + ], + imports: [ + BrowserModule, + AppRoutingModule, + FormsModule, + HttpClientModule, + NgxFilesizeModule, + ], + providers: [FileSizePipe], + bootstrap: [AppComponent], }) export class AppModule {} diff --git a/client/src/app/auth.guard.ts b/client/src/app/auth.guard.ts new file mode 100644 index 0000000..981d41c --- /dev/null +++ b/client/src/app/auth.guard.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { + CanActivate, + ActivatedRouteSnapshot, + RouterStateSnapshot, + Router, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { UsersService } from './users.service'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate { + constructor(private router: Router, private usersService: UsersService) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Observable | Promise | boolean { + return true; + + //this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); + //return false; + } +} diff --git a/client/src/app/file-status.pipe.ts b/client/src/app/file-status.pipe.ts new file mode 100644 index 0000000..f81a746 --- /dev/null +++ b/client/src/app/file-status.pipe.ts @@ -0,0 +1,25 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { TorrentFile } from './models/torrent.model'; +import { DownloadStatus } from './models/download.model'; + +@Pipe({ + name: 'fileStatus', +}) +export class FileStatusPipe implements PipeTransform { + transform(value: TorrentFile): string { + if ( + !value.download || + value.download.status === DownloadStatus.PendingDownload + ) { + return 'Pending'; + } + + if (value.download.status === DownloadStatus.Downloading) { + return `${value.download.progress}%`; + } + + if (value.download.status === DownloadStatus.Finished) { + return `Finished`; + } + } +} diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html new file mode 100644 index 0000000..b903e79 --- /dev/null +++ b/client/src/app/login/login.component.html @@ -0,0 +1,36 @@ +
+
+
+
+
+ +
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+ +
+
+
+
+
+
+
diff --git a/client/src/app/app.component.html b/client/src/app/login/login.component.scss similarity index 100% rename from client/src/app/app.component.html rename to client/src/app/login/login.component.scss diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts new file mode 100644 index 0000000..c74528f --- /dev/null +++ b/client/src/app/login/login.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) +export class LoginComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/client/src/app/main-layout/main-layout.component.html b/client/src/app/main-layout/main-layout.component.html new file mode 100644 index 0000000..1ad0a23 --- /dev/null +++ b/client/src/app/main-layout/main-layout.component.html @@ -0,0 +1,6 @@ + +
+
+ +
+
diff --git a/client/src/app/main-layout/main-layout.component.scss b/client/src/app/main-layout/main-layout.component.scss new file mode 100644 index 0000000..1ccab01 --- /dev/null +++ b/client/src/app/main-layout/main-layout.component.scss @@ -0,0 +1,3 @@ +.main-list { + padding-top: 6em; +} diff --git a/client/src/app/main-layout/main-layout.component.ts b/client/src/app/main-layout/main-layout.component.ts new file mode 100644 index 0000000..626457a --- /dev/null +++ b/client/src/app/main-layout/main-layout.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-main-layout', + templateUrl: './main-layout.component.html', + styleUrls: ['./main-layout.component.scss'] +}) +export class MainLayoutComponent implements OnInit { + constructor() {} + + ngOnInit(): void {} +} diff --git a/client/src/app/models/download.model.ts b/client/src/app/models/download.model.ts new file mode 100644 index 0000000..663b456 --- /dev/null +++ b/client/src/app/models/download.model.ts @@ -0,0 +1,19 @@ +export class Download { + public downloadId: string; + + public torrentId: string; + + public link: string; + + public added: Date; + + public status: DownloadStatus; + + public progress: number; +} + +export enum DownloadStatus { + PendingDownload = 0, + Downloading, + Finished, +} diff --git a/client/src/app/models/profile.model.ts b/client/src/app/models/profile.model.ts new file mode 100644 index 0000000..cc3a993 --- /dev/null +++ b/client/src/app/models/profile.model.ts @@ -0,0 +1,4 @@ +export class Profile { + userName: string; + expiration: Date; +} diff --git a/client/src/app/models/setting.model.ts b/client/src/app/models/setting.model.ts new file mode 100644 index 0000000..7e8c8d9 --- /dev/null +++ b/client/src/app/models/setting.model.ts @@ -0,0 +1,4 @@ +export class Setting { + public settingId: string; + public value: string; +} diff --git a/client/src/app/models/torrent.model.ts b/client/src/app/models/torrent.model.ts new file mode 100644 index 0000000..1700ee9 --- /dev/null +++ b/client/src/app/models/torrent.model.ts @@ -0,0 +1,41 @@ +import { Download } from './download.model'; + +export class Torrent { + torrentId: string; + hash: string; + status: TorrentStatus; + rdId: string; + rdName: string; + rdSize: number; + rdHost: string; + rdSplit: number; + rdProgress: number; + rdStatus: string; + rdAdded: Date; + rdEnded: Date; + rdSpeed: number; + rdSeeders: number; + rdFiles: string; + + files: TorrentFile[]; + downloads: Download[]; + downloadProgress: number; +} + +export class TorrentFile { + id: string; + path: string; + bytes: string; + selected: boolean; + + download: Download; +} + +export enum TorrentStatus { + RealDebrid = 0, + WaitingForDownload, + Downloading, + Finished, + + Error = 99, +} diff --git a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.html b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.html new file mode 100644 index 0000000..ee22118 --- /dev/null +++ b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.html @@ -0,0 +1,66 @@ + diff --git a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.scss b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.scss new file mode 100644 index 0000000..3cef3d3 --- /dev/null +++ b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.scss @@ -0,0 +1,4 @@ +.file-name { + max-width: 24em; + width: 24em; +} diff --git a/client/src/app/navbar/add-new-torrent/add-new-torrent.component.ts b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.ts new file mode 100644 index 0000000..72397c0 --- /dev/null +++ b/client/src/app/navbar/add-new-torrent/add-new-torrent.component.ts @@ -0,0 +1,91 @@ +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { TorrentService } from 'src/app/torrent.service'; + +@Component({ + selector: 'app-add-new-torrent', + templateUrl: './add-new-torrent.component.html', + styleUrls: ['./add-new-torrent.component.scss'] +}) +export class AddNewTorrentComponent implements OnInit { + @Input() + public get open(): boolean { + return this.isActive; + } + + public set open(val: boolean) { + this.reset(); + this.isActive = val; + } + + @Output() + public openChange = new EventEmitter(); + + public isActive = false; + + public fileName: string; + public magnetLink: string; + public saving = false; + public error: string; + + private selectedFile: File; + + constructor(private torrentService: TorrentService) {} + + ngOnInit(): void {} + + public reset(): void { + this.fileName = ''; + this.magnetLink = ''; + this.saving = false; + this.selectedFile = null; + this.error = null; + } + + public pickFile(evt: Event): void { + const files = (evt.target as HTMLInputElement).files; + + if (files.length === 0) { + return; + } + + const file = files[0]; + + this.fileName = file.name; + + this.selectedFile = file; + } + + public ok(): void { + this.saving = true; + this.error = null; + + if (this.magnetLink) { + this.torrentService.uploadMagnet(this.magnetLink).subscribe( + () => { + this.cancel(); + }, + err => { + this.error = err.error; + this.saving = false; + } + ); + } else if (this.selectedFile) { + this.torrentService.uploadFile(this.selectedFile).subscribe( + () => { + this.cancel(); + }, + err => { + this.error = err.error; + this.saving = false; + } + ); + } else { + this.cancel(); + } + } + + public cancel(): void { + this.isActive = false; + this.openChange.emit(this.open); + } +} diff --git a/client/src/app/navbar/navbar.component.html b/client/src/app/navbar/navbar.component.html new file mode 100644 index 0000000..27d3abe --- /dev/null +++ b/client/src/app/navbar/navbar.component.html @@ -0,0 +1,77 @@ + + + + diff --git a/client/src/app/app.component.scss b/client/src/app/navbar/navbar.component.scss similarity index 100% rename from client/src/app/app.component.scss rename to client/src/app/navbar/navbar.component.scss diff --git a/client/src/app/navbar/navbar.component.ts b/client/src/app/navbar/navbar.component.ts new file mode 100644 index 0000000..1f3b3f1 --- /dev/null +++ b/client/src/app/navbar/navbar.component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit } from '@angular/core'; +import { TorrentService } from '../torrent.service'; +import { SettingsService } from '../settings.service'; +import { Profile } from '../models/profile.model'; + +@Component({ + selector: 'app-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'], +}) +export class NavbarComponent implements OnInit { + public showMobileMenu = false; + + public showNewTorrent = false; + public showSettings = false; + + public profile: Profile; + + constructor(private settingsService: SettingsService) {} + + ngOnInit(): void { + this.settingsService.getProfile().subscribe((result) => { + this.profile = result; + }); + } +} diff --git a/client/src/app/navbar/settings/settings.component.html b/client/src/app/navbar/settings/settings.component.html new file mode 100644 index 0000000..43c09d7 --- /dev/null +++ b/client/src/app/navbar/settings/settings.component.html @@ -0,0 +1,79 @@ + diff --git a/client/src/assets/.gitkeep b/client/src/app/navbar/settings/settings.component.scss similarity index 100% rename from client/src/assets/.gitkeep rename to client/src/app/navbar/settings/settings.component.scss diff --git a/client/src/app/navbar/settings/settings.component.ts b/client/src/app/navbar/settings/settings.component.ts new file mode 100644 index 0000000..99293bd --- /dev/null +++ b/client/src/app/navbar/settings/settings.component.ts @@ -0,0 +1,103 @@ +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { SettingsService } from 'src/app/settings.service'; +import { Setting } from 'src/app/models/setting.model'; + +@Component({ + selector: 'app-settings', + templateUrl: './settings.component.html', + styleUrls: ['./settings.component.scss'], +}) +export class SettingsComponent implements OnInit { + @Input() + public get open(): boolean { + return this.isActive; + } + + public set open(val: boolean) { + this.reset(); + this.isActive = val; + } + + @Output() + public openChange = new EventEmitter(); + + public isActive = false; + + public saving = false; + public error: string; + + public settingRealDebridApiKey: string; + public settingDownloadFolder: string; + public settingDownloadLimit: number; + + constructor(private settingsService: SettingsService) {} + + ngOnInit(): void {} + + public reset(): void { + this.saving = false; + this.error = null; + + this.settingsService.get().subscribe( + (results) => { + this.settingRealDebridApiKey = this.getSetting( + results, + 'RealDebridApiKey' + ); + this.settingDownloadFolder = this.getSetting(results, 'DownloadFolder'); + this.settingDownloadLimit = parseInt( + this.getSetting(results, 'DownloadLimit'), + 10 + ); + }, + (err) => { + this.error = err.error; + this.saving = true; + } + ); + } + + public ok(): void { + this.saving = true; + + const settings: Setting[] = [ + { + settingId: 'RealDebridApiKey', + value: this.settingRealDebridApiKey, + }, + { + settingId: 'DownloadFolder', + value: this.settingDownloadFolder, + }, + { + settingId: 'DownloadLimit', + value: (this.settingDownloadLimit ?? 10).toString(), + }, + ]; + + this.settingsService.update(settings).subscribe( + () => { + this.isActive = false; + this.openChange.emit(this.open); + }, + (err) => { + this.error = err; + } + ); + } + + public cancel(): void { + this.isActive = false; + this.openChange.emit(this.open); + } + + private getSetting(settings: Setting[], key: string): string { + const setting = settings.filter((m) => m.settingId === key); + + if (setting.length !== 1) { + throw new Error(`Unable to find setting with key ${key}`); + } + + return setting[0].value; + } +} diff --git a/client/src/app/settings.service.ts b/client/src/app/settings.service.ts new file mode 100644 index 0000000..db2a355 --- /dev/null +++ b/client/src/app/settings.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Setting } from './models/setting.model'; +import { Profile } from './models/profile.model'; + +@Injectable({ + providedIn: 'root', +}) +export class SettingsService { + constructor(private http: HttpClient) {} + + public get(): Observable { + return this.http.get(`/Api/Settings`); + } + + public update(settings: Setting[]): Observable { + return this.http.put(`/Api/Settings`, { settings }); + } + + public getProfile(): Observable { + return this.http.get(`/Api/Settings/Profile`); + } +} diff --git a/client/src/app/torrent-file/torrent-file.component.html b/client/src/app/torrent-file/torrent-file.component.html new file mode 100644 index 0000000..83d990f --- /dev/null +++ b/client/src/app/torrent-file/torrent-file.component.html @@ -0,0 +1,10 @@ + + + {{ file.path }} + +{{ file.bytes | filesize }} + + {{ file | fileStatus }} + + + diff --git a/client/src/app/torrent-file/torrent-file.component.scss b/client/src/app/torrent-file/torrent-file.component.scss new file mode 100644 index 0000000..03bcfa7 --- /dev/null +++ b/client/src/app/torrent-file/torrent-file.component.scss @@ -0,0 +1,3 @@ +td { + font-size: smaller; +} diff --git a/client/src/app/torrent-file/torrent-file.component.ts b/client/src/app/torrent-file/torrent-file.component.ts new file mode 100644 index 0000000..e390f17 --- /dev/null +++ b/client/src/app/torrent-file/torrent-file.component.ts @@ -0,0 +1,17 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Torrent, TorrentFile } from '../models/torrent.model'; +import { TorrentService } from '../torrent.service'; + +@Component({ + selector: '[app-torrent-file]', + templateUrl: './torrent-file.component.html', + styleUrls: ['./torrent-file.component.scss'], +}) +export class TorrentFileComponent implements OnInit { + @Input() + public file: TorrentFile; + + constructor(private torrentService: TorrentService) {} + + ngOnInit(): void {} +} diff --git a/client/src/app/torrent-row/torrent-row.component.html b/client/src/app/torrent-row/torrent-row.component.html new file mode 100644 index 0000000..25a8523 --- /dev/null +++ b/client/src/app/torrent-row/torrent-row.component.html @@ -0,0 +1,27 @@ +{{ torrent.rdName }} + + {{ torrent.rdSize | filesize }} + + + {{ torrent | status }} + + + + + + + + + + + diff --git a/client/src/app/torrent-row/torrent-row.component.scss b/client/src/app/torrent-row/torrent-row.component.scss new file mode 100644 index 0000000..e8bc505 --- /dev/null +++ b/client/src/app/torrent-row/torrent-row.component.scss @@ -0,0 +1,4 @@ +.delete-icon { + color: red; + cursor: pointer; +} diff --git a/client/src/app/torrent-row/torrent-row.component.ts b/client/src/app/torrent-row/torrent-row.component.ts new file mode 100644 index 0000000..26b4777 --- /dev/null +++ b/client/src/app/torrent-row/torrent-row.component.ts @@ -0,0 +1,36 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Torrent, TorrentStatus } from 'src/app/models/torrent.model'; +import { TorrentService } from 'src/app/torrent.service'; + +@Component({ + selector: '[app-torrent-row]', + templateUrl: './torrent-row.component.html', + styleUrls: ['./torrent-row.component.scss'], +}) +export class TorrentRowComponent implements OnInit { + @Input() + public torrent: Torrent; + + public loading = false; + + constructor(private torrentService: TorrentService) {} + + ngOnInit(): void {} + + public download(event: Event): void { + event.stopPropagation(); + + this.loading = true; + this.torrentService.download(this.torrent.torrentId).subscribe(() => { + this.loading = false; + this.torrent.status = TorrentStatus.Downloading; + }); + } + + public delete(event: Event): void { + event.stopPropagation(); + + this.loading = true; + this.torrentService.delete(this.torrent.torrentId).subscribe(() => {}); + } +} diff --git a/client/src/app/torrent-status.pipe.ts b/client/src/app/torrent-status.pipe.ts new file mode 100644 index 0000000..ecee843 --- /dev/null +++ b/client/src/app/torrent-status.pipe.ts @@ -0,0 +1,28 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Torrent, TorrentStatus } from './models/torrent.model'; +import { FileSizePipe } from 'ngx-filesize'; + +@Pipe({ + name: 'status', +}) +export class TorrentStatusPipe implements PipeTransform { + constructor(private pipe: FileSizePipe) {} + + transform(torrent: Torrent): string { + switch (torrent.status) { + case TorrentStatus.RealDebrid: + const speed = this.pipe.transform(torrent.rdSpeed, 'filesize'); + return `Downloading from RD (${torrent.rdProgress}% - ${speed}/s)`; + case TorrentStatus.WaitingForDownload: + return `Waiting to download`; + case TorrentStatus.Downloading: + return `Downloading (${torrent.downloadProgress}%)`; + case TorrentStatus.Finished: + return `Finished`; + case TorrentStatus.Error: + return `Error`; + default: + return 'Unknown status'; + } + } +} diff --git a/client/src/app/torrent-table/torrent-table.component.html b/client/src/app/torrent-table/torrent-table.component.html new file mode 100644 index 0000000..211eb04 --- /dev/null +++ b/client/src/app/torrent-table/torrent-table.component.html @@ -0,0 +1,48 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameSizeStatus
+ +
+
diff --git a/client/src/app/torrent-table/torrent-table.component.scss b/client/src/app/torrent-table/torrent-table.component.scss new file mode 100644 index 0000000..41c6f23 --- /dev/null +++ b/client/src/app/torrent-table/torrent-table.component.scss @@ -0,0 +1,5 @@ +table { + tr { + cursor: pointer; + } +} diff --git a/client/src/app/torrent-table/torrent-table.component.ts b/client/src/app/torrent-table/torrent-table.component.ts new file mode 100644 index 0000000..b5e0799 --- /dev/null +++ b/client/src/app/torrent-table/torrent-table.component.ts @@ -0,0 +1,61 @@ +import { + Component, + OnInit, + OnDestroy, + HostListener, + ElementRef, +} from '@angular/core'; +import { Torrent } from '../models/torrent.model'; +import { TorrentService } from '../torrent.service'; + +@Component({ + selector: 'app-torrent-table', + templateUrl: './torrent-table.component.html', + styleUrls: ['./torrent-table.component.scss'], +}) +export class TorrentTableComponent implements OnInit, OnDestroy { + public torrents: Torrent[] = []; + + public showFiles: { [key: string]: boolean } = {}; + + private timer: any; + + constructor(private torrentService: TorrentService) {} + + ngOnInit(): void { + this.timer = setInterval(() => { + this.torrentService.getList().subscribe((result) => { + this.torrents = result; + }); + }, 1000); + } + + ngOnDestroy(): void { + clearInterval(this.timer); + } + + public selectTorrent(torrent: Torrent): void { + this.showFiles[torrent.torrentId] = !this.showFiles[torrent.torrentId]; + + if (this.showFiles[torrent.torrentId]) { + this.torrentService.getDetails(torrent.torrentId).subscribe((result) => { + torrent.files = result.files; + torrent.downloads = result.downloads; + + torrent.files.forEach((file) => { + const downloads = torrent.downloads.filter( + (m) => m.link === file.path + ); + + if (downloads.length > 0) { + file.download = downloads[0]; + } + }); + }); + } + } + + public trackByMethod(index: number, el: Torrent): string { + return el.torrentId; + } +} diff --git a/client/src/app/torrent.service.ts b/client/src/app/torrent.service.ts new file mode 100644 index 0000000..5edfa32 --- /dev/null +++ b/client/src/app/torrent.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, Subject } from 'rxjs'; +import { Torrent } from './models/torrent.model'; + +@Injectable({ + providedIn: 'root', +}) +export class TorrentService { + constructor(private http: HttpClient) {} + + public getList(): Observable { + return this.http.get(`/Api/Torrents`); + } + + public getDetails(torrentId: string): Observable { + return this.http.get(`/Api/Torrents/${torrentId}`); + } + + public uploadMagnet(magnetLink: string): Observable { + return this.http.post(`/Api/Torrents/UploadMagnet`, { magnetLink }); + } + + public uploadFile(file: File): Observable { + const formData: FormData = new FormData(); + formData.append('file', file); + return this.http.post(`/Api/Torrents/UploadFile`, formData); + } + + public download(torrentId: string): Observable { + return this.http.get(`/Api/Torrents/Download/${torrentId}`); + } + + public delete(torrentId: string): Observable { + return this.http.delete(`/Api/Torrents/${torrentId}`); + } +} diff --git a/client/src/app/users.service.ts b/client/src/app/users.service.ts new file mode 100644 index 0000000..25f96f4 --- /dev/null +++ b/client/src/app/users.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class UsersService { + constructor() {} + + public getLoggedInUser() {} +} diff --git a/client/src/assets/logo.png b/client/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a74a86a348fb5b9e6bb0dcd1feb6a7e20f20d5b1 GIT binary patch literal 15087 zcmY*=by!n>*!Q73q+2?KAt50tAV@3S-8n#FfFPX$0@5LZ)F`F9L%MS?N)a)f$AUpqoQm{k-Mm_59p_D-&L003Lb({a$%*{71e zLY~X3#e{rNgKAUaGHc7nhfpMQK4QTmRgGc!v`neh^FUD%lm1&l3`SyN$U90cKEgQs zC7kapg^A%sF>xa|n}J0x3+-3C<97{ApJj>`5`ot3 zoh+_~8$tm*3rLhN0iG$Lh2&*&>Hr0pfaR!-^)~Pd0$6gr-JJzO^8RE6q5&o!SSZm7 zk^yFNyO`&Iqa;u{sUN2Z7zzLs_GjxIzJJH+j-kmDHB(VU>b=PKU5mgHFjP_VyZaOQzJ8^urWTb7r1zJFg*?Uy&P z8ZijEbLh3`Hn?@Y4wm|Nb+OU8#U3hT7WxA7&qnXqxq30fWC~t{_3CcYi+@c7_x~7Y zS^HJ2TMQpjY-^EuCO*%8cP3QK8Y`c|Gr%!(Z@ab5cY`6bE&_aUmO%?lhLoAvU|gzk zks5k;Z(RYv?>1=995*gTh-3KXq~HCC?CpyJ4j{x{CB+i}%%8LK>yFgP4&eg8^McSv zU*zb|duaH&u^D=>R(lBkS&Bx;v-kJPlgi`UhA?GV&b4QJTVSHrycUJ>2b~c$jkEaLKJ$R$|bs;|$pnajCN+|FHBZW0J+QGY`j5 zXtAm#izo|f$LTWbDKq|-_CkLeZK~9oEcp!(3i}t;{zC9WXswp?9ASe(OPLs5gi;~F zl&!a5b`n)V&XiX@o%B1=PkU1pjwCBdG73Wi-9Kn>g!^3u_y_yszl!tWrP_>sWvwKv z7^M56Uy0Ecdto#A)en;_RIwXR6w^t|DENx34WgSGtbX|bhfy)N1Rn@cSv`h z>|onv3d!jeWoxgMebRj(l4LAj^{G+2Mk~6E{E1KY)A4XB6}953!n9dBM{36_r@zU_}PfrzuRfP@ew9h`+%i?Tjdj84v*2LT6F3mU#jzdzM z@NDtWmpoB#5wtUPfv0Dr8&)V+XjQy8>=3O!D$OhDP;4{02_=>rQspw`df|{Xft9zX zv#hgLieE~lAuVxH%@21ervKFT+^@^M^f)uW(>x?!9a@U?@7Vjx5cK#~_tx!i>BS2| zL+m<2c5DRU1dY^yh$mEv@$nGimzIKA-!p|`t2C>O&4}cdX{n-lr359CcTR(*+w9wJ zgKmRGoYOof{Q|XIeR&aIqOZ9#xP^P z#XfSTsyG(fs@h5)l(uN5YF6YgIP`t_)i~T( z-bDLlX9*io-RRaBWaVH@?a(w(-FKYYks8z*lyEDJLlnJCT)}9}82q~4i$X$Q0+~IL zdn_9?aGt*Z*uij;baF{}yODg9r{5`0CujDB1GPiT>+rLf_*k{DQT_}W{v7(jwQn8Y zRlP4f%{BvOF7k5fa(a)-5H-i5$4A&o4?#JZclHI2UPgXUXA zt}DD{ySKd$xSs|}LpabJFk(aHLQT+b{sg>X=}MH9K-gW@E+5IgD`=6UluHV$3ft+a zVsZTT^L-V|)n4XA@`Mv{-!&Cl8)uQgMo$LRO z&Z4*Swg|QZsWTscGxPBOG|_t=SxQyTPsS&a{!BeaQ~blJ9O`*w9A^|c&{e5!0;W;Y>e zi4MW=l;roG@^BW7)JBH`hh2N$(Mr3>+PeYw_@>89X*k0!b?+;&j3D@p@+CiXjky;e z;nX_V8Q6D?$BsNrugK>eKN_nWLA)2sJ^6TlMAl2TnbKGsrDgzOG~Df|bbeYZ<>`;W z-=@Ukjnc=ilxmJ^eB?I&vf{`fqdK^QGeE;7)VyaTtfv9Z<&Z!Re?4{vj zqxA->zfpPgGL*4#)f5Qo49d4l+^eu1`e^7#SJvu>`tpkY7TZbt8T@1)CJ6kD7C;P_4+NAa6n9TIT0W#p<3&cZ`+$T~-{;FrX!%i#v zVY8_YzbD`2rR4<$^Ixx8A_^M=_iQHCok^f4t)$+YGm7`|dy?&v6Q;kLy?@stcE}By z4YF$_EZh#f52Wr=XI~Dr<+f#4x0t%L90znX1lnKi)8~s#tetoG|Dw8In9qMIz1DFS z0KFQxx>%=L3-;o=ofvIf_TxIPYJb^oasKQ`ZdqY1dS2G#ZguHo^{sQaU%}b*>WeGv z$neR#xjQ{NTdjaE}rJr4def4Dd? zojLs}<99}Cm)o1-+k@^y+8V@Q?;um>Ficp`{9OO$sK;;BkMX2vo#f^BH_UIj?;Wqd zUg%7f-lWo|rru*>WS;%A5r!Z@yvb~%tgQh60UQ7j5)J^@_u$Wc0Pqn2fCEbakjMl8 zYG{hZfD!-*(5XC^(}k`4%yV<3H|RXQV0k$6*pAWg$BcY4((V&Qwwi|6m(VG!^(c83 z4}|OTOiCm+lzOJE_|+TelDqkj?G=rcdVdCJ+Mwsz@2LjF7y7M!sHgi;cWVRVc$QeX z{*&}HRekn~?dRFT0To{PBze=1MxGXBO)J-?okhz=4rV`ny*rk>{?Ed!8hdm@eUfdj zq%EJW*~_D?8ujkrq3n75V|$YOe#J<9%164{`lRx@<=PEpK54IK&0bz4G5)#TuZWh} ze~6Ivcm5ahc4MUHe7OdxxFg}QV;Nw%wNZ8V^|tPI8%O5k;rjhvC@)TvPC=18_%M7Z z_#jxtr`&AuKdY!`JQWGi{bFiE{pEv$zeATVFHvhit^9oc1!|2H$TIEXQ~hGXiF2tL+0Oe(i7)$KM7ItV z$!9E1W`EHKUGW^BC{ZUCS^YA6u!~*IpnjF~@Xx=w{+_)byzfz)GtY{sc^4x3K;AX>d{qZZnl)tL)V$`N&>+ zJvaqYU3p`73!8CeVvdIchid55jO=91mR##`y5g#>RLli1X z)}OHcLt$?(u^==yys3;bQMG|JZfmmA5w#2nmxio+p2>413AQx55#bI`$b`v$CTgR` z4zQfx>d>Fy1wIv@)$ukHC0kJ@72{)?t@OCLSGU;HtyE^T6l1K zXT7m2Qa!}m@c;oci6*8buAZ}hni7(>o8s;thqhHdOwtc|-q49~>XV9L0ODh>YZKi2 zb;}pfkk}3ZO#fXp8Uw_*Wr3_Y&GGzTCN>$o7e|j0oQtHRkHs}UkMuDC0S5fYSA0|o z(~ff=2CW4tVxPZXJnVSx@I~SQrqxRU8or4xnI@T!(BuhXb6EcR=0$)OJ6Vgvn)6u{ z_{SMin2nJiH--E!*uT!bi2wd|8sy(0l3QyqC?RVa&g83w;5BlAcluVmb^|H4dlGn* za!W-7etZ9!8~>3-Q=~i^Ph6OIB4RzpGw5o~@8j0s`R&!ZMYTOfIH^q8 z3L1;6Q`l>%DPw9FeeS-$EtqQc`5!DFEO9DPt=>#%_(|)zNr8DW$%fhjYF}k?%nvre zF#EAOcW-sRZvLAo@ax;3-6u|~gp|VQEN}4SV=A5bGZfa}-rw3ugwGEjA(|{EZH1Jz zK5VVp3()jG&Nj|JsvZd3X#B1!{c+@Ui z3jf8dt(63j_5Yo}p@4 z0wC^I)Co}`!^aNSt5`pIVO~Rx29Wj%pe1f!X4t!HmACmE=l!NO=YVaCSC5`tN!BuX z9QJPi;`3l=%IHO4v~l_=;C4lkR;T}1gu{=W<}w!q+Z!B@71mhiaWkDFNhcO*Dmih# ztjn>u`(LAfjCcIQ+r*Q(U7fO}ZWOHI5F}n)Hk*{8MMh7lj{*KC;X}hh7^~?J^(um8&X( z{Jmr_p$#X~#qpu@!v_Zk3az36e1{as5r|o<@8a@`Qf*za@_;&2oF+u@v0WtW5n3+Ybn*zS#BCa)+j4?ZLx!u=%(M|Pc;-sdkLt{StH)} zR8_f(#)GGsVn^%WVcst=rUmvU?smO7-_(a{4ag-w+T6Ax;`icmoABV%l9oLmwUak` zrwg);Z@u~#O&K#+)!Jpk6xrZSU;U#jbJbsnTZE#OZ=l&&-;+ zjR%`iUz9V&qQL=EI-~cnb`<~~Ut{eJWI3uyhaBgl@Poa}&48d}RJ-%K`-?MsYl9QK z{$IdzQ^j}KUNw{Uig^spp%DY!(-526ZWGZQMP6RskNUz?xSKYpqzdmiHopq!;3mcW zm)lDZHfBn4^j#tVu7A2v$f5Yn$17utOjv@ad0r18c-7V?+Mf4OmQ~LqL-R==K{3-X zK|`2W#^F#c0Eh2-0@PsDWOxrH^cq=tfl6j=%0C zH20lz=Iq7e5Zmew~)M&6$j%@uv=hWi;tu zux#Nk=phEut0mV4{3v}r{i8zDNOcCGEr~|2Jg=JgF6>1T_*&#`cM>=a3oYImbxxAq$wGcR`$r{5dI@H3 z(suJL<{B?t0Hg>+IdH+qVm;6v`ZO*=}dIf8bZY z*oq4Lr2BFjjov1_9*C4@K`e-YkFx?R;x8B(0DQj)NS@qdb5==S!W^x2z?6fvt{iTO zR}%mp^NF>5fl=Gd{BC_n?V_7D&P@%;N!rzH+~_{7w(ddC)nEX?0iH=s(pTfvEXKp~ zUjQKw0l+{}>&*ZjjMps*71iMWNB{v6;7?C)CNp3;caT{x($Z9EGm=s466E7^!EyvG zfh6Jy5(;z0ZRIHKyaQWuZ|zUoj++HT2n;QU(+xN}_va)p_&ByX) z%rV0ddg$)y1FR3WRahj(*j-<~nY>tkl(N zK$MuNNiLYfDZ`(4_!Q(w^B2q)zMWhZfeA+zi>b<)s;os(c%W#_mW=!R<`x$SF-(`X zY4Q;66M){lk)a^OPG(n0*FBMVg`GwKUX&Mq%`eFffX(nq_a2H>ZWpv&5}E}hywsCjH&D@1A zc6)PF>c#lPw`S{V1{9XvY>6ox(juR5<8A`{q_}S!tx}QT1*fpH7^TPm0)Ysz zolsWcAyH|uqP1`qp(;BWcy?@2R_MO93>cx^JKhp*ZNibqB|Jw`1@lsB5_+OG2_{I< z?JX9Th!QbRInqXdseCLjR0^jZrvpm;q6Lgez=mX3x4iY22g2ZLf9H8x67>GfsK6M* zUQNk(q)ZxHZ8^?vQIdFG;;9nq%EdWnfwaqazuN2IA3PrhO zWz1hvNrve@877;o$uT>}3=-m5ErTdL3v_2lO%}c4_MU9W^e6dX5lk-Ictvk4@tzWQTJ1(b@q!6v2>pva`y1kCjH_L#-J5AlV(1ZH;JMtZ$ zz)eg$UF~56tEQ_Vygzft?ZJTR64?}8#{-^ydR1mfxKg0=&3QPWdqHGu$Eys)gwI(+ zVfi(gLU)*r+BCewf^gA9x4iAcN-hdn00&)<}~Sb`XkbeR}l_ z`>@elz2dVbZNM0O{iKVeo+ZKpvA>iux-$hfamh66b1A0DuwaM;`&MFU5RGm-6+^uU zfczO^LcoHoS~ymVr5bm0=0FQ5wfk?iuZHbb#%N1QpSgMh{aTX)%N8}SM-h?{WE7#Q zU0A@ozqfZ=oC$$i74QIUtq%??5lL0m)iM;#EzNQm5#uOiCNg-Qz|EAz1_1R3wYqv2 z*Se2<<|>^5NnDoP{_!>$CR~;jc2pr)tUPUOZT(B?t}|{cglbKOJQJ~d{_LY(58exy zv2e0Pv{5Qv?`&D0S_Mfv4t#EMzG6!<0nuMvWVq+4zVQb-F;2+nB}7pxpD?;F=UfoN zkNvRo_1=Vz@m`AFYNk-uJ9i!#XG~~wci?TOL!HrTX4pzyOT!y=(q1m@a$T+o=jtRC ziUBdflMjBndPlsAwnEFLaOa13CGR#8z+z6=$L=%Agw|TP612tyq8Q^~2-rHoEoEbk zNc18ih%!psG!?x@DHpTjjn}fdM!h?;$ylzsP3$t9*56gc?oh-)@^3^Vos*kiIgBD6 z;<=Yta>VC=rGQ%0K9wngRK}i+*`n9zDSl)av4>BEaLGkl5N%V|k7~y8N3aXIFfq5I z4o6GRz^J|JLdx=gSHreKWgzaQGHr^A3|FBtB5Qx}q!@m{By2OY@mu{m)BCF3{%8g4 zIk+A(P7A8=hTb*@aI<=fTER}+3W;CQp`khP^fXa;&x^ElxhY;r2a1QuUDIR z<|KpIzT|HbE&9lvWI27OJG;46yi&d61P93^bqKllYmWiNXM`hU1pTL-T@Gq*RS+d4OjZ?rPN6$ZH` zwidOxTr8wpalKvAr!;YD2o|m3bt7~k3dB7nNRLiL7`2uBtiSx$2x-KZfgP{g3dV(S znv;dso|lUl%!v~*6^0-l;ZK?$~nh_oeQw9-aFG?*mfkg@P*j9%QG zuD-@_MbF!{^Hwrdl1>L1$Nwrwflngfv$Uk2pY7F(U=6fIY^P9 zq~%`|MyicO?>)yT`26Z8a>!!IV8`=yJpxY&d+5aw_+ZR zhC#@KL?TP^&=(gMfvaDQXBdR-1knv6ZEOYwrd64(Lyo~Vk-%E{$aFL>Sy zfjB-ExknO0_yKvU=4I6Fv4ww1_(qrNgTV?n(5#xDY)5-DBTE0yeP~8cyEDRVPWmwk zVNJ=|v$}4?2ZsO~aR$u?8*gXI1)XR=E@{*6RL|m{Qh7P68>fE{;s+mcTt{jU()1w@ z)~(bFQoui7KL7b8?djfdX9;I%Z%FG7XB?ft9GqV)qq8Izscj^BuvL9vQ1{H#D1$MUe5Yxf8xjwui0g9_U8x2BH099krZpUv0o0>I1oIH6d533ku?cBb1m z9o_%Y{%6bu@3n8fcHv>lu%FX?VD#1Le22kdl->o^CDG00cy=OX+0bQM*PX+R`*O5a zy|%SuV=hyj`1jb{X8eu*LvReVg*aBbVI|h#W#c9 z(vp6@t@F?B?V8f5Z}N>}_k92F;jtMGMx^GMkc1$;JeRaqef~y*-Li1(^RVwiK^Rx` z+HZ2I}jawUNpmcI+<3Aqqb``xJYuZBevb5B z(mg~tQwbG6Di<^Ycg?G^A$eQXw%EOqBg$f{$Z-X2v9^g92j+Y`NX%V$A#Ti2Y1PhW4kl zuI{tHFWL7)&5VuzUg11Irl+T0ZdSU(;Q@L1ZyDO{glN5QE{_&9dJ*v1V--_QSBbpN z$DXBOi1XMBOsLc(RE0BjiI`H8{n3YnQu9+$tv$YR;{_nq`FosC@0uZgt^y|ue2tU6 zfo#I+%FsRquhO2o>BEO}fy0Bm8v7&Jsng3}dMDf9X@4GS-wyyYAxMI{<)HtmGai(% zkKy9_%|LLNzIiv;0F{?28E?0!Fs(?e>eNI=+~|m-dD^Wu5~i@7R)z&N{7b};ok1on zxh_q#unMeKTN^|fK5hV%=!WJi0W^$RN+W(q9jmt@tfqJLiSkl}xrbLGs^ZueUz$osfY5^d za5x+vY8=m*8yGk8WW1rsTS$li?w-Lv>vkgZW3ikX%{9vmC6t3MoiK1 zxmGL^=7cParvCI=N->D<5A`P)a&Fj!cX0L9;!~dm=|4U~){|C$qWYRhKL4b!iv-E< zpVZ-q3EiuCD{$nj&7+}TCe{w>WfbuP=Ek7>Vyg1Lyt4_Ux72_KB@hzUWBK(5gwy2w zb8H3=6*6}$Wd5(IwJ^=mLb45(XB{(cpm^e;wb3OZU`YcAILk%f2H(r_wChDRfqcu> z=x$_K@cQ-+P2P7qC@WhWC9}B@BcEKse|o!GvTbNU<0)}5gF7cSsyaf8rN>&y{fx%) zT_Y!KQztT-&y{=bs(d^yTJfm57VsC~qU8LBO~Wl|s@A`hceE=9+m@>sm&_^K%`PvF z1;U!T0%iVmg16xJS81d2ikJ%N%y?GfM|TP`s&qRjJr6kg1Ax>?IfeZ*4(&GG>l_X7 zGMsin!q;=bj+@lZM8?t`NPit-Y<~5O$ zENz{({_ti%{wKp~zrX5~fP$ib$F_1R$lfC#;}j2 zi6spstv&Uk(SLo#LC)lH50;iyTq>>V0{gnnJuvfZ!O@+&@REM=ir}}{~ z8ye2{>YN%w4%)DyQ+qVzckFpP-nCqg@Qo|qc>OGAzG&@Xe@S(D-5d;n!vvsSMmsEX z6}a>A`_Yc}UWK!!PbFhtDq>2HvK^H{iTn&nWH~c8poW(gj>sWG21M1I3=FjG-=9#B zAC_~p?+1yTT}y$=VHEs6;eoepol!l=kXS)wM5zxf;h^U=RbYJYUI%;=7}HQmdGFpc z3rXb62{+NlhfegNIs!J({Y4Ik6#GgNrPHYQnz)3Ul|U$kXe2*1JiBy;M7xKdjSznr z5C{`$@~tGspgwBHNUs8eq75%Ks|X^(t?$Yy`lZ7e2GZ4PP(>zZU)=wbAmi!rpIG^~sC6v?WyPNya&2K_AK})0RH8xRBBG$~rh*nCuVMaRC`+!%MyS z3_ZOy9Tkw`utc0Z!=2oK_hfkUgBptgrZ_|Uz6!;Emrll00%C1Nt$(!OV!#ZY1Qp~{ zsigBp6LG)ttat4L0~>t+kO`{1pI&hrKN{VeaQ~zaic%EFu|5P!!?j-szw-mj^8>Ia zL6jau%6}@{p?Uq#f-~VGD7}g0oGz5Xn!H9A-s5pQS1A}^2``uGZ@y|obFU4zggoH0 zJF>H>@b0{nu+2V()3)o4h`Tj@js%q^>OFqQJX_Jmw1gqjRpe%2<@)EK+t~FR^Aqw| zP}s4?LaL^6(?qH0YbOn*f-VsNJTtFp%EWyJG}RY!zszot0lK}Vm>!3j_-}4H6U59& zKzBDFA18}Iv^-XgW2Tns;{AT{Q@5&|=So5cv8RD=Dz*e)V*s7k z65WdGm-gfJmOW=RH|%_V~EFVPlXeaeg0v)zeUxsTnP(E z{JHbcU8_`jHORX`WdS5ahM{5=DimOxC>Cjfs`>ju?ta20TOryfCg-Vp*Lj~_;S#u) z>#F(|k{*Bnq)P`TY_JE(QqMLgC>|E64tCxLg+l*#XhsFU3Dh(>dteFGDb_I*z%I}BtW(V7<0=PJV8CMZk@ zLiKkLDDTHg92jqC=*?dtDts(3Odkw+29EcdIFUtCj_-JV#tf^6!-MRF)yzOlymtmU z$okZ9;GnWO4#p6vyMr{T1UEATIrU*63w7P(rwjX**Rjq$8vOfqPFyB+TBp0!LctZ; z@8+G?-V1To>54w3O1W9wzYu;Y(aCm-GG-ENgK`L4r`M$)zB%!h?NN%+D(vn0o1^4c z%>|nk-S@u!Hy=Nn8`M*O@K!Fpmn7oR=9vA^G~) zmje0Dv~>Kvj|8X}|C#;_Yf70PFK;xwh!P{h*Z1Ujjvm?_O~nT_q4!Br4tkY$TytIU zA7p2X@Swz@2S1^vZ(usWgEmiLK+NlsdUJ$Nl|r_iH{Hq-69z^lxyKCfV%J(N}NhDUWwYGN2pwxhApV(duOX}bW>W2zOJ57J+t*nXmae@XYWO1 z-v1G0TinO;@LuyZ5eMl7&a?Ky`EivDCyWg@(hR}o`ucORx>Nnm@4Luf^oNl_K1s`qM>qqTsX>+rd@=e} z9~C%v*WJ!ctFYajXHQGk2!cAJV@@xNE~=n|+@7D_{_EbW_TfZTQDmS-Yv3{BgJAPl z-rCu{YAz5bl^2P_`a&^z5a$b3CT|_*f>P|RE4*N{rokuT{!Cz#m%&Zg_s?Jd#Es8N zVROe3+4PVD41w05o5ry4ctjJlOA`WZxDj|=ru(fu6l>tfQay)!K~$KkQd_{41xU4> zo@p4honB4EPk&bQ*PT(cDoKHAYj1^dB-5>ew6u*tM|Nt&M>8{2ot&yk+wepnLabwv zXZr8%(~$qXW$7NoO~x)cNb&UGN4vTPk=VvaBb{;yXf?e*&;qO+5nz+Vqp{JAXhLuPGHFKNo>^SR?{#GUpR z4)5l_2;R2GhJ9sRDJg4XmaHf8M|NwoE|x9YHO0DPiobV${pSrCCutW&p)usyOG&i3 z6}3A_Ewsc(XusPPsJeXPrxvh}s2?n%^3N$3RX1C%i^e!j`J0D< z<1K0~Ha-<%?Q0-MBWc!%0iY)v&v@t(B(j&C9w3o@2|NtMFY;EELEGRf%(bo?HO4?x zeTFrbna>;|yCv@aHhclYS3Ry!9G`mALtHhp)M@P2oenlD(eT{U#>tlt?X}L$Bo1_=`{_-Feyg(BSb@*&+^|npW6$(+ zkeV(Xzh|uG9flZddy6Um;~Z{vWy0yoQRGzMlUxEhhd4-Q z+rrcu5iH{AYJ9N}JwuWTo-z|Wq$E7#O7$#=r2Ro<3R%RLdEN5&UMw7se=Ia5@dw2Q zL`m0FKaEF#y7F5A>?tSn>RhuCP6ikGE|6u{8||I>gXRlkg1;ayO28?HXFvYPh0F3A z6p-ebK3ZiKeKma7ocsjiKa?s&NIn%9XHD00KW%9W-CR&$O?IT2+E!Kn4n8IJIf(ox zbzi%JZp8I%FyD7LJbOsaoxB&}zH0n#Xgg-R8NOvfO&v~B#}}BR+x8Xv)E-BOoxEF{ z7nbVc=u}5$!{xH)QlV98_>`i>4ZZw%43RhdOs>JG&VG+R&Cq9$Xu&RLUjn-Gd*J8h z$9I1MVtv-ZS(d5I(Z$6N1Q)lPEqljkVtwLb4klJrVlW+w9iv=54EZv##u~})WYbd6 zDf-HHSl!z@V`rJ*jE@rj2>Jv<9iOP8&(&xcw(6@qn-mzE#;%MWZ+5_uk9n@83p$Be zW*!&-3bTN!0>9LzE9hkN)ni|Uc6!%vi=Wc}Kb%W_=YOcKwgS+jK1b6Qp zC9&4_Q`-MtU=7evehX=R8Cav@2fC!lD3eu?63q8hd4%^H;<}*UxRI*J4>Av z2(b$x$J}%Rk*0uG#sVI4p*7uPeO2V$S9*BuTuhpwwDhlnK3l#VJsRiN9(P=-d`X%U zL2X3ed=ST9o~0yXfsz$ym4@lNlVlw8FwWe;s zzP{K#011`pJP7&+y1#C(&$lWewR~)#Cj>%B5TCyUM@}x53P$P(DHVB)F@t0tq66v# zVW;(38Y>`l|8slC%1j$-QnU$SN_tqg#YBl|g{mi@LxVnfRyH>h+M+p|Cp=R1@$Wwe zAzF47GIZcycWq@3h(IsCJ?B8{IF(w#nJ1I#RDzUocU> z)(BDIoMgvt5iEK0_I~kR_Q$`~2Vk9tps%LUul)$y10;s7r|{k@oLFSPoD(KK-mX9n zwSlKIk7~$DrXob{nts}blcyyKf^i52mI!VNe3qKUimv`t!ahs@IHU4jI^cYX9^);M z5Z1$t4lsk)tVOZ@-9MNMbT&c4z!JB`%QxCe8-)kb8~IP9Bep`65ch;AF=^#D?(Xgj zj{LMySr~u^I9G;-q7u{~li9M5k6+UU(~7IoEK41G$LlF5YIyVJc5=F-gY!AA_6vRW z^R8OCimyI4fwTg&twDnUoDhQG_ChZ%y20@@a99TeC_w|jz$>y9QbCW`1Gy6=m{Pqa z;ta?Ss>QZ5FLEdM6hI%yC|#NjTx0PWF9;)n5Gw#UzkZ`+slHkD^T!T=49N_9*AUZN1{wUHyE~TVyXvE=<*-dYV4E z^QZ@bW%@2TSi^o-1awwmQsxn&9T7@T>v`jT+fx3rKe22zquA3;w?naFZVI6e&hxH| zfR1#RhRuV{{cSA@EKno)es+CqLIPd{C`fGm5+AXz`;3VN<; zm0LAMStrU}S`<4OpEHNR2w?rsbML=SP%7fi)6BsZ>+s``Y-(+FGGUO=?xpTB<=1A@@@~-9khp9w=ih1Zag9`ym=*Zp~ zH%J*w`3+Wa6P|$YbVBu%cWSJ{$X+2@ZE3N-)6DtTwY$#0DB$9FZ#q)0JL1h?$43>2hXWn9B}-QQQpo|Un5%kz zv(z49l2bfoF4OhbxI2j3iMEd#cXE|IQVgy^I4Zj|(`c9rdYb-Hs{cKA?O2W6TZ_4hNyIlI?KYY0^9;s+D;9bcOCN}(A$-o6N>Tqn0)aJ*Y6>uX?iHi1W z;m^*e%dVG99L)~4xSN4Wp247YT!l_jEgb4wDNFm$zA1d*dHk^!?j|_zc2`bC#Z0uN zxivDn#8Bf0PM?0ouh?M0wTi<`D^`7^^Qon#$yn-S_qUg5wz)?YOV7pAg=ariQWslt zFhh3E)E;#cu;eZv;^lCg$nJI_;A~cop-DrgXk~Hj@#(Htizx)D)`aO7PNVs2T6Ta`TAwepGMf4_3Ar5WpEEFv$ENq(@U#!`BRDaR1v2{dU_VV%!Y3& zuz8>#Ov8ERgU{N-J^m6$co^3amk>R(*Tbk6Chc9@(N6})WXpf%|NKf{_c_hfU!lv~ z6bFOh9ON<+&U$T&kLR4FlaIsU@X3tvkCbcM&)*n=60V49ZE4lQYhdNzrA6+-G!#P>y~`r`B)x0OXFB zUPGu>@!taH`C)X7pK&juKZAWv^9KJ%T~<8msZ3+~*7QP!<3q2cj=8;&xNC_&HLe%2M6gy5*#+jsD+9pH7w1 z4q79cwAEI5I2T^y_fzrIYk~9H|2vCb?FpMj?WhlkIcBglkfF6Suq_Qa0!{rp8keTu zynp}xzhmo^-@fv8{e9PVaXEZAJ^8kNBnz#T(x|RF=r^t3pi>qsWXW$dis+C!|F;y5+j|V)ox(aUY11+>_&*8& N6-CYG)$*2M{{wS#Srq^P literal 0 HcmV?d00001 diff --git a/client/src/styles.scss b/client/src/styles.scss index 90d4ee0..8649996 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -1 +1,2 @@ -/* You can add global styles to this file, and also import other style files */ +@import "../node_modules/bulma/bulma.sass"; +@import "../node_modules/@fortawesome/fontawesome-free/css/all.css"; diff --git a/server/RdtClient.Data/Data/DataContext.cs b/server/RdtClient.Data/Data/DataContext.cs new file mode 100644 index 0000000..60b19b1 --- /dev/null +++ b/server/RdtClient.Data/Data/DataContext.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using RdtClient.Data.Models.Data; + +namespace RdtClient.Data.Data +{ + public class DataContext : IdentityDbContext + { + public DataContext(DbContextOptions options) : base(options) + { + } + + public DataContext() + { + } + + public static String ConnectionString => $"Data Source={AppContext.BaseDirectory}database.db"; + + public DbSet Downloads { get; set; } + public DbSet Settings { get; set; } + public DbSet Torrents { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlite(ConnectionString); + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity() + .HasData(new Setting + { + SettingId = "RealDebridApiKey", + Type = "String", + Value = "" + }); + + builder.Entity() + .HasData(new Setting + { + SettingId = "DownloadFolder", + Type = "String", + Value = @"C:\Downloads" + }); + + builder.Entity() + .HasData(new Setting + { + SettingId = "DownloadLimit", + Type = "Int32", + Value = "10" + }); + } + + public void Migrate() + { + Database.Migrate(); + } + } +} \ No newline at end of file diff --git a/server/RdtClient.Data/Data/DownloadData.cs b/server/RdtClient.Data/Data/DownloadData.cs new file mode 100644 index 0000000..bb59e75 --- /dev/null +++ b/server/RdtClient.Data/Data/DownloadData.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using RdtClient.Data.Enums; +using RdtClient.Data.Models.Data; + +namespace RdtClient.Data.Data +{ + public interface IDownloadData + { + Task> Get(); + Task Add(Guid torrentId, String link); + Task UpdateStatus(Guid downloadId, DownloadStatus status); + Task DeleteForTorrent(Guid torrentId); + } + + public class DownloadData : IDownloadData + { + private readonly DataContext _dataContext; + + public DownloadData(DataContext dataContext) + { + _dataContext = dataContext; + } + + public async Task> Get() + { + return await _dataContext.Downloads.ToListAsync(); + } + + public async Task Add(Guid torrentId, String link) + { + var download = new Download + { + DownloadId = Guid.NewGuid(), + TorrentId = torrentId, + Link = link, + Added = DateTimeOffset.UtcNow, + Status = DownloadStatus.PendingDownload + }; + + _dataContext.Downloads.Add(download); + + await _dataContext.SaveChangesAsync(); + + return download; + } + + public async Task UpdateStatus(Guid downloadId, DownloadStatus status) + { + var download = await _dataContext.Downloads.FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + download.Status = status; + + await _dataContext.SaveChangesAsync(); + } + + public async Task DeleteForTorrent(Guid torrentId) + { + var downloads = await _dataContext.Downloads + .Where(m => m.TorrentId == torrentId) + .ToListAsync(); + + _dataContext.Downloads.RemoveRange(downloads); + + await _dataContext.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/server/RdtClient.Data/Data/SettingData.cs b/server/RdtClient.Data/Data/SettingData.cs new file mode 100644 index 0000000..a6fc13e --- /dev/null +++ b/server/RdtClient.Data/Data/SettingData.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using RdtClient.Data.Models.Data; + +namespace RdtClient.Data.Data +{ + public interface ISettingData + { + Task> GetAll(); + Task Update(IList settings); + Task Get(String key); + } + + public class SettingData : ISettingData + { + private readonly DataContext _dataContext; + + public SettingData(DataContext dataContext) + { + _dataContext = dataContext; + } + + public async Task> GetAll() + { + return await _dataContext.Settings.ToListAsync(); + } + + public async Task Update(IList settings) + { + var dbSettings = await _dataContext.Settings.ToListAsync(); + + foreach (var dbSetting in dbSettings) + { + var setting = settings.FirstOrDefault(m => m.SettingId == dbSetting.SettingId); + + if (setting != null) + { + dbSetting.Value = setting.Value; + } + } + + await _dataContext.SaveChangesAsync(); + } + + public async Task Get(String key) + { + return await _dataContext.Settings.FirstOrDefaultAsync(m => m.SettingId == key); + } + } +} diff --git a/server/RdtClient.Data/Data/TorrentData.cs b/server/RdtClient.Data/Data/TorrentData.cs new file mode 100644 index 0000000..15732e9 --- /dev/null +++ b/server/RdtClient.Data/Data/TorrentData.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using RdtClient.Data.Enums; +using RdtClient.Data.Models.Data; + +namespace RdtClient.Data.Data +{ + public interface ITorrentData + { + Task> Get(); + Task Get(Guid id); + Task Add(String realDebridId, String hash); + Task UpdateRdData(Torrent torrent); + Task UpdateStatus(Guid torrentId, TorrentStatus status); + Task Delete(Guid id); + } + + public class TorrentData : ITorrentData + { + private readonly DataContext _dataContext; + + public TorrentData(DataContext dataContext) + { + _dataContext = dataContext; + } + + public async Task> Get() + { + return await _dataContext.Torrents.ToListAsync(); + } + + public async Task Get(Guid id) + { + var results = await _dataContext.Torrents + .Include(m => m.Downloads) + .FirstOrDefaultAsync(m => m.TorrentId == id); + + foreach (var file in results.Downloads) + { + file.Torrent = null; + } + + return results; + } + + public async Task Add(String realDebridId, String hash) + { + var torrent = new Torrent + { + TorrentId = Guid.NewGuid(), + RdId = realDebridId, + Hash = hash, + Status = TorrentStatus.RealDebrid + }; + + _dataContext.Torrents.Add(torrent); + + await _dataContext.SaveChangesAsync(); + + return torrent; + } + + public async Task UpdateRdData(Torrent torrent) + { + var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId); + + dbTorrent.RdName = torrent.RdName; + dbTorrent.RdSize = torrent.RdSize; + dbTorrent.RdHost = torrent.RdHost; + dbTorrent.RdSplit = torrent.RdSplit; + dbTorrent.RdProgress = torrent.RdProgress; + dbTorrent.RdStatus = torrent.RdStatus; + dbTorrent.RdAdded = torrent.RdAdded; + dbTorrent.RdEnded = torrent.RdEnded; + dbTorrent.RdSpeed = torrent.RdSpeed; + dbTorrent.RdSeeders = torrent.RdSeeders; + + await _dataContext.SaveChangesAsync(); + } + + public async Task UpdateStatus(Guid torrentId, TorrentStatus status) + { + var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); + + dbTorrent.Status = status; + + await _dataContext.SaveChangesAsync(); + } + + public async Task Delete(Guid id) + { + var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == id); + + _dataContext.Torrents.Remove(dbTorrent); + + await _dataContext.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/server/RdtClient.Data/DataContext/DataContext.cs b/server/RdtClient.Data/DataContext/DataContext.cs deleted file mode 100644 index 18ee4c7..0000000 --- a/server/RdtClient.Data/DataContext/DataContext.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; - -namespace RdtClient.Data.DataContext -{ - public class DataContext : IdentityDbContext - { - - } -} diff --git a/server/RdtClient.Data/DiConfig.cs b/server/RdtClient.Data/DiConfig.cs index f0b7288..72ceb17 100644 --- a/server/RdtClient.Data/DiConfig.cs +++ b/server/RdtClient.Data/DiConfig.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using RdtClient.Data.Data; namespace RdtClient.Data { @@ -6,7 +7,9 @@ namespace RdtClient.Data { public static void Config(IServiceCollection services) { - //services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } } diff --git a/server/RdtClient.Data/Enums/DownloadStatus.cs b/server/RdtClient.Data/Enums/DownloadStatus.cs new file mode 100644 index 0000000..9e97a1d --- /dev/null +++ b/server/RdtClient.Data/Enums/DownloadStatus.cs @@ -0,0 +1,9 @@ +namespace RdtClient.Data.Enums +{ + public enum DownloadStatus + { + PendingDownload = 0, + Downloading, + Finished + } +} diff --git a/server/RdtClient.Data/Enums/TorrentStatus.cs b/server/RdtClient.Data/Enums/TorrentStatus.cs new file mode 100644 index 0000000..f89f7cf --- /dev/null +++ b/server/RdtClient.Data/Enums/TorrentStatus.cs @@ -0,0 +1,12 @@ +namespace RdtClient.Data.Enums +{ + public enum TorrentStatus + { + RealDebrid = 0, + WaitingForDownload, + Downloading, + Finished, + + Error = 99 + } +} diff --git a/server/RdtClient.Data/Migrations/20200403195110_Initial.Designer.cs b/server/RdtClient.Data/Migrations/20200403195110_Initial.Designer.cs new file mode 100644 index 0000000..2c0f547 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20200403195110_Initial.Designer.cs @@ -0,0 +1,395 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RdtClient.Data.Data; + +namespace RdtClient.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20200403195110_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + 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"); + }); + + 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") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + 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") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.Property("DownloadId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TorrentId") + .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("Type") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + + b.HasData( + new + { + SettingId = "RealDebridApiKey", + Type = "String", + Value = "" + }, + new + { + SettingId = "DownloadFolder", + Type = "String", + Value = "C:\\Downloads" + }, + new + { + SettingId = "SpeedLimit", + Type = "Int32", + Value = "0" + }, + new + { + SettingId = "SegmentsLimit", + Type = "Int32", + Value = "10" + }); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Property("TorrentId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + 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("TEXT"); + + b.Property("Status") + .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.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent") + .WithMany("Downloads") + .HasForeignKey("TorrentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/RdtClient.Data/Migrations/20200403195110_Initial.cs b/server/RdtClient.Data/Migrations/20200403195110_Initial.cs new file mode 100644 index 0000000..c31bd57 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20200403195110_Initial.cs @@ -0,0 +1,305 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RdtClient.Data.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + SecurityStamp = table.Column(nullable: true), + ConcurrencyStamp = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + TwoFactorEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + LockoutEnabled = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Settings", + columns: table => new + { + SettingId = table.Column(nullable: false), + Value = table.Column(nullable: true), + Type = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Settings", x => x.SettingId); + }); + + migrationBuilder.CreateTable( + name: "Torrents", + columns: table => new + { + TorrentId = table.Column(nullable: false), + Hash = table.Column(nullable: true), + Status = table.Column(nullable: false), + RdId = table.Column(nullable: true), + RdName = table.Column(nullable: true), + RdSize = table.Column(nullable: false), + RdHost = table.Column(nullable: true), + RdSplit = table.Column(nullable: false), + RdProgress = table.Column(nullable: false), + RdStatus = table.Column(nullable: true), + RdAdded = table.Column(nullable: false), + RdEnded = table.Column(nullable: true), + RdSpeed = table.Column(nullable: true), + RdSeeders = table.Column(nullable: true), + RdFiles = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Torrents", x => x.TorrentId); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Downloads", + columns: table => new + { + DownloadId = table.Column(nullable: false), + TorrentId = table.Column(nullable: false), + Link = table.Column(nullable: true), + Added = table.Column(nullable: false), + Status = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Downloads", x => x.DownloadId); + table.ForeignKey( + name: "FK_Downloads_Torrents_TorrentId", + column: x => x.TorrentId, + principalTable: "Torrents", + principalColumn: "TorrentId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Settings", + columns: new[] { "SettingId", "Type", "Value" }, + values: new object[] { "RealDebridApiKey", "String", "" }); + + migrationBuilder.InsertData( + table: "Settings", + columns: new[] { "SettingId", "Type", "Value" }, + values: new object[] { "DownloadFolder", "String", "C:\\Downloads" }); + + migrationBuilder.InsertData( + table: "Settings", + columns: new[] { "SettingId", "Type", "Value" }, + values: new object[] { "DownloadLimit", "Int32", "1" }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Downloads_TorrentId", + table: "Downloads", + column: "TorrentId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "Downloads"); + + migrationBuilder.DropTable( + name: "Settings"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "Torrents"); + } + } +} diff --git a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs new file mode 100644 index 0000000..91277f7 --- /dev/null +++ b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs @@ -0,0 +1,387 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RdtClient.Data.Data; + +namespace RdtClient.Data.Migrations +{ + [DbContext(typeof(DataContext))] + partial class DataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + 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"); + }); + + 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") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + 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") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.Property("DownloadId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TorrentId") + .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("Type") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + + b.HasData( + new + { + SettingId = "RealDebridApiKey", + Type = "String", + Value = "" + }, + new + { + SettingId = "DownloadFolder", + Type = "String", + Value = "C:\\Downloads" + }, + new + { + SettingId = "DownloadLimit", + Type = "Int32", + Value = "10" + }); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Property("TorrentId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + 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("TEXT"); + + b.Property("Status") + .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.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent") + .WithMany("Downloads") + .HasForeignKey("TorrentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/RdtClient.Data/Models/Data/Download.cs b/server/RdtClient.Data/Models/Data/Download.cs new file mode 100644 index 0000000..c94f803 --- /dev/null +++ b/server/RdtClient.Data/Models/Data/Download.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using RdtClient.Data.Enums; + +namespace RdtClient.Data.Models.Data +{ + public class Download + { + [Key] + public Guid DownloadId { get; set; } + + public Guid TorrentId { get; set; } + + public String Link { get; set; } + + public DateTimeOffset Added { get; set; } + + public DownloadStatus Status { get; set; } + + [ForeignKey("TorrentId")] + public Torrent Torrent { get; set; } + + [NotMapped] + public Int32 Progress { get; set; } + } +} diff --git a/server/RdtClient.Data/Models/Data/Setting.cs b/server/RdtClient.Data/Models/Data/Setting.cs new file mode 100644 index 0000000..1a50983 --- /dev/null +++ b/server/RdtClient.Data/Models/Data/Setting.cs @@ -0,0 +1,15 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace RdtClient.Data.Models.Data +{ + public class Setting + { + [Key] + public String SettingId { get; set; } + + public String Value { get; set; } + + public String Type { get; set; } + } +} diff --git a/server/RdtClient.Data/Models/Data/Torrent.cs b/server/RdtClient.Data/Models/Data/Torrent.cs new file mode 100644 index 0000000..a76bead --- /dev/null +++ b/server/RdtClient.Data/Models/Data/Torrent.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; +using RDNET.Models; +using RdtClient.Data.Enums; + +namespace RdtClient.Data.Models.Data +{ + public class Torrent + { + [Key] + public Guid TorrentId { get; set; } + + public String Hash { get; set; } + + public TorrentStatus Status { get; set; } + + [InverseProperty("Torrent")] + public IList Downloads { get; set; } + + public String RdId { get; set; } + public String RdName { get; set; } + public Int64 RdSize { get; set; } + public String RdHost { get; set; } + public Int64 RdSplit { get; set; } + public Int64 RdProgress { get; set; } + public String RdStatus { get; set; } + public DateTimeOffset RdAdded { get; set; } + public DateTimeOffset? RdEnded { get; set; } + public Int64? RdSpeed { get; set; } + public Int64? RdSeeders { get; set; } + public String RdFiles { get; set; } + + [NotMapped] + public Int32 DownloadProgress { get; set; } + + [NotMapped] + public IList Files + { + get + { + if (String.IsNullOrWhiteSpace(RdFiles)) + { + return new List(); + } + + try + { + return JsonConvert.DeserializeObject>(RdFiles); + } + catch + { + return new List(); + } + } + } + } +} diff --git a/server/RdtClient.Data/Models/Internal/AppSettings.cs b/server/RdtClient.Data/Models/Internal/AppSettings.cs index 8eb0313..6a603bc 100644 --- a/server/RdtClient.Data/Models/Internal/AppSettings.cs +++ b/server/RdtClient.Data/Models/Internal/AppSettings.cs @@ -1,14 +1,6 @@ -using System; - -namespace RdtClient.Data.Models.Internal +namespace RdtClient.Data.Models.Internal { public class AppSettings { - public AppSettingsConnectionStrings ConnectionStrings { get; set; } - } - - public class AppSettingsConnectionStrings - { - public String Client { get; set; } } } diff --git a/server/RdtClient.Data/RdtClient.Data.csproj b/server/RdtClient.Data/RdtClient.Data.csproj index ea3151a..883337a 100644 --- a/server/RdtClient.Data/RdtClient.Data.csproj +++ b/server/RdtClient.Data/RdtClient.Data.csproj @@ -4,14 +4,47 @@ netcoreapp3.1 + + + + + + + + + + + + + + + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + ..\libs\RDNET.dll + diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index 688f566..11e088b 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using RdtClient.Service.Services; namespace RdtClient.Service { @@ -6,7 +7,10 @@ namespace RdtClient.Service { public static void Config(IServiceCollection services) { - //services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } -} +} \ No newline at end of file diff --git a/server/RdtClient.Service/Models/Profile.cs b/server/RdtClient.Service/Models/Profile.cs new file mode 100644 index 0000000..c2eaa11 --- /dev/null +++ b/server/RdtClient.Service/Models/Profile.cs @@ -0,0 +1,10 @@ +using System; + +namespace RdtClient.Service.Models +{ + public class Profile + { + public String UserName { get; set; } + public DateTimeOffset Expiration { get; set; } + } +} diff --git a/server/RdtClient.Service/Properties/launchSettings.json b/server/RdtClient.Service/Properties/launchSettings.json new file mode 100644 index 0000000..7eb414c --- /dev/null +++ b/server/RdtClient.Service/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53591/", + "sslPort": 44310 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "RdtClient.Service": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/server/RdtClient.Service/RdtClient.Service.csproj b/server/RdtClient.Service/RdtClient.Service.csproj index 3d39cc9..ed62c6d 100644 --- a/server/RdtClient.Service/RdtClient.Service.csproj +++ b/server/RdtClient.Service/RdtClient.Service.csproj @@ -1,11 +1,25 @@ - + netcoreapp3.1 + library + + + + + + + + + + ..\libs\RDNET.dll + + + diff --git a/server/RdtClient.Service/RdtClient.Service.csproj.user b/server/RdtClient.Service/RdtClient.Service.csproj.user new file mode 100644 index 0000000..cff74a9 --- /dev/null +++ b/server/RdtClient.Service/RdtClient.Service.csproj.user @@ -0,0 +1,6 @@ + + + + IIS Express + + \ No newline at end of file diff --git a/server/RdtClient.Service/Services/DownloadManager.cs b/server/RdtClient.Service/Services/DownloadManager.cs new file mode 100644 index 0000000..cdab342 --- /dev/null +++ b/server/RdtClient.Service/Services/DownloadManager.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using RdtClient.Data.Data; +using RdtClient.Data.Enums; +using RdtClient.Data.Models.Data; +using SharpCompress.Common; +using SharpCompress.Readers; + +namespace RdtClient.Service.Services +{ + public class DownloadManager + { + public static readonly ConcurrentDictionary ActiveDownloads = new ConcurrentDictionary(); + + private static readonly Object Lock = new Object(); + + static DownloadManager() + { + ServicePointManager.Expect100Continue = false; + ServicePointManager.DefaultConnectionLimit = 100; + ServicePointManager.MaxServicePointIdleTime = 1000; + } + + public static async Task Download(Download download, String destinationFolderPath) + { + await UpdateStatus(download.DownloadId, DownloadStatus.Downloading, TorrentStatus.Downloading); + + if (!ActiveDownloads.TryAdd(download.DownloadId, download)) + { + return; + } + + var fileUrl = download.Link; + + var uri = new Uri(fileUrl); + var filePath = Path.Combine(destinationFolderPath, uri.Segments.Last()); + + var webRequest = WebRequest.Create(fileUrl); + webRequest.Method = "HEAD"; + Int64 responseLength; + using (var webResponse = await webRequest.GetResponseAsync()) + { + responseLength = Int64.Parse(webResponse.Headers.Get("Content-Length")); + } + + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + var request = WebRequest.Create(fileUrl); + using (var response = await request.GetResponseAsync()) + { + await using var stream = response.GetResponseStream(); + await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write); + + var buffer = new Byte[4096]; + + while (fileStream.Length < response.ContentLength) + { + var read = stream.Read(buffer, 0, buffer.Length); + + if (read > 0) + { + fileStream.Write(buffer, 0, read); + + download.Progress = (Int32) (fileStream.Length * 100 / responseLength); + ActiveDownloads.TryAdd(download.DownloadId, download); + } + else + { + break; + } + } + } + + try + { + await using Stream stream = File.OpenRead(filePath); + + var reader = ReaderFactory.Open(stream); + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + Console.WriteLine(reader.Entry.Key); + reader.WriteEntryToDirectory(destinationFolderPath, + new ExtractionOptions + { + ExtractFullPath = true, + Overwrite = true + }); + } + } + + File.Delete(filePath); + } + catch + { + // ignored + } + + await UpdateStatus(download.DownloadId, DownloadStatus.Finished, TorrentStatus.Finished); + + ActiveDownloads.TryRemove(download.DownloadId, out _); + } + + private static async Task UpdateStatus(Guid downloadId, DownloadStatus downloadStatus, TorrentStatus torrentStatus) + { + await using var context = new DataContext(); + + var download = await context.Downloads.FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + download.Status = downloadStatus; + + await context.SaveChangesAsync(); + + var torrent = await context.Torrents.FirstOrDefaultAsync(m => m.TorrentId == download.TorrentId); + + if (torrentStatus == TorrentStatus.Finished) + { + var allDownloads = await context.Downloads.Where(m => m.TorrentId == download.TorrentId) + .ToListAsync(); + + if (allDownloads.All(m => m.Status == DownloadStatus.Finished)) + { + torrent.Status = TorrentStatus.Finished; + } + else + { + torrent.Status = TorrentStatus.Downloading; + } + } + else + { + torrent.Status = torrentStatus; + } + + await context.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/server/RdtClient.Service/Services/Downloads.cs b/server/RdtClient.Service/Services/Downloads.cs new file mode 100644 index 0000000..b407012 --- /dev/null +++ b/server/RdtClient.Service/Services/Downloads.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using RdtClient.Data.Data; +using RdtClient.Data.Enums; +using RdtClient.Data.Models.Data; + +namespace RdtClient.Service.Services +{ + public interface IDownloads + { + Task> Get(); + Task Add(Guid torrentId, String link); + Task UpdateStatus(Guid downloadId, DownloadStatus status); + Task DeleteForTorrent(Guid torrentId); + } + + public class Downloads : IDownloads + { + private readonly IDownloadData _downloadData; + private readonly ISettings _settings; + + public Downloads(IDownloadData downloadData, ISettings settings) + { + _downloadData = downloadData; + _settings = settings; + } + + public async Task> Get() + { + return await _downloadData.Get(); + } + + public async Task Add(Guid torrentId, String link) + { + return await _downloadData.Add(torrentId, link); + } + + public async Task UpdateStatus(Guid downloadId, DownloadStatus status) + { + await _downloadData.UpdateStatus(downloadId, status); + } + + public async Task DeleteForTorrent(Guid torrentId) + { + await _downloadData.DeleteForTorrent(torrentId); + } + } +} diff --git a/server/RdtClient.Service/Services/Scheduler.cs b/server/RdtClient.Service/Services/Scheduler.cs new file mode 100644 index 0000000..98e6767 --- /dev/null +++ b/server/RdtClient.Service/Services/Scheduler.cs @@ -0,0 +1,62 @@ +using System.Linq; +using System.Threading.Tasks; +using Hangfire; +using RdtClient.Data.Enums; + +namespace RdtClient.Service.Services +{ + public interface IScheduler + { + void Start(); + Task Process(); + } + + public class Scheduler : IScheduler + { + private readonly IDownloads _downloads; + private readonly ISettings _settings; + private readonly ITorrents _torrents; + + public Scheduler(ITorrents torrents, IDownloads downloads, ISettings settings) + { + _torrents = torrents; + _downloads = downloads; + _settings = settings; + } + + public void Start() + { + RecurringJob.AddOrUpdate(() => Process(), "* * * * *"); + BackgroundJob.Enqueue(() => Process()); + } + + [DisableConcurrentExecution(5)] + public async Task Process() + { + await _torrents.Update(); + + var downloads = await _downloads.Get(); + + downloads = downloads.Where(m => m.Status != DownloadStatus.Finished) + .OrderByDescending(m => m.Status) + .ThenByDescending(m => m.Added) + .ToList(); + + var maxDownloads = await _settings.GetNumber("DownloadLimit"); + var destinationFolderPath = await _settings.GetString("DownloadFolder"); + + foreach (var download in downloads) + { + if (DownloadManager.ActiveDownloads.Count >= maxDownloads) + { + return; + } + + download.Torrent = null; + BackgroundJob.Enqueue(() => DownloadManager.Download(download, destinationFolderPath)); + + await Task.Delay(1000); + } + } + } +} \ No newline at end of file diff --git a/server/RdtClient.Service/Services/Settings.cs b/server/RdtClient.Service/Services/Settings.cs new file mode 100644 index 0000000..282cab9 --- /dev/null +++ b/server/RdtClient.Service/Services/Settings.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using RdtClient.Data.Data; +using RdtClient.Data.Models.Data; + +namespace RdtClient.Service.Services +{ + public interface ISettings + { + Task> GetAll(); + Task Update(IList settings); + Task GetString(String key); + Task GetNumber(String key); + } + + public class Settings : ISettings + { + private readonly ISettingData _settingData; + + public Settings(ISettingData settingData) + { + _settingData = settingData; + } + + public async Task> GetAll() + { + return await _settingData.GetAll(); + } + + public async Task Update(IList settings) + { + await _settingData.Update(settings); + } + + public async Task GetString(String key) + { + var setting = await _settingData.Get(key); + + if (setting == null) + { + throw new Exception($"Setting with key {key} not found"); + } + + return setting.Value; + } + + public async Task GetNumber(String key) + { + var setting = await _settingData.Get(key); + + if (setting == null) + { + throw new Exception($"Setting with key {key} not found"); + } + + return Int32.Parse(setting.Value); + } + } +} diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs new file mode 100644 index 0000000..02c8fe3 --- /dev/null +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using RDNET; +using RdtClient.Data.Data; +using RdtClient.Data.Enums; +using RdtClient.Data.Models.Data; +using RdtClient.Service.Models; + +namespace RdtClient.Service.Services +{ + public interface ITorrents + { + Task> Get(); + Task Get(Guid id); + Task> Update(); + Task UploadMagnet(String magnetLink); + Task UploadFile(Byte[] bytes); + Task Delete(Guid id); + Task Download(Guid id); + void Reset(); + Task GetProfile(); + } + + public class Torrents : ITorrents + { + private readonly ITorrentData _torrentData; + private readonly ISettings _settings; + private readonly IDownloads _downloads; + + private static RdNetClient _rdtClient; + + private static DateTime _rdtLastUpdate = DateTime.UtcNow; + + private static readonly SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1,1); + + private RdNetClient RdNetClient + { + get + { + if (_rdtClient == null) + { + var apiKey = _settings.GetString("RealDebridApiKey").Result; + + if (String.IsNullOrWhiteSpace(apiKey)) + { + throw new Exception("RealDebrid API Key not set in the settings"); + } + + _rdtClient = new RdNetClient("X245A4XAIBGVM", null, null, null, null, apiKey); + } + + return _rdtClient; + } + } + + public Torrents(ITorrentData torrentData, ISettings settings, IDownloads downloads) + { + _torrentData = torrentData; + _settings = settings; + _downloads = downloads; + } + + public async Task> Get() + { + var torrents = await _torrentData.Get(); + + if (DateTime.UtcNow > _rdtLastUpdate) + { + _rdtLastUpdate = DateTime.UtcNow.AddSeconds(10); + + torrents = await Update(); + } + + foreach (var torrent in torrents) + { + var downloads = DownloadManager.ActiveDownloads.Where(m => m.Value.TorrentId == torrent.TorrentId).ToList(); + + if (torrent.Files.Count > 0) + { + torrent.DownloadProgress = downloads.Sum(m => m.Value.Progress) / torrent.Files.Count; + } + } + + return torrents; + } + + public async Task Get(Guid id) + { + var torrent = await _torrentData.Get(id); + + if (torrent != null) + { + var rdTorrent = await RdNetClient.TorrentInfoAsync(torrent.RdId); + + await Update(torrent, rdTorrent); + } + + return torrent; + } + + public async Task> Update() + { + var torrents = await _torrentData.Get(); + + var w = await SemaphoreSlim.WaitAsync(10); + if (!w) + { + return torrents; + } + + try + { + var rdTorrents = await RdNetClient.TorrentsAsync(0, 100); + + foreach (var rdTorrent in rdTorrents) + { + var torrent = torrents.FirstOrDefault(m => m.RdId == rdTorrent.Id); + + if (torrent == null) + { + var newTorrent = await _torrentData.Add(rdTorrent.Id, rdTorrent.Hash); + await Get(newTorrent.TorrentId); + } + else + { + await Update(torrent, rdTorrent); + } + } + + foreach (var torrent in torrents) + { + var rdTorrent = rdTorrents.FirstOrDefault(m => m.Id == torrent.RdId); + + if (rdTorrent == null) + { + await _torrentData.Delete(torrent.TorrentId); + } + } + + return torrents; + } + finally + { + SemaphoreSlim.Release(); + } + } + + public async Task UploadMagnet(String magnetLink) + { + var magnet = MonoTorrent.MagnetLink.Parse(magnetLink); + + var rdTorrent = await RdNetClient.TorrentAddMagnet(magnetLink); + + await Add(rdTorrent.Id, magnet.InfoHash.ToHex()); + } + + public async Task UploadFile(Byte[] bytes) + { + var torrent = MonoTorrent.Torrent.Load(bytes); + + var rdTorrent = await RdNetClient.TorrentAddFile(bytes); + + await Add(rdTorrent.Id, torrent.InfoHash.ToHex()); + } + + public async Task Delete(Guid id) + { + var torrent = await Get(id); + + if (torrent != null) + { + await _torrentData.Delete(id); + await RdNetClient.TorrentDelete(torrent.RdId); + } + } + + public async Task Download(Guid id) + { + var torrent = await _torrentData.Get(id); + + await _downloads.DeleteForTorrent(id); + + var rdTorrent = await RdNetClient.TorrentInfoAsync(torrent.RdId); + + foreach (var file in rdTorrent.Files) + { + var unrestrictedLink = await RdNetClient.UnrestrictLinkAsync(file.Path); + + await _downloads.Add(torrent.TorrentId, unrestrictedLink.Download); + } + } + + public void Reset() + { + _rdtClient = null; + _rdtLastUpdate = DateTime.UtcNow; + } + + public async Task GetProfile() + { + var user = await _rdtClient.UserAsync(); + + var profile = new Profile + { + UserName = user.Username, + Expiration = user.Expiration + }; + + return profile; + } + + private async Task Add(String rdTorrentId, String infoHash) + { + var newTorrent = await _torrentData.Add(rdTorrentId, infoHash); + + var rdTorrent = await RdNetClient.TorrentInfoAsync(rdTorrentId); + + if (rdTorrent.Files != null && rdTorrent.Files.Count > 0) + { + if (!rdTorrent.Files.Any(m => m.Selected)) + { + var fileIds = rdTorrent.Files.Select(m => m.Id.ToString()).ToArray(); + + await RdNetClient.TorrentSelectFiles(rdTorrentId, fileIds); + } + } + + await Update(newTorrent, rdTorrent); + } + + private async Task Update(Torrent torrent, RDNET.Models.Torrent rdTorrent) + { + if (!String.IsNullOrWhiteSpace(rdTorrent.Filename)) + { + torrent.RdName = rdTorrent.Filename; + } + else if (!String.IsNullOrWhiteSpace(rdTorrent.OriginalFilename)) + { + torrent.RdName = rdTorrent.OriginalFilename; + } + + if (rdTorrent.Bytes > 0) + { + torrent.RdSize = rdTorrent.Bytes; + } + else if (rdTorrent.OriginalBytes > 0) + { + torrent.RdSize = rdTorrent.OriginalBytes; + } + + if (rdTorrent.Files != null) + { + torrent.RdFiles = JsonConvert.SerializeObject(rdTorrent.Files); + } + + torrent.RdHost = rdTorrent.Host; + torrent.RdSplit = rdTorrent.Split; + torrent.RdProgress = rdTorrent.Progress; + torrent.RdStatus = rdTorrent.Status; + torrent.RdAdded = rdTorrent.Added; + torrent.RdEnded = rdTorrent.Ended; + torrent.RdSpeed = rdTorrent.Speed; + torrent.RdSeeders = rdTorrent.Seeders; + + await _torrentData.UpdateRdData(torrent); + + if (torrent.Status == TorrentStatus.RealDebrid && torrent.RdProgress == 100) + { + await _torrentData.UpdateStatus(torrent.TorrentId, TorrentStatus.WaitingForDownload); + } + } + } +} diff --git a/server/RdtClient.Web/Controllers/SettingsController.cs b/server/RdtClient.Web/Controllers/SettingsController.cs new file mode 100644 index 0000000..7a350e5 --- /dev/null +++ b/server/RdtClient.Web/Controllers/SettingsController.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using RdtClient.Data.Models.Data; +using RdtClient.Service.Models; +using RdtClient.Service.Services; + +namespace RdtClient.Web.Controllers +{ + [Route("Api/Settings")] + public class SettingsController : Controller + { + private readonly ISettings _settings; + private readonly ITorrents _torrents; + + public SettingsController(ISettings settings, ITorrents torrents) + { + _settings = settings; + _torrents = torrents; + } + + [HttpGet] + [Route("")] + public async Task>> Get() + { + try + { + var result = await _settings.GetAll(); + return Ok(result); + } + catch (Exception ex) + { + return BadRequest(ex); + } + } + + [HttpPut] + [Route("")] + public async Task Update([FromBody] SettingsControllerUpdateRequest request) + { + try + { + await _settings.Update(request.Settings); + _torrents.Reset(); + + return Ok(); + } + catch (Exception ex) + { + return BadRequest(ex); + } + } + + [HttpGet] + [Route("Profile")] + public async Task> Profile() + { + try + { + var profile = await _torrents.GetProfile(); + return Ok(profile); + } + catch (Exception ex) + { + return BadRequest(ex); + } + } + } + + public class SettingsControllerUpdateRequest + { + public IList Settings { get; set; } + } +} \ No newline at end of file diff --git a/server/RdtClient.Web/Controllers/TorrentsController.cs b/server/RdtClient.Web/Controllers/TorrentsController.cs new file mode 100644 index 0000000..17e828a --- /dev/null +++ b/server/RdtClient.Web/Controllers/TorrentsController.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RdtClient.Data.Models.Data; +using RdtClient.Service.Services; + +namespace RdtClient.Web.Controllers +{ + [Route("Api/Torrents")] + public class TorrentsController : Controller + { + private readonly ITorrents _torrents; + private readonly IScheduler _scheduler; + + public TorrentsController(ITorrents torrents, IScheduler scheduler) + { + _torrents = torrents; + _scheduler = scheduler; + } + + [HttpGet] + [Route("")] + public async Task>> Get() + { + try + { + var result = await _torrents.Get(); + return Ok(result); + } + catch (Exception ex) + { + return BadRequest(ex); + } + } + + [HttpGet] + [Route("{id}")] + public async Task> Get(Guid id) + { + try + { + var result = await _torrents.Get(id); + + if (result == null) + { + throw new Exception("Torrent not found"); + } + + return Ok(result); + } + catch (Exception ex) + { + return BadRequest(ex); + } + } + + [HttpPost] + [Route("UploadFile")] + public async Task UploadFile([FromForm] IFormFile file) + { + try + { + if (file == null || file.Length <= 0) + { + throw new Exception("Invalid torrent file"); + } + + var fileStream = file.OpenReadStream(); + + await using var memoryStream = new MemoryStream(); + + fileStream.CopyTo(memoryStream); + + var bytes = memoryStream.ToArray(); + + await _torrents.UploadFile(bytes); + + return Ok(); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPost] + [Route("UploadMagnet")] + public async Task UploadMagnet([FromBody] TorrentControllerUploadMagnetRequest request) + { + try + { + await _torrents.UploadMagnet(request.MagnetLink); + + return Ok(); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpDelete] + [Route("{id}")] + public async Task Delete(Guid id) + { + try + { + await _torrents.Delete(id); + + return Ok(); + } + catch(Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpGet] + [Route("Download/{id}")] + public async Task Download(Guid id) + { + try + { + await _torrents.Download(id); + await _scheduler.Process(); + + return Ok(); + } + catch(Exception ex) + { + return BadRequest(ex.Message); + } + } + } + + public class TorrentControllerUploadMagnetRequest + { + public String MagnetLink { get; set; } + } +} diff --git a/server/RdtClient.Web/Program.cs b/server/RdtClient.Web/Program.cs index 208bb44..7023f9f 100644 --- a/server/RdtClient.Web/Program.cs +++ b/server/RdtClient.Web/Program.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace RdtClient.Web { @@ -13,8 +14,15 @@ namespace RdtClient.Web .Run(); } - private static IHostBuilder CreateHostBuilder(String[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + private static IHostBuilder CreateHostBuilder(String[] args) + { + return Host + .CreateDefaultBuilder(args) + .ConfigureLogging(logging => + { + logging.AddFile("app.log"); + }) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } } } \ No newline at end of file diff --git a/server/RdtClient.Web/Properties/launchSettings.json b/server/RdtClient.Web/Properties/launchSettings.json index 93fe097..8c47cc0 100644 --- a/server/RdtClient.Web/Properties/launchSettings.json +++ b/server/RdtClient.Web/Properties/launchSettings.json @@ -1,18 +1,16 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", +{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:52923", - "sslPort": 44356 + "applicationUrl": "http://localhost:4201", + "sslPort": 0 } }, + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "IIS Express": { "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "weatherforecast", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -21,10 +19,10 @@ "commandName": "Project", "launchBrowser": true, "launchUrl": "weatherforecast", - "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" } } -} +} \ No newline at end of file diff --git a/server/RdtClient.Web/RdtClient.Web.csproj b/server/RdtClient.Web/RdtClient.Web.csproj index cc58959..b32f011 100644 --- a/server/RdtClient.Web/RdtClient.Web.csproj +++ b/server/RdtClient.Web/RdtClient.Web.csproj @@ -5,11 +5,13 @@ - + + + @@ -19,6 +21,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/server/RdtClient.Web/Startup.cs b/server/RdtClient.Web/Startup.cs index 7b82547..6911319 100644 --- a/server/RdtClient.Web/Startup.cs +++ b/server/RdtClient.Web/Startup.cs @@ -1,3 +1,5 @@ +using Hangfire; +using Hangfire.MemoryStorage; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -6,8 +8,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using RdtClient.Data.DataContext; +using RdtClient.Data.Data; using RdtClient.Data.Models.Internal; +using RdtClient.Service.Services; namespace RdtClient.Web { @@ -27,7 +30,7 @@ namespace RdtClient.Web services.AddSingleton(appSettings); - services.AddDbContext(options => options.UseSqlite(appSettings.ConnectionStrings.Client)); + services.AddDbContext(options => options.UseSqlite(DataContext.ConnectionString)); services.AddControllers(); @@ -51,6 +54,8 @@ namespace RdtClient.Web services.AddHttpsRedirection(options => { options.HttpsPort = 443; }); + services.AddHttpClient(); + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.SlidingExpiration = true; }); @@ -68,15 +73,22 @@ namespace RdtClient.Web .AddEntityFrameworkStores() .AddDefaultTokenProviders(); + services.AddHangfire(configuration => configuration + .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseMemoryStorage()); + + services.AddHangfireServer(); + Data.DiConfig.Config(services); Service.DiConfig.Config(services); } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext dataContext, IScheduler scheduler) { if (env.IsDevelopment()) { - app.UseDeveloperExceptionPage(); app.UseCors("Dev"); } else @@ -92,6 +104,8 @@ namespace RdtClient.Web app.UseAuthorization(); + app.UseHangfireServer(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); @@ -101,6 +115,10 @@ namespace RdtClient.Web { spa.Options.SourcePath = "wwwroot"; }); + + dataContext.Migrate(); + + scheduler.Start(); } } } \ No newline at end of file diff --git a/server/RdtClient.Web/app.log b/server/RdtClient.Web/app.log new file mode 100644 index 0000000..59e484e --- /dev/null +++ b/server/RdtClient.Web/app.log @@ -0,0 +1,512 @@ +2020-04-03T15:59:20.1355273-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T15:59:20.1385649-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T15:59:20.2294123-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T15:59:20.2294368-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T15:59:20.2510079-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:17836:3146ca38 successfully announced in 101.8542 ms +2020-04-03T15:59:20.2510111-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:17836:f605d0e0 successfully announced in 20.7292 ms +2020-04-03T15:59:20.2541883-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:17836:3146ca38 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T15:59:20.2541883-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:17836:f605d0e0 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T15:59:20.2588662-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:17836:f605d0e0 all the dispatchers started +2020-04-03T15:59:20.2589265-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:17836:3146ca38 all the dispatchers started +2020-04-03T15:59:20.9901692-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-03T15:59:20.9902829-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-03T15:59:20.9903021-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-03T15:59:22.6292638-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T15:59:22.6322230-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T15:59:22.7242705-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T15:59:22.7243034-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T15:59:22.7452766-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:c48269cc successfully announced in 19.9586 ms +2020-04-03T15:59:22.7452765-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:15034db2 successfully announced in 101.7602 ms +2020-04-03T15:59:22.7481327-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:15034db2 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T15:59:22.7481351-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:c48269cc is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T15:59:22.7533167-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:15034db2 all the dispatchers started +2020-04-03T15:59:22.7535232-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:c48269cc all the dispatchers started +2020-04-03T15:59:23.4204963-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-03T15:59:23.4206249-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-03T15:59:23.4206510-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-03T15:59:30.7772267-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:c48269cc caught stopping signal... +2020-04-03T15:59:30.7779767-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application is shutting down... +2020-04-03T15:59:30.7783343-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:c48269cc caught stopped signal... +2020-04-03T15:59:30.7794920-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:15034db2 caught stopping signal... +2020-04-03T15:59:30.7796436-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:15034db2 caught stopped signal... +2020-04-03T15:59:30.7812297-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:15034db2 All dispatchers stopped +2020-04-03T15:59:30.7812297-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:c48269cc All dispatchers stopped +2020-04-03T15:59:30.7828238-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:15034db2 successfully reported itself as stopped in 0.2783 ms +2020-04-03T15:59:30.7828222-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:c48269cc successfully reported itself as stopped in 0.2778 ms +2020-04-03T15:59:30.7828386-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:15034db2 has been stopped in total 3.303 ms +2020-04-03T15:59:30.7828431-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25392:c48269cc has been stopped in total 5.4344 ms +2020-04-03T15:59:49.2420348-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T15:59:49.2449966-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T15:59:49.7330627-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T15:59:49.7342224-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T15:59:49.7767552-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:7656:238a3170 successfully announced in 518.619 ms +2020-04-03T15:59:49.7778417-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:7656:b384ecba successfully announced in 37.4892 ms +2020-04-03T15:59:49.7897107-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:7656:238a3170 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T15:59:49.7907947-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:7656:b384ecba is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T15:59:49.9335939-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:7656:238a3170 all the dispatchers started +2020-04-03T15:59:49.9493934-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:7656:b384ecba all the dispatchers started +2020-04-03T15:59:50.8034032-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-03T15:59:50.8108739-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-03T15:59:50.8127580-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-03T16:06:29.6254854-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T16:06:29.6288727-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T16:06:29.7314839-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T16:06:29.7315143-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T16:06:29.7529384-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18896:dd12c44a successfully announced in 111.5091 ms +2020-04-03T16:06:29.7529396-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18896:15c956d5 successfully announced in 20.4625 ms +2020-04-03T16:06:29.7557415-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18896:dd12c44a is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T16:06:29.7557411-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18896:15c956d5 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T16:06:29.7606532-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18896:15c956d5 all the dispatchers started +2020-04-03T16:06:29.7608761-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18896:dd12c44a all the dispatchers started +2020-04-03T16:06:30.9858222-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-03T16:06:30.9940710-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-03T16:06:30.9966766-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-03T17:20:18.4268508-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T17:20:18.4340796-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T17:20:18.6487165-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T17:20:18.6500981-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T17:20:18.6947841-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:21168:a6490904 successfully announced in 34.9489 ms +2020-04-03T17:20:18.6960854-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:21168:b2e9a60c successfully announced in 230.7784 ms +2020-04-03T17:20:18.7105510-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:21168:a6490904 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T17:20:18.7121052-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:21168:b2e9a60c is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T17:20:18.8720117-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:21168:a6490904 all the dispatchers started +2020-04-03T17:20:18.8774595-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:21168:b2e9a60c all the dispatchers started +2020-04-03T17:20:19.7386911-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-03T17:20:19.7416238-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-03T17:20:19.7486505-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-03T17:59:08.0700362-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T17:59:08.0750113-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T17:59:08.2398601-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-03T17:59:08.2411315-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-03T17:59:08.2913718-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:10716:5ebe162c successfully announced in 42.0268 ms +2020-04-03T17:59:08.2925085-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:10716:fb956c19 successfully announced in 197.5027 ms +2020-04-03T17:59:08.3038936-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:10716:5ebe162c is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T17:59:08.3049895-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:10716:fb956c19 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-03T17:59:08.4396784-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:10716:5ebe162c all the dispatchers started +2020-04-03T17:59:08.4538634-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:10716:fb956c19 all the dispatchers started +2020-04-03T17:59:09.4423137-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-03T17:59:09.4523143-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-03T17:59:09.4550262-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T13:36:51.3899679-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T13:36:51.3954070-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T13:36:52.2046648-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T13:36:52.2058469-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T13:36:52.2165588-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18576:7232176f successfully announced in 5.629 ms +2020-04-04T13:36:52.2175955-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18576:0c930dc4 successfully announced in 783.68 ms +2020-04-04T13:36:52.2258088-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18576:0c930dc4 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T13:36:52.2270592-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18576:7232176f is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T13:36:52.3608248-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18576:0c930dc4 all the dispatchers started +2020-04-04T13:36:52.3651180-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18576:7232176f all the dispatchers started +2020-04-04T13:36:53.3815332-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T13:36:53.3835819-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T13:36:53.3855069-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T13:39:17.4290475-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T13:39:17.4351614-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T13:39:17.6188800-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T13:39:17.6200228-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T13:39:17.6633116-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:12312:46ba0401 successfully announced in 205.437 ms +2020-04-04T13:39:17.6653058-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:12312:626ef5ce successfully announced in 36.9854 ms +2020-04-04T13:39:17.6810736-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:12312:46ba0401 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T13:39:17.6821454-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:12312:626ef5ce is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T13:39:17.8313892-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:12312:626ef5ce all the dispatchers started +2020-04-04T13:39:17.8518228-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:12312:46ba0401 all the dispatchers started +2020-04-04T13:39:18.6777049-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T13:39:18.6853112-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T13:39:18.6872717-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T13:45:52.2178972-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T13:45:52.2209059-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T13:45:52.7257245-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T13:45:52.7268420-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T13:45:52.7605590-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25428:371ab538 successfully announced in 29.31 ms +2020-04-04T13:45:52.7616163-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25428:beb90aed successfully announced in 527.3207 ms +2020-04-04T13:45:52.7947890-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25428:371ab538 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T13:45:52.7958992-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25428:beb90aed is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T13:45:52.9335865-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25428:beb90aed all the dispatchers started +2020-04-04T13:45:52.9433737-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:25428:371ab538 all the dispatchers started +2020-04-04T13:45:53.7610065-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T13:45:53.7667118-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T13:45:53.7686187-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:06:51.9875312-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:06:51.9910970-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:06:52.4849160-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:06:52.4860716-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:06:52.5226185-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:4496:e9cb1104 successfully announced in 517.9776 ms +2020-04-04T14:06:52.5276877-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:4496:2aa9fc9e successfully announced in 32.1255 ms +2020-04-04T14:06:52.5356524-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:4496:2aa9fc9e is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:06:52.5368007-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:4496:e9cb1104 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:06:52.6798034-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:4496:e9cb1104 all the dispatchers started +2020-04-04T14:06:52.6843382-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:4496:2aa9fc9e all the dispatchers started +2020-04-04T14:06:53.5337779-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:06:53.5419397-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:06:53.5438903-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:09:28.6901247-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:09:28.6934944-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:09:28.8003885-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:09:28.8004416-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:09:28.8235716-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11712:2f63d054 successfully announced in 22.2108 ms +2020-04-04T14:09:28.8235763-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11712:6de68e38 successfully announced in 117.3382 ms +2020-04-04T14:09:28.8266785-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11712:6de68e38 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:09:28.8266804-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11712:2f63d054 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:09:28.8331305-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11712:6de68e38 all the dispatchers started +2020-04-04T14:09:28.8332117-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11712:2f63d054 all the dispatchers started +2020-04-04T14:09:30.0597550-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:09:30.0661109-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:09:30.0679982-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:10:30.2700465-06:00 FAIL [Microsoft.AspNetCore.Server.IIS.Core.IISHttpServer] [ApplicationError] Connection ID "18158513750708060161", Request ID "80000002-000c-fc00-b63f-84710c7967bb": An unhandled exception was thrown by the application.System.Text.Json.JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32. + at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_SerializerCycleDetected(Int32 maxDepth) + at System.Text.Json.JsonSerializer.Write(Utf8JsonWriter writer, Int32 originalWriterDepth, Int32 flushThreshold, JsonSerializerOptions options, WriteStack& state) + at System.Text.Json.JsonSerializer.WriteAsyncCore(Stream utf8Json, Object value, Type inputType, JsonSerializerOptions options, CancellationToken cancellationToken) + at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|27_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) + at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) + at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) + at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) + at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) + at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync() + +2020-04-04T14:11:12.4711056-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:11:12.4740177-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:11:12.5685353-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:11:12.5685678-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:11:12.5896902-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11556:f733969e successfully announced in 104.0185 ms +2020-04-04T14:11:12.5896902-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11556:85c7f28f successfully announced in 20.4301 ms +2020-04-04T14:11:12.5927719-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11556:85c7f28f is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:11:12.5927773-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11556:f733969e is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:11:12.5982186-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11556:85c7f28f all the dispatchers started +2020-04-04T14:11:12.5982679-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11556:f733969e all the dispatchers started +2020-04-04T14:11:13.8266479-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:11:13.8300908-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:11:13.8397525-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:23:21.7687075-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:23:21.7715620-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:23:21.8650133-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:23:21.8650397-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:23:21.8862045-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:13516:63077182 successfully announced in 103.9192 ms +2020-04-04T14:23:21.8862045-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:13516:8f1cdbd3 successfully announced in 19.9902 ms +2020-04-04T14:23:21.8889891-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:13516:63077182 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:23:21.8889884-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:13516:8f1cdbd3 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:23:21.8939088-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:13516:63077182 all the dispatchers started +2020-04-04T14:23:21.8939936-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:13516:8f1cdbd3 all the dispatchers started +2020-04-04T14:23:23.0689646-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:23:23.0709673-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:23:23.0779228-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:24:37.9847342-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:24:37.9882985-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:24:38.0825819-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:24:38.0826064-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:24:38.1036306-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22580:c1366df7 successfully announced in 103.9479 ms +2020-04-04T14:24:38.1036305-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22580:e3e831b7 successfully announced in 20.0026 ms +2020-04-04T14:24:38.1065641-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22580:c1366df7 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:24:38.1065662-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22580:e3e831b7 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:24:38.1114065-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22580:e3e831b7 all the dispatchers started +2020-04-04T14:24:38.1114793-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22580:c1366df7 all the dispatchers started +2020-04-04T14:24:39.2282210-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:24:39.2423550-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:24:39.2447909-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:28:26.3492596-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:28:26.3544781-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:28:26.5174135-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:28:26.5187379-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:28:26.5578223-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:20488:c6bc92b2 successfully announced in 182.3826 ms +2020-04-04T14:28:26.5588998-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:20488:20d17df5 successfully announced in 34.3576 ms +2020-04-04T14:28:26.5704666-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:20488:c6bc92b2 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:28:26.5716322-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:20488:20d17df5 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:28:26.7149113-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:20488:20d17df5 all the dispatchers started +2020-04-04T14:28:26.7226988-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:20488:c6bc92b2 all the dispatchers started +2020-04-04T14:28:27.5476942-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:28:27.5548312-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:28:27.5565374-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:29:22.4970867-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:29:22.5000868-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:29:22.5972226-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:29:22.5972498-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:29:22.6185406-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:8704:7f562bd0 successfully announced in 20.3044 ms +2020-04-04T14:29:22.6185503-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:8704:25c03ed6 successfully announced in 107.2263 ms +2020-04-04T14:29:22.6212883-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:8704:25c03ed6 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:29:22.6212868-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:8704:7f562bd0 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:29:22.6272480-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:8704:25c03ed6 all the dispatchers started +2020-04-04T14:29:22.6275399-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:8704:7f562bd0 all the dispatchers started +2020-04-04T14:29:23.8020576-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:29:23.8092332-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:29:23.8114626-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:33:29.3029557-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:33:29.3060421-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:33:29.3995563-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:33:29.3996017-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:33:29.4210320-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11468:78e298c5 successfully announced in 103.7016 ms +2020-04-04T14:33:29.4210325-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11468:79ee0690 successfully announced in 20.3431 ms +2020-04-04T14:33:29.4238373-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11468:78e298c5 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:33:29.4238390-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11468:79ee0690 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:33:29.4293346-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11468:78e298c5 all the dispatchers started +2020-04-04T14:33:29.4294031-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:11468:79ee0690 all the dispatchers started +2020-04-04T14:33:30.5171651-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:33:30.5207118-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:33:30.5262510-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:35:26.4931298-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:35:26.4960740-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:35:26.5902385-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:35:26.5902725-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:35:26.6112554-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9364:548bcc22 successfully announced in 20.0425 ms +2020-04-04T14:35:26.6112554-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9364:a39fa135 successfully announced in 104.5842 ms +2020-04-04T14:35:26.6138703-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9364:548bcc22 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:35:26.6138680-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9364:a39fa135 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:35:26.6186498-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9364:a39fa135 all the dispatchers started +2020-04-04T14:35:26.6187979-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9364:548bcc22 all the dispatchers started +2020-04-04T14:35:27.7550471-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:35:27.7577200-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:35:27.7656669-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:36:13.4963403-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:36:13.4995347-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:36:13.5937296-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:36:13.5937661-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:36:13.6136017-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:24484:d3f096b7 successfully announced in 102.8649 ms +2020-04-04T14:36:13.6136017-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:24484:0486235c successfully announced in 18.8774 ms +2020-04-04T14:36:13.6161824-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:24484:0486235c is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:36:13.6161810-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:24484:d3f096b7 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:36:13.6211768-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:24484:0486235c all the dispatchers started +2020-04-04T14:36:13.6212520-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:24484:d3f096b7 all the dispatchers started +2020-04-04T14:36:14.7116432-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:36:14.7193962-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:36:14.7217224-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:44:06.0389258-06:00 FAIL [Microsoft.AspNetCore.Server.IIS.Core.IISHttpServer] [ApplicationError] Connection ID "18302628887244309062", Request ID "80000247-0000-fe00-b63f-84710c7967bb": An unhandled exception was thrown by the application.System.InvalidOperationException: The SPA default page middleware could not return the default page '/index.html' because it was not found, and no other middleware handled the request. + + at Microsoft.AspNetCore.SpaServices.SpaDefaultPageMiddleware.<>c__DisplayClass0_0.b__1(HttpContext context, Func`1 next) + at Microsoft.AspNetCore.Builder.UseExtensions.<>c__DisplayClass0_1.b__1(HttpContext context) + at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.TryServeStaticFile(HttpContext context, String contentType, PathString subPath) + at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context) + at Microsoft.AspNetCore.Builder.UseExtensions.<>c__DisplayClass0_2.b__2() + at Microsoft.AspNetCore.SpaServices.SpaDefaultPageMiddleware.<>c__DisplayClass0_0.b__0(HttpContext context, Func`1 next) + at Microsoft.AspNetCore.Builder.UseExtensions.<>c__DisplayClass0_1.b__1(HttpContext context) + at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext) + at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) + at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync() + +2020-04-04T14:44:07.1916383-06:00 FAIL [Microsoft.AspNetCore.Server.IIS.Core.IISHttpServer] [ApplicationError] Connection ID "18302628930193982207", Request ID "80000300-000a-fe00-b63f-84710c7967bb": An unhandled exception was thrown by the application.System.InvalidOperationException: The SPA default page middleware could not return the default page '/index.html' because it was not found, and no other middleware handled the request. + + at Microsoft.AspNetCore.SpaServices.SpaDefaultPageMiddleware.<>c__DisplayClass0_0.b__1(HttpContext context, Func`1 next) + at Microsoft.AspNetCore.Builder.UseExtensions.<>c__DisplayClass0_1.b__1(HttpContext context) + at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.TryServeStaticFile(HttpContext context, String contentType, PathString subPath) + at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context) + at Microsoft.AspNetCore.Builder.UseExtensions.<>c__DisplayClass0_2.b__2() + at Microsoft.AspNetCore.SpaServices.SpaDefaultPageMiddleware.<>c__DisplayClass0_0.b__0(HttpContext context, Func`1 next) + at Microsoft.AspNetCore.Builder.UseExtensions.<>c__DisplayClass0_1.b__1(HttpContext context) + at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext) + at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) + at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync() + +2020-04-04T14:47:54.0323392-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:47:54.0367216-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:47:54.1985129-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:47:54.1996782-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:47:54.2766426-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9924:7a1f1a6b successfully announced in 70.8826 ms +2020-04-04T14:47:54.2806642-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9924:d0351e6f successfully announced in 214.1212 ms +2020-04-04T14:47:54.2889140-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9924:7a1f1a6b is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:47:54.2900438-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9924:d0351e6f is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:47:54.4139151-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9924:d0351e6f all the dispatchers started +2020-04-04T14:47:54.4368669-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:9924:7a1f1a6b all the dispatchers started +2020-04-04T14:47:55.2715624-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:47:55.2771286-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:47:55.2789075-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:48:49.5565338-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:48:49.5594208-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:48:49.6547594-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:48:49.6547886-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:48:49.6754648-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22936:b12db8cb successfully announced in 105.0262 ms +2020-04-04T14:48:49.6754648-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22936:fd149353 successfully announced in 19.6826 ms +2020-04-04T14:48:49.6783402-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22936:b12db8cb is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:48:49.6783431-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22936:fd149353 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:48:50.1962026-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22936:fd149353 all the dispatchers started +2020-04-04T14:48:50.1981452-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:22936:b12db8cb all the dispatchers started +2020-04-04T14:48:51.0147387-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:48:51.0220660-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:48:51.0239132-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web +2020-04-04T14:50:09.5762316-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:50:09.5792141-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:50:09.6739724-06:00 INFO [Hangfire.BackgroundJobServer] [0] Starting Hangfire Server using job storage: 'Hangfire.MemoryStorage.MemoryStorage' +2020-04-04T14:50:09.6740028-06:00 INFO [Hangfire.BackgroundJobServer] [0] Using the following options for Hangfire Server: + Worker count: 20 + Listening queues: 'default' + Shutdown timeout: 00:00:15 + Schedule polling interval: 00:00:15 +2020-04-04T14:50:09.6944222-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18196:9c827089 successfully announced in 19.5569 ms +2020-04-04T14:50:09.6944222-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18196:a1cafa69 successfully announced in 104.5052 ms +2020-04-04T14:50:09.6970626-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18196:9c827089 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:50:09.6970626-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18196:a1cafa69 is starting the registered dispatchers: ServerWatchdog, ServerJobCancellationWatcher, ExpirationManager, CountersAggregator, Worker, DelayedJobScheduler, RecurringJobScheduler... +2020-04-04T14:50:09.7022113-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18196:9c827089 all the dispatchers started +2020-04-04T14:50:09.7024526-06:00 INFO [Hangfire.Server.BackgroundServerProcess] [0] Server roger-pc:18196:a1cafa69 all the dispatchers started +2020-04-04T14:50:10.8721989-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Application started. Press Ctrl+C to shut down. +2020-04-04T14:50:10.8807198-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Hosting environment: Development +2020-04-04T14:50:10.8830457-06:00 INFO [Microsoft.Hosting.Lifetime] [0] Content root path: C:\Projects\rdt-client\server\RdtClient.Web diff --git a/server/RdtClient.Web/appsettings.json b/server/RdtClient.Web/appsettings.json index 0cd7023..1553cb2 100644 --- a/server/RdtClient.Web/appsettings.json +++ b/server/RdtClient.Web/appsettings.json @@ -3,11 +3,15 @@ "LogLevel": { "Default": "Information", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Information", + "Hangfire": "Information" + }, + "File": { + "Path": "app.log", + "Append": "True", + "FileSizeLimitBytes": 2048, + "MaxRollingFiles": 5 } }, - "AllowedHosts": "*", - "ConnectionStrings": { - "Client": "Host=localhost;Database=fieldcap;Username=fieldcap;Password=fieldcap;Maximum Pool Size=30" - } + "AllowedHosts": "*" } diff --git a/server/libs/RDNET.deps.json b/server/libs/RDNET.deps.json new file mode 100644 index 0000000..cae48d8 --- /dev/null +++ b/server/libs/RDNET.deps.json @@ -0,0 +1,63 @@ +{ + "runtimeTarget": { + "name": ".NETStandard,Version=v2.0/", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETStandard,Version=v2.0": {}, + ".NETStandard,Version=v2.0/": { + "RDNET/1.0.0": { + "dependencies": { + "NETStandard.Library": "2.0.3", + "Newtonsoft.Json": "12.0.3" + }, + "runtime": { + "RDNET.dll": {} + } + }, + "Microsoft.NETCore.Platforms/1.1.0": {}, + "NETStandard.Library/2.0.3": { + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Newtonsoft.Json/12.0.3": { + "runtime": { + "lib/netstandard2.0/Newtonsoft.Json.dll": { + "assemblyVersion": "12.0.0.0", + "fileVersion": "12.0.3.23909" + } + } + } + } + }, + "libraries": { + "RDNET/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.NETCore.Platforms/1.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==", + "path": "microsoft.netcore.platforms/1.1.0", + "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512" + }, + "NETStandard.Library/2.0.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "path": "netstandard.library/2.0.3", + "hashPath": "netstandard.library.2.0.3.nupkg.sha512" + }, + "Newtonsoft.Json/12.0.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-6mgjfnRB4jKMlzHSl+VD+oUc1IebOZabkbyWj2RiTgWwYPPuaK1H97G1sHqGwPlS5npiF5Q0OrxN1wni2n5QWg==", + "path": "newtonsoft.json/12.0.3", + "hashPath": "newtonsoft.json.12.0.3.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/server/libs/RDNET.dll b/server/libs/RDNET.dll new file mode 100644 index 0000000000000000000000000000000000000000..9fb3f783262227af399b1e100e25b4963c3586bc GIT binary patch literal 66560 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 Z372*bxjFgtz?_mf3+`UOybHaL`F|?}coYBt literal 0 HcmV?d00001