Fixed status columns, separate downloads and files in the detail list.

Add UnpackLimit setting.
This commit is contained in:
Roger Far 2021-01-12 20:42:06 -07:00
parent 21ad2ebfd6
commit 67532645aa
26 changed files with 253 additions and 236 deletions

View file

@ -1,24 +1,24 @@
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgxFilesizeModule, FileSizePipe } from 'ngx-filesize';
import { BrowserModule } from '@angular/platform-browser';
import { curray } from 'curray';
import { FileSizePipe, NgxFilesizeModule } 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';
import { AuthInterceptor } from './auth.interceptor';
import { curray } from 'curray';
import { DownloadStatusPipe } from './download-status.pipe';
import { LoginComponent } from './login/login.component';
import { MainLayoutComponent } from './main-layout/main-layout.component';
import { AddNewTorrentComponent } from './navbar/add-new-torrent/add-new-torrent.component';
import { NavbarComponent } from './navbar/navbar.component';
import { SettingsComponent } from './navbar/settings/settings.component';
import { SetupComponent } from './setup/setup.component';
import { TorrentDownloadComponent } from './torrent-download/torrent-download.component';
import { TorrentFileComponent } from './torrent-file/torrent-file.component';
import { TorrentRowComponent } from './torrent-row/torrent-row.component';
import { TorrentStatusPipe } from './torrent-status.pipe';
import { TorrentTableComponent } from './torrent-table/torrent-table.component';
curray();
@ -33,21 +33,13 @@ curray();
TorrentFileComponent,
SettingsComponent,
TorrentStatusPipe,
FileStatusPipe,
DownloadStatusPipe,
LoginComponent,
SetupComponent,
TorrentDownloadComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule,
NgxFilesizeModule,
],
providers: [
FileSizePipe,
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
imports: [BrowserModule, AppRoutingModule, FormsModule, HttpClientModule, NgxFilesizeModule],
providers: [FileSizePipe, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }],
bootstrap: [AppComponent],
})
export class AppModule {}

View file

@ -0,0 +1,54 @@
import { Pipe, PipeTransform } from '@angular/core';
import { FileSizePipe } from 'ngx-filesize';
import { Download } from './models/download.model';
@Pipe({
name: 'downloadStatus',
})
export class DownloadStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {}
transform(value: Download): string {
if (!value) {
return 'Pending';
}
if (value.error) {
return `Error: ${value.error}`;
}
if (value.completed != null) {
return 'Finished';
}
if (value.unpackingFinished) {
return 'Unpacking finished';
}
if (value.unpackingStarted) {
const progress = ((value.bytesDone / value.bytesTotal) * 100).toFixed(2);
return `Unpacking ${progress || 0}%`;
}
if (value.unpackingQueued) {
return 'Unpacking queued';
}
if (value.downloadFinished) {
return 'Download finished';
}
if (value.downloadStarted) {
const progress = ((value.bytesDone / value.bytesTotal) * 100).toFixed(2);
const speed = this.pipe.transform(value.speed, 'filesize');
return `Downloading ${progress || 0}% (${speed}/s)`;
}
if (value.downloadQueued) {
return 'Download queued';
}
return 'Pending';
}
}

View file

@ -1,58 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { FileSizePipe } from 'ngx-filesize';
import { TorrentFile } from './models/torrent.model';
@Pipe({
name: 'fileStatus',
})
export class FileStatusPipe implements PipeTransform {
constructor(private pipe: FileSizePipe) {}
transform(value: TorrentFile): string {
if (!value.download) {
return 'Pending download';
}
if (value.download.error) {
return `Error: ${value.download.error}`;
}
if (value.download.completed != null) {
return 'Finished';
}
if (value.download.unpackingFinished) {
return 'Unpacking finished';
}
if (value.download.unpackingStarted) {
const progress = ((value.download.bytesDone / value.download.bytesTotal) * 100).toFixed(2);
return `Unpacking ${progress || 0}%`;
}
if (value.download.unpackingQueued) {
return 'Unpacking queued';
}
if (value.download.downloadFinished) {
return 'Download finished';
}
if (value.download.downloadStarted) {
const progress = ((value.download.bytesDone / value.download.bytesTotal) * 100).toFixed(2);
const speed = this.pipe.transform(value.download.speed, 'filesize');
return `Downloading ${progress || 0}% (${speed}/s)`;
}
if (value.download.downloadQueued) {
return 'Download queued';
}
if (value.download.added) {
return 'Pending';
}
return '';
}
}

