Use a new Downloader from https://github.com/bezzad/Downloader, add a write and download speed test to the settings interface.

This commit is contained in:
Roger Far 2021-01-20 10:44:59 -07:00
parent 49071a7f48
commit 19e5e5eb10
12 changed files with 301 additions and 185 deletions

View file

@ -64,22 +64,84 @@
</div>
</div>
<div class="notification is-danger is-light" *ngIf="error?.length > 0">Error saving settings: {{ error }}</div>
<div class="field">
<label class="label">Test download path permissions</label>
<div class="control">
<button
class="button is-warning"
(click)="testDownloadPath()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testPathError && !testPathSuccess"
>
Test permissions
</button>
<div class="notification is-danger is-light" *ngIf="testPathError">
Could not test your download path<br />
{{ testPathError }}
</div>
<div class="notification is-danger is-light" *ngIf="testPathError?.length > 0">
Could not test your download path<br />
{{ testPathError }}
<div class="notification is-success is-light" *ngIf="testPathSuccess">Your download path looks good!</div>
</div>
<div class="help">This will check if the download folder has write permissions.</div>
</div>
<div class="notification is-success is-light" *ngIf="testPathSuccess">Successfully tested your download path</div>
<div class="field">
<label class="label">Test RealDebrid download speed</label>
<div class="control">
<button
class="button is-warning"
(click)="testDownloadSpeed()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testDownloadSpeedError && !testDownloadSpeedSuccess"
>
Test download speed
</button>
<div class="notification is-danger is-light" *ngIf="testDownloadSpeedError">
Could not test your download speed<br />
{{ testDownloadSpeedError }}
</div>
<div class="notification is-success is-light" *ngIf="testDownloadSpeedSuccess">
Download speed {{ testDownloadSpeedSuccess | filesize }}/s
</div>
</div>
<div class="help">
This will download a 10GB test file from RealDebrid. Hit cancel when you have seen enough.
</div>
</div>
<div class="field">
<label class="label">Test download folder write speed</label>
<div class="control">
<button
class="button is-warning"
(click)="testWriteSpeed()"
[disabled]="saving"
[ngClass]="{ 'is-loading': saving }"
*ngIf="!testWriteSpeedError && !testWriteSpeedSuccess"
>
Test write speed
</button>
<div class="notification is-danger is-light" *ngIf="testWriteSpeedError">
Could not test your download speed<br />
{{ testWriteSpeedError }}
</div>
<div class="notification is-success is-light" *ngIf="testWriteSpeedSuccess">
Write speed {{ testWriteSpeedSuccess | filesize }}/s
</div>
</div>
<div class="help">This will write a small file to your download folder to see how fast it can write to it.</div>
</div>
<div class="notification is-danger is-light" *ngIf="error?.length > 0">Error saving settings: {{ error }}</div>
</section>
<footer class="modal-card-foot">
<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 }">
Test permissions
</button>
<button class="button" (click)="cancel()" [disabled]="saving" [ngClass]="{ 'is-loading': saving }">Cancel</button>
</footer>
</div>

View file