View file

@ -56,8 +56,8 @@
Remove torrent when finished downloading on host
</label>
</div>
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
Cannot add torrent: {{ error }}
<div class="notification is-danger is-light" *ngIf="error">
Cannot add torrent: {{ error | json }}
</div>
</section>
<footer class="modal-card-foot">

View file

@ -46,7 +46,8 @@
<span class="icon">
<i class="fas fa-euro-sign" aria-hidden="true"></i>
</span>
<span>Premium Status: {{ profile.expiration | date }}</span>
<span *ngIf="profile.expiration">Premium Status: {{ profile.expiration | date }}</span>
<span *ngIf="!profile.expiration" class="no-premium">Not premium</span>
</a>
</div>

View file

@ -0,0 +1,3 @@
.no-premium {
color: red;
}

View file

@ -9,59 +9,38 @@
<div class="field">
<label class="label">Real-Debrid API Key</label>
<div class="control">
<input
class="input"
type="text"
maxlength="100"
[(ngModel)]="settingRealDebridApiKey"
/>
<input class="input" type="text" maxlength="100" [(ngModel)]="settingRealDebridApiKey" />
</div>
<p class="help">
You can find your API key here:
<a
href="https://real-debrid.com/apitoken"
target="_blank"
rel="noopener"
>https://real-debrid.com/apitoken</a
<a href="https://real-debrid.com/apitoken" target="_blank" rel="noopener">https://real-debrid.com/apitoken</a
>.
</p>
</div>
<div class="field">
<label class="label">Download folder</label>
<div class="control">
<input
class="input"
type="text"
maxlength="1000"
[(ngModel)]="settingDownloadFolder"
/>
<input class="input" type="text" maxlength="1000" [(ngModel)]="settingDownloadFolder" />
</div>
<p class="help">Path on the host where RealDebrid Client is hosted.</p>
</div>
<div class="field">
<label class="label">Maximum parallel downloads</label>
<div class="control">
<input
class="input"
type="number"
max="100"
min="1"
[(ngModel)]="settingDownloadLimit"
/>
<input class="input" type="number" max="100" min="1" [(ngModel)]="settingDownloadLimit" />
</div>
<p class="help">
Maximum amount of torrents that get downloaded to your host at the
same time.
</p>
<p class="help">Maximum amount of torrents that get downloaded to your host at the same time.</p>
</div>
<div class="notification is-danger is-light" *ngIf="error?.length > 0">
Error saving settings: {{ error }}
<div class="field">
<label class="label">Maximum unpack processes</label>
<div class="control">
<input class="input" type="number" max="100" min="1" [(ngModel)]="settingUnpackLimit" />
</div>
<p class="help">Maximum amount of downloads that get unpacked on your host at the same time.</p>
</div>
<div class="notification is-danger is-light" *ngIf="error?.length > 0">Error saving settings: {{ error }}</div>
<div
class="notification is-danger is-light"
*ngIf="testFolderError?.length > 0"
>
<div class="notification is-danger is-light" *ngIf="testFolderError?.length > 0">
Could not test your download folder<br />
{{ testFolderError }}
</div>
@ -71,30 +50,13 @@
</div>
</section>
<footer class="modal-card-foot">
<button
class="button is-success"
(click)="ok()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
>
<button class="button is-success" (click)="ok()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">
Save Settings
</button>
<button
class="button is-warning"
(click)="test()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
>
<button class="button is-warning" (click)="test()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">
Test permissions
</button>
<button
class="button"
(click)="cancel()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
>
Cancel
</button>
<button class="button" (click)="cancel()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">Cancel</button>
</footer>
</div>
</div>

View file

@ -31,6 +31,7 @@ export class SettingsComponent implements OnInit {
public settingRealDebridApiKey: string;
public settingDownloadFolder: string;
public settingDownloadLimit: number;
public settingUnpackLimit: number;
constructor(private settingsService: SettingsService) {}
@ -45,6 +46,7 @@ export class SettingsComponent implements OnInit {
this.settingRealDebridApiKey = this.getSetting(results, 'RealDebridApiKey');
this.settingDownloadFolder = this.getSetting(results, 'DownloadFolder');
this.settingDownloadLimit = parseInt(this.getSetting(results, 'DownloadLimit'), 10);
this.settingUnpackLimit = parseInt(this.getSetting(results, 'UnpackLimit'), 10);
},
(err) => {
this.error = err.error;
@ -69,6 +71,10 @@ export class SettingsComponent implements OnInit {
settingId: 'DownloadLimit',
value: (this.settingDownloadLimit ?? 10).toString(),
},
{
settingId: 'UnpackLimit',
value: (this.settingUnpackLimit ?? 1).toString(),
},
];
this.settingsService.update(settings).subscribe(

View file

@ -0,0 +1,11 @@
<td colspan="4">
<i class="fas fa-download" aria-hidden="true"></i>
{{ download.link }}
</td>
<td>
{{ download.bytesTotal | filesize }}
</td>
<td>
{{ download | downloadStatus }}
</td>
<td></td>

View file

@ -0,0 +1,3 @@
td {
font-size: smaller;
}

View file

@ -0,0 +1,18 @@
import { Component, Input, OnInit } from '@angular/core';
import { Download } from '../models/download.model';
@Component({
selector: '[app-torrent-download]',
templateUrl: './torrent-download.component.html',
styleUrls: ['./torrent-download.component.scss'],
})
export class TorrentDownloadComponent implements OnInit {
@Input()
public download: Download;
constructor() {}
ngOnInit(): void {
}
}

View file

@ -1,12 +1,6 @@
<td>
<td colspan="4">
<i class="fas fa-file" aria-hidden="true"></i>
{{ file.path }}
</td>
<td>{{ file.bytes | filesize }}</td>
<td></td>
<td></td>
<td>
{{ file | fileStatus }}
</td>
<td></td>
<td></td>
<td colspan="10"></td>

View file

@ -1,6 +1,5 @@
import { Component, OnInit, Input } from '@angular/core';
import { Torrent, TorrentFile } from '../models/torrent.model';
import { TorrentService } from '../torrent.service';
import { Component, Input, OnInit } from '@angular/core';
import { TorrentFile } from '../models/torrent.model';
@Component({
selector: '[app-torrent-file]',
@ -11,7 +10,7 @@ export class TorrentFileComponent implements OnInit {
@Input()
public file: TorrentFile;
constructor(private torrentService: TorrentService) {}
constructor() {}
ngOnInit(): void {}
}

View file

@ -1,7 +1,4 @@
<td>{{ torrent.rdName }}</td>
<td>
{{ torrent.rdSize | filesize }}
</td>
<td>
{{ torrent.files.length | number }}
</td>
@ -13,6 +10,9 @@
<i class="fas fa-box-open" *ngIf="torrent.autoUnpack" title="Auto unpack"></i>
<i class="fas fa-trash" *ngIf="torrent.autoDelete" title="Auto delete"></i>
</td>
<td>
{{ torrent.rdSize | filesize }}
</td>
<td>
{{ torrent | status }}
</td>

View file

@ -13,59 +13,69 @@ export class TorrentStatusPipe implements PipeTransform {
return 'Finished';
}
if (torrent.downloads && torrent.downloads.length > 0) {
const allFinished = torrent.downloads.all((m) => m.completed != null);
if (allFinished) {
return 'Finished';
const allFinished = torrent.downloads.all((m) => m.completed != null);
if (allFinished) {
return 'Finished';
}
const errors = torrent.downloads.where((m) => m.error != null);
if (errors.length > 0) {
return 'Error';
}
const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished);
if (downloading.length > 0) {
const bytesDone = downloading.sum((m) => m.bytesDone);
const bytesTotal = downloading.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100;
let allSpeeds = downloading.sum((m) => m.speed) / downloading.length;
let speed: string | string[] = '0';
if (allSpeeds > 0) {
speed = this.pipe.transform(allSpeeds, 'filesize');
return `Downloading (${progress.toFixed(2)}% - ${speed}/s)`;
}
}
const errors = torrent.downloads.where((m) => m.error != null);
const downloading = torrent.downloads.where((m) => m.downloadStarted && !m.downloadFinished);
const downloaded = torrent.downloads.where((m) => m.downloadFinished != null);
const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished);
const queuedForDownload = torrent.downloads.where((m) => m.downloadQueued && !m.downloadStarted);
const queuedForUnpacking = torrent.downloads.where((m) => m.unpackingQueued && !m.unpackingStarted);
const unpacking = torrent.downloads.where((m) => m.unpackingStarted && !m.unpackingFinished);
if (errors.length > 0) {
return 'Error';
if (unpacking.length > 0) {
const bytesDone = unpacking.sum((m) => m.bytesDone);
const bytesTotal = unpacking.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100;
let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length;
if (allSpeeds > 0) {
return `Extracting (${progress.toFixed(2)}%)`;
}
}
if (downloading.length > 0) {
const bytesDone = downloading.sum((m) => m.bytesDone);
const bytesTotal = downloading.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100;
let allSpeeds = downloading.sum((m) => m.speed) / downloading.length;
const queuedForUnpacking = torrent.downloads.where((m) => m.unpackingQueued && !m.unpackingStarted);
let speed: string | string[] = '0';
if (allSpeeds > 0) {
speed = this.pipe.transform(allSpeeds, 'filesize');
if (queuedForUnpacking.length > 0) {
return `Queued for unpacking`;
}
return `Downloading (${progress.toFixed(2)}% - ${speed}/s)`;
}
}
const queuedForDownload = torrent.downloads.where((m) => m.downloadQueued && !m.downloadStarted);
if (unpacking.length > 0) {
const bytesDone = unpacking.sum((m) => m.bytesDone);
const bytesTotal = unpacking.sum((m) => m.bytesTotal);
let progress = (bytesDone / bytesTotal) * 100;
let allSpeeds = unpacking.sum((m) => m.speed) / unpacking.length;
if (queuedForDownload.length > 0) {
return `Queued for downloading`;
}
if (allSpeeds > 0) {
return `Extracting (${progress.toFixed(2)}%)`;
}
}
const unpacked = torrent.downloads.where((m) => m.unpackingFinished != null);
if (queuedForUnpacking.length > 0) {
return `Queued for unpacking`;
}
if (unpacked.length > 0) {
return `Files unpacked`;
}
if (queuedForDownload.length > 0) {
return `Queued for downloading`;
}
const downloaded = torrent.downloads.where((m) => m.downloadFinished != null);
if (downloaded.length > 0) {
return `Files downloaded to host`;
}
if (downloaded.length > 0) {
return `Files downloaded to host`;
}
switch (torrent.rdStatus) {

View file

@ -5,8 +5,8 @@
<div class="table-container">
<table class="table is-fullwidth is-hoverable">
<colgroup>
<col style="width: calc(40% - 550px)" />
<col style="width: 15%" />
<col style="width: calc(60% - 650px)" />
<col style="width: 100px" />
<col style="width: 150px" />
<col style="width: 150px" />
<col style="width: 150px" />
@ -16,10 +16,10 @@
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>Files</th>
<th>Downloads</th>
<th>Auto</th>
<th>Size</th>
<th>Status</th>
<th>Action</th>
</tr>
@ -33,6 +33,13 @@
(delete)="showDeleteModal($event)"
></tr>
<ng-container *ngIf="showFiles[torrent.torrentId]">
<tr class="separator">
<td colspan="10">Downloads</td>
</tr>
<tr app-torrent-download [download]="download" *ngFor="let download of torrent.downloads"></tr>
<tr class="separator">
<td colspan="10">Files in torrent</td>
</tr>
<tr app-torrent-file [file]="file" *ngFor="let file of torrent.files"></tr>
</ng-container>
</ng-container>

View file

@ -2,4 +2,11 @@ table {
tr {
cursor: pointer;
}
tr.separator {
td {
font-size: smaller;
font-weight: bold;
}
}
}

View file

@ -45,21 +45,6 @@ export class TorrentTableComponent implements OnInit, OnDestroy {
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 {

View file

@ -31,10 +31,6 @@ export class TorrentService {
return this.http.get<Torrent[]>(`/Api/Torrents`);
}
public getDetails(torrentId: string): Observable<Torrent> {
return this.http.get<Torrent>(`/Api/Torrents/${torrentId}`);
}
public uploadMagnet(
magnetLink: string,
autoDownload: boolean,

View file

@ -61,6 +61,12 @@ namespace RdtClient.Data.Data
SettingId = "DownloadLimit",
Type = "Int32",
Value = "10"
},
new Setting
{
SettingId = "UnpackLimit",
Type = "Int32",
Value = "1"
}
};

View file

@ -18,6 +18,7 @@ namespace RdtClient.Data.Data
Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateError(Guid downloadId, String error);
Task DeleteForTorrent(Guid torrentId);
}
@ -122,13 +123,22 @@ namespace RdtClient.Data.Data
await _dataContext.SaveChangesAsync();
}
public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.Completed = dateTime;
await _dataContext.SaveChangesAsync();
}
public async Task UpdateError(Guid downloadId, String error)
{
var dbDownload = await _dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId);
dbDownload.Completed = DateTimeOffset.UtcNow;
dbDownload.Error = error;
await _dataContext.SaveChangesAsync();

View file

@ -5,6 +5,6 @@ namespace RdtClient.Service.Models
public class Profile
{
public String UserName { get; set; }
public DateTimeOffset Expiration { get; set; }
public DateTimeOffset? Expiration { get; set; }
}
}

View file

@ -17,6 +17,7 @@ namespace RdtClient.Service.Services
Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime);
Task UpdateError(Guid downloadId, String error);
Task DeleteForTorrent(Guid torrentId);
}
@ -75,6 +76,11 @@ namespace RdtClient.Service.Services
await _downloadData.UpdateUnpackingFinished(downloadId, dateTime);
}
public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime)
{
await _downloadData.UpdateCompleted(downloadId, dateTime);
}
public async Task UpdateError(Guid downloadId, String error)
{
await _downloadData.UpdateError(downloadId, error);

View file

@ -36,7 +36,9 @@ namespace RdtClient.Service.Services
{
// When starting up reset any pending downloads or unpackings so that they are restarted.
var torrents = await _torrents.Get();
torrents = torrents.Where(m => m.Completed == null).ToList();
var downloads = torrents.SelectMany(m => m.Downloads)
.Where(m => m.DownloadQueued != null && m.DownloadStarted != null && m.DownloadFinished == null)
.OrderBy(m => m.DownloadQueued);
@ -75,6 +77,7 @@ namespace RdtClient.Service.Services
if (downloadClient.Error != null)
{
await _downloads.UpdateError(downloadId, downloadClient.Error);
await _downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow);
}
else
{
@ -95,10 +98,12 @@ namespace RdtClient.Service.Services
if (unpackClient.Error != null)
{
await _downloads.UpdateError(downloadId, unpackClient.Error);
await _downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow);
}
else
{
await _downloads.UpdateUnpackingFinished(downloadId, DateTimeOffset.UtcNow);
await _downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow);
}
ActiveUnpackClients.TryRemove(downloadId, out _);