@ -25,9 +25,16 @@ export class SettingsComponent implements OnInit {
public saving = false;
public error: string;
public testPathError: string;
public testPathSuccess: boolean;
public testDownloadSpeedError: string;
public testDownloadSpeedSuccess: number;
public testWriteSpeedError: string;
public testWriteSpeedSuccess: number;
public settingRealDebridApiKey: string;
public settingDownloadPath: string;
public settingMappedPath: string;
@ -100,7 +107,7 @@ export class SettingsComponent implements OnInit {
);
}
public test(): void {
public testDownloadPath(): void {
this.saving = true;
this.testPathError = null;
this.testPathSuccess = false;
@ -111,13 +118,45 @@ export class SettingsComponent implements OnInit {
this.testPathSuccess = true;
},
(err) => {
console.log(err);
this.testPathError = err.error;
this.saving = false;
}
);
}
public testDownloadSpeed(): void {
this.saving = true;
this.testDownloadSpeedError = null;
this.testDownloadSpeedSuccess = 0;
this.settingsService.testDownloadSpeed().subscribe(
(result) => {
this.saving = false;
this.testDownloadSpeedSuccess = result;
},
(err) => {
this.testDownloadSpeedError = err.error;
this.saving = false;
}
);
}
public testWriteSpeed(): void {
this.saving = true;
this.testWriteSpeedError = null;
this.testWriteSpeedSuccess = 0;
this.settingsService.testWriteSpeed().subscribe(
(result) => {
this.saving = false;
this.testWriteSpeedSuccess = result;
},
(err) => {
this.testWriteSpeedError = err.error;
this.saving = false;
}
);
}
public cancel(): void {
this.isActive = false;
this.openChange.emit(this.open);

View file

@ -25,4 +25,12 @@ export class SettingsService {
public testPath(path: string): Observable<void> {
return this.http.post<void>(`/Api/Settings/TestPath`, { path });
}
public testDownloadSpeed(): Observable<number> {
return this.http.get<number>(`/Api/Settings/TestDownloadSpeed`);
}
public testWriteSpeed(): Observable<number> {
return this.http.get<number>(`/Api/Settings/TestWriteSpeed`);
}
}

View file

@ -5,11 +5,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.1" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.1">
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.2" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -12,10 +12,11 @@
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Exceptions" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="SharpCompress" Version="0.27.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Downloader\Downloader.csproj" />
<ProjectReference Include="..\RdtClient.Data\RdtClient.Data.csproj" />
</ItemGroup>

View file

@ -1,30 +1,22 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Downloader;
using RdtClient.Data.Models.Data;
namespace RdtClient.Service.Services
{
public class DownloadClient
{
public Boolean Finished { get; private set; }
public String Error { get; private set; }
public Int64 Speed { get; private set; }
public Int64 BytesTotal { get; private set; }
public Int64 BytesDone { get; private set; }
private readonly String _destinationPath;
private readonly Download _download;
private readonly String _destinationPath;
private readonly Torrent _torrent;
private Boolean _cancelled = false;
private Int64 _bytesLastUpdate;
private DateTime _nextUpdate;
private DownloadService _downloader;
public DownloadClient(Download download, String destinationPath)
{
@ -33,7 +25,18 @@ namespace RdtClient.Service.Services
_torrent = download.Torrent;
}
public async Task Start()
public Boolean Finished { get; private set; }
public String Error { get; private set; }
public Int64 Speed { get; private set; }
public Int64 BytesTotal { get; private set; }
public Int64 BytesDone { get; private set; }
private static Int64 LastTick { get; set; }
private static ConcurrentBag<Int64> AverageSpeed { get; } = new ConcurrentBag<Int64>();
public async Task Start(Boolean writeToMemory)
{
BytesDone = 0;
BytesTotal = 0;
@ -58,7 +61,7 @@ namespace RdtClient.Service.Services
var fileName = uri.Segments.Last();
var filePath = Path.Combine(torrentPath, fileName);
if (File.Exists(filePath))
{
File.Delete(filePath);
@ -66,10 +69,7 @@ namespace RdtClient.Service.Services
await Task.Factory.StartNew(async delegate
{
if (!_cancelled)
{
await Download(uri, filePath);
}
await Download(filePath, writeToMemory);
});
}
catch (Exception ex)
@ -81,98 +81,80 @@ namespace RdtClient.Service.Services
public void Cancel()
{
_cancelled = true;
_downloader?.CancelAsync();
}
private async Task Download(Uri uri, String filePath)
private async Task Download(String filePath, Boolean writeToMemory)
{
try
{
_bytesLastUpdate = 0;
_nextUpdate = DateTime.UtcNow.AddSeconds(1);
LastTick = 0;
// Determine the file size
var webRequest = WebRequest.Create(uri);
webRequest.Method = "HEAD";
webRequest.Timeout = 5000;
Int64 responseLength;
using (var webResponse = await webRequest.GetResponseAsync())
var downloadOpt = new DownloadConfiguration
{
responseLength = Int64.Parse(webResponse.Headers.Get("Content-Length"));
}
MaxTryAgainOnFailover = Int32.MaxValue,
ParallelDownload = true,
ChunkCount = 8,
Timeout = 100,
OnTheFlyDownload = writeToMemory,
BufferBlockSize = 1024 * 8,
MaximumBytesPerSecond = 100 * 1024 * 1024,
TempDirectory = @"C:\temp",
RequestConfiguration =
{
Accept = "*/*",
UserAgent = $"rdt-client",
ProtocolVersion = HttpVersion.Version11,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
KeepAlive = true,
UseDefaultCredentials = false
}
};
var timeout = DateTimeOffset.UtcNow.AddHours(1);
_downloader = new DownloadService(downloadOpt);
while (timeout > DateTimeOffset.UtcNow && !_cancelled)
_downloader.DownloadProgressChanged += (_, args) =>
{
try
var isElapsedTimeMoreThanOneSecond = Environment.TickCount - LastTick >= 1000;
if (isElapsedTimeMoreThanOneSecond)
{
var request = WebRequest.Create(uri);
using var response = await request.GetResponseAsync();
await using var stream = response.GetResponseStream();
if (stream == null)
{
throw new IOException("No stream");
}
await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write);
var buffer = new Byte[64 * 1024];
while (fileStream.Length < response.ContentLength && !_cancelled)
{
var read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length));
if (read > 0)
{
fileStream.Write(buffer, 0, read);
BytesDone = fileStream.Length;
BytesTotal = responseLength;
if (DateTime.UtcNow > _nextUpdate)
{
Speed = fileStream.Length - _bytesLastUpdate;
_nextUpdate = DateTime.UtcNow.AddSeconds(1);
_bytesLastUpdate = fileStream.Length;
timeout = DateTimeOffset.UtcNow.AddHours(1);
}
}
else
{
break;
}
}
break;
AverageSpeed.Add(args.BytesPerSecondSpeed);
LastTick = Environment.TickCount;
}
catch (IOException)
{
await Task.Delay(1000);
}
catch (WebException)
{
await Task.Delay(1000);
}
}
if (timeout <= DateTimeOffset.UtcNow)
Speed = (Int64) AverageSpeed.Average();
BytesDone = args.BytesReceived;
BytesTotal = args.TotalBytesToReceive;
};
_downloader.DownloadStarted += (_, args) =>
{
throw new Exception($"Download timed out");
}
AverageSpeed?.Clear();
Speed = 0;
BytesDone = 0;
BytesTotal = args.TotalBytesToReceive;
};
Speed = 0;
_downloader.DownloadFileCompleted += (_, args) =>
{
if (args.Cancelled)
{
Error = $"The download was cancelled";
}
else if (args.Error != null)
{
Error = args.Error.Message;
}
Finished = true;
};
await _downloader.DownloadFileAsync(_download.Link, filePath);
}
catch (Exception ex)
{
Error = $"An unexpected error occurred downloading {_download.Link} for torrent {_torrent.RdName}: {ex.Message}";
}
finally
{
Finished = true;
}
}

View file

@ -1,8 +1,9 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using RdtClient.Data.Data;
using RdtClient.Data.Models.Data;
@ -16,8 +17,8 @@ namespace RdtClient.Service.Services
Task<String> GetString(String key);
Task<Int32> GetNumber(String key);
Task TestPath(String path);
Task<Double> TestDownloadSpeed();
Task<Double> TestWriteSpeed(String path);
Task<Double> TestDownloadSpeed(CancellationToken cancellationToken);
Task<Double> TestWriteSpeed();
}
public class Settings : ISettings
@ -29,6 +30,9 @@ namespace RdtClient.Service.Services
_settingData = settingData;
}
private static Int64 LastTick { get; set; }
private static ConcurrentBag<Int64> AverageSpeed { get; } = new ConcurrentBag<Int64>();
public async Task<IList<Setting>> GetAll()
{
return await _settingData.GetAll();
@ -83,78 +87,93 @@ namespace RdtClient.Service.Services
File.Delete(testFile);
}
public async Task<Double> TestDownloadSpeed()
public async Task<Double> TestDownloadSpeed(CancellationToken cancellationToken)
{
var watch = new Stopwatch();
var downloadPath = await GetString("DownloadPath");
var request = WebRequest.Create(new Uri("https://34.download.real-debrid.com/speedtest/testDefault.rar/" + DateTime.Now.Ticks));
watch.Start();
using var response = await request.GetResponseAsync();
await using var stream = response.GetResponseStream();
if (stream == null)
{
throw new IOException("No stream");
}
var buffer = new Byte[64 * 1024];
Int64 totalRead = 0;
while (totalRead < response.ContentLength)
{
var read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length));
if (read > 0)
{
totalRead += read;
}
else
{
break;
}
}
watch.Stop();
var downloadSpeed = totalRead / watch.Elapsed.TotalSeconds;
return downloadSpeed;
}
public async Task<Double> TestWriteSpeed(String path)
{
var testFilePath = Path.Combine(path, "test.tmp");
const Int32 testFileSize = 1024 * 1024 * 1024;
var watch = new Stopwatch();
var testFilePath = Path.Combine(downloadPath, "testDefault.rar");
if (File.Exists(testFilePath))
{
File.Delete(testFilePath);
}
var download = new Download
{
Link = "https://34.download.real-debrid.com/speedtest/testDefault.rar",
Torrent = new Torrent
{
RdName = ""
}
};
var downloadClient = new DownloadClient(download, downloadPath);
await downloadClient.Start(true);
while (!downloadClient.Finished)
{
await Task.Delay(1000);
if (cancellationToken.IsCancellationRequested)
{
downloadClient.Cancel();
}
if (downloadClient.BytesDone > 1024 * 1024 * 100)
{
downloadClient.Cancel();
}
}
if (File.Exists(testFilePath))
{
File.Delete(testFilePath);
}
return downloadClient.Speed;
}
public async Task<Double> TestWriteSpeed()
{
var downloadPath = await GetString("DownloadPath");
var testFilePath = Path.Combine(downloadPath, "test.tmp");
if (File.Exists(testFilePath))
{
File.Delete(testFilePath);
}
const Int32 testFileSize = 64 * 1024 * 1024;
var watch = new Stopwatch();
watch.Start();
var rnd = new Random();
await using var fileStream = new FileStream(testFilePath, FileMode.Create, FileAccess.Write, FileShare.Write);
var buffer = new Byte[64 * 1024];
while (fileStream.Length < testFileSize)
{
rnd.NextBytes(buffer);
fileStream.Write(buffer, 0, buffer.Length);
}
watch.Stop();
var writeSpeed = fileStream.Length / watch.Elapsed.TotalSeconds;
fileStream.Close();
if (File.Exists(testFilePath))
{
File.Delete(testFilePath);
}
return writeSpeed;
}