View file

@ -234,8 +234,7 @@ namespace RdtClient.Service.Services
public async Task Unpack(Guid downloadId)
{
//var settingUnpackLimit = await _settings.GetNumber("UnpackLimit");
var settingUnpackLimit = 1;
var settingUnpackLimit = await _settings.GetNumber("UnpackLimit");
var settingDownloadFolder = await _settings.GetString("DownloadFolder");
var download = await _downloads.GetById(downloadId);

View file

@ -18,11 +18,13 @@ namespace RdtClient.Web.Controllers
{
private readonly ITorrents _torrents;
private readonly IDownloads _downloads;
private readonly ITorrentRunner _torrentRunner;
public TorrentsController(ITorrents torrents, IDownloads downloads)
public TorrentsController(ITorrents torrents, IDownloads downloads, ITorrentRunner torrentRunner)
{
_torrents = torrents;
_downloads = downloads;
_torrentRunner = torrentRunner;
}
[HttpGet]
@ -40,19 +42,18 @@ namespace RdtClient.Web.Controllers
return Ok(results);
}
/// <summary>
/// Used for debugging only. Force a tick.
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("{id}")]
public async Task<ActionResult<Torrent>> Get(Guid id)
[Route("Tick")]
public async Task<ActionResult> Tick()
{
var result = await _torrents.GetById(id);
await _torrentRunner.Tick();
if (result == null)
{
throw new Exception("Torrent not found");
}
return Ok(result);
}
return Ok();
}
[HttpPost]
[Route("UploadFile")]