View file

@ -172,7 +172,7 @@ namespace RdtClient.Service.Services
if (TorrentRunner.ActiveDownloadClients.TryAdd(download.DownloadId, downloadClient))
{
await downloadClient.Start();
await downloadClient.Start(false);
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -58,24 +59,21 @@ namespace RdtClient.Web.Controllers
}
[HttpGet]
[Route("TestSpeed")]
public async Task<ActionResult> TestSpeed()
[Route("TestDownloadSpeed")]
public async Task<ActionResult> TestDownloadSpeed(CancellationToken cancellationToken)
{
var downloadPath = await _settings.GetString("DownloadPath");
var mappedPath = await _settings.GetString("MappedPath");
var downloadSpeed = await _settings.TestDownloadSpeed(cancellationToken);
return Ok(downloadSpeed);
}
[HttpGet]
[Route("TestWriteSpeed")]
public async Task<ActionResult> TestWriteSpeed()
{
var writeSpeed = await _settings.TestWriteSpeed();
var downloadSpeed = await _settings.TestDownloadSpeed();
var containerWriteSpeed = await _settings.TestWriteSpeed(downloadPath);
var remoteWriteSpeed = await _settings.TestWriteSpeed(mappedPath);
var result = new
{
downloadSpeed,
containerWriteSpeed,
remoteWriteSpeed
};
return Ok(result);
return Ok(writeSpeed);
}
}

View file

@ -7,16 +7,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.1">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="5.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NReco.Logging.File" Version="1.1.1" />
<PackageReference Include="Serilog" Version="2.10.0" />

View file

@ -5,9 +5,11 @@ VisualStudioVersion = 16.0.29911.84
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdtClient.Web", "RdtClient.Web\RdtClient.Web.csproj", "{A8C7F095-89C6-4CD1-AFB2-27106F470D62}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RdtClient.Service", "RdtClient.Service\RdtClient.Service.csproj", "{06D27710-AE9A-4FA2-B043-1A4303561716}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdtClient.Service", "RdtClient.Service\RdtClient.Service.csproj", "{06D27710-AE9A-4FA2-B043-1A4303561716}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RdtClient.Data", "RdtClient.Data\RdtClient.Data.csproj", "{92EF8817-AD73-4301-93BD-745D7D61DD74}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdtClient.Data", "RdtClient.Data\RdtClient.Data.csproj", "{92EF8817-AD73-4301-93BD-745D7D61DD74}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Downloader", "Downloader\Downloader.csproj", "{48FF995D-AD14-4E41-BC69-7A144C84E3B6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -27,6 +29,10 @@ Global
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|Any CPU.Build.0 = Release|Any CPU
{48FF995D-AD14-4E41-BC69-7A144C84E3B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{48FF995D-AD14-4E41-BC69-7A144C84E3B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48FF995D-AD14-4E41-BC69-7A144C84E3B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48FF995D-AD14-4E41-BC69-7A144C84E3B6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/Browsers/Enable/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=48FF995D_002DAD14_002D4E41_002DBC69_002D7A144C84E3B6/@EntryIndexedValue">ExplicitlyExcluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=92EF8817_002DAD73_002D4301_002D93BD_002D745D7D61DD74_002Fd_003AMigrations/@EntryIndexedValue">ExplicitlyExcluded</s:String>
<s:String x:Key="/Default/CodeInspection/GeneratedCode/GeneratedCodeRegions/=_002A_002Eappxmanifest/@EntryIndexedValue">*.appxmanifest</s:String>
<s:String x:Key="/Default/CodeInspection/GeneratedCode/GeneratedCodeRegions/=_002A_002Ed_002Ets/@EntryIndexedValue">*.d.ts</s:String>