Migrated the simple downloader to the Downloader.
This commit is contained in:
parent
ee9ce2840b
commit
46f6e06572
17 changed files with 641 additions and 13169 deletions
22
README.md
22
README.md
|
|
@ -140,24 +140,16 @@ navigate to http://<ipaddress>:6500, if all is good then we'll create a service
|
|||
|
||||
Currently there 2 available download clients:
|
||||
|
||||
#### Simple Downloader
|
||||
#### Internal Downloader
|
||||
|
||||
This is a simple 1 connection only download manager. It uses less resources than the multi-part downloader. It downloads straight to the download path.
|
||||
This [downloader](https://github.com/bezzad/Downloader) can be used to download files in parallel and with multiple chunks.
|
||||
|
||||
It has the following options:
|
||||
|
||||
- Maximum parallel downloads: This number indicates how many completed torrents from Real-Debrid can be downloaded at the same time. On low powered systems it is recommended to keep this number low.
|
||||
|
||||
#### Multi Part Downloader
|
||||
|
||||
This [downloader](https://github.com/bezzad/Downloader) as more options and such uses more resources (memory, CPU) to download files. Recommended more powerful systems.
|
||||
|
||||
It has the following options:
|
||||
|
||||
- Temp Download path: Set this path to where the downloader temporarily stores chunks. This path can be an internal path in Docker (i.e. `/data/temp`) but make sure you have enough disk space to complete the whole download. When all chunks are completed the completed file is copied to your download folder.
|
||||
- Maximum parallel downloads: This number indicates how many completed torrents from Real-Debrid can be downloaded at the same time.
|
||||
- Parallel connections per download: This number indicates how many threads/connections/parts/chunks it will use per download. This can increase speed, recommended is no more than 8.
|
||||
- Download speed (in MB/s): This number indicates the speed in MB/s per download. If you set this to 10 and `Maximum parallel downloads` to 2, you can download with a maximum of 20MB/s.
|
||||
- Download speed (in MB/s): This number indicates the speed in MB/s per download over all parallel downloads and chunks.
|
||||
- Parallel connections per download: This number indicates how many parallel it will use per download. This can increase speed, recommended is no more than 8.
|
||||
- Parallel chunks per download: This number indicates in how many chunks each download is split, recommended is no more than 8.
|
||||
- Connection Timeout: This number indicates the timeout in milliseconds before a download chunk times out. It will retry each chunk 5 times before completely failing.
|
||||
|
||||
#### Aria2c downloader
|
||||
|
||||
|
|
@ -168,6 +160,8 @@ It has the following options:
|
|||
- Url: The full URL to your Aria2c service. This must end in /jsonrpc. A standard path is `http://192.168.10.2:6800/jsonrpc`.
|
||||
- Secret: Optional secret to connecto to your Aria2c service.
|
||||
|
||||
If Aria2c is selected, none of the above options for `Internal Downloader` are used, you have to configure Aria2c manually.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- If you forgot your logins simply delete the `rdtclient.db` and restart the service.
|
||||
|
|
|
|||
12693
client/package-lock.json
generated
12693
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,11 +4,8 @@ namespace RdtClient.Data.Enums;
|
|||
|
||||
public enum DownloadClient
|
||||
{
|
||||
[Description("Simple Downloader")]
|
||||
Simple,
|
||||
|
||||
[Description("Multi-Part Downloader")]
|
||||
MultiPart,
|
||||
[Description("Internal Downloader")]
|
||||
Internal,
|
||||
|
||||
[Description("Aria2c")]
|
||||
Aria2c
|
||||
|
|
|
|||
463
server/RdtClient.Data/Migrations/20221129035837_Settings_Migrate_InternalDownloader.Designer.cs
generated
Normal file
463
server/RdtClient.Data/Migrations/20221129035837_Settings_Migrate_InternalDownloader.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RdtClient.Data.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RdtClient.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20221129035837_Settings_Migrate_InternalDownloader")]
|
||||
partial class Settings_Migrate_InternalDownloader
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
|
||||
{
|
||||
b.Property<Guid>("DownloadId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Added")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("Completed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("DownloadFinished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("DownloadQueued")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("DownloadStarted")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Error")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RemoteId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RetryCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("TorrentId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnpackingFinished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnpackingQueued")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnpackingStarted")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("DownloadId");
|
||||
|
||||
b.HasIndex("TorrentId");
|
||||
|
||||
b.ToTable("Downloads");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b =>
|
||||
{
|
||||
b.Property<string>("SettingId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SettingId");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
|
||||
{
|
||||
b.Property<Guid>("TorrentId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Added")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("Completed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("DeleteOnError")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DownloadAction")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DownloadManualFiles")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("DownloadMinSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DownloadRetryAttempts")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Error")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileOrMagnet")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("FilesSelected")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("FinishedAction")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("HostDownloadAction")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsFile")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Lifetime")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Priority")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("RdAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("RdEnded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RdFiles")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RdHost")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RdId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RdName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("RdProgress")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("RdSeeders")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("RdSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("RdSpeed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("RdSplit")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("RdStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RdStatusRaw")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("Retry")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RetryCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TorrentRetryAttempts")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("TorrentId");
|
||||
|
||||
b.ToTable("Torrents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
|
||||
{
|
||||
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
|
||||
.WithMany("Downloads")
|
||||
.HasForeignKey("TorrentId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Torrent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
|
||||
{
|
||||
b.Navigation("Downloads");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RdtClient.Data.Migrations
|
||||
{
|
||||
public partial class Settings_Migrate_InternalDownloader : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("UPDATE Settings SET Value = 0 WHERE SettingId = 'DownloadClient:Client' AND Value = 1");
|
||||
migrationBuilder.Sql("UPDATE Settings SET Value = 1 WHERE SettingId = 'DownloadClient:Client' AND Value = 2");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ public class DbSettingsDownloadClient
|
|||
[DisplayName("Download client")]
|
||||
[Description(@"Select which download client to use, see the
|
||||
<a href=""https://github.com/rogerfar/rdt-client/"" target=""_blank"">README</a> for the various options.")]
|
||||
public DownloadClient Client { get; set; } = DownloadClient.Simple;
|
||||
public DownloadClient Client { get; set; } = DownloadClient.Internal;
|
||||
|
||||
[DisplayName("Download path")]
|
||||
[Description("Path in the docker container to download files to (i.e. /data/downloads), or a local path when using as a service.")]
|
||||
|
|
@ -83,10 +83,26 @@ public class DbSettingsDownloadClient
|
|||
[Description("Path where files are downloaded to on your host (i.e. D:\\Downloads). This path is used for *arr to find your downloads.")]
|
||||
public String MappedPath { get; set; } = @"C:\Downloads";
|
||||
|
||||
[DisplayName("Download speed (in MB/s) (only used for the Simple and Multi Downloader)")]
|
||||
[DisplayName("Download speed (in MB/s) (only used for the Internal Downloader)")]
|
||||
[Description("Maximum download speed in Megabytes per second. When set to 0 unlimited speed is used.")]
|
||||
public Int32 MaxSpeed { get; set; } = 0;
|
||||
|
||||
[DisplayName("Parallel connections per download (only used for the Internal Downloader)")]
|
||||
[Description("Maximum amount of parallel threads that are used to download a single file to your host. If set to 0 no parallel downloading will be done.")]
|
||||
public Int32 ParallelCount { get; set; } = 0;
|
||||
|
||||
[DisplayName("Parallel chunks per download (only used for the Internal Downloader)")]
|
||||
[Description("Split each parallel download in chunks.")]
|
||||
public Int32 ChunkCount { get; set; } = 1;
|
||||
|
||||
[DisplayName("Connection Timeout (only used for the Internal Downloader)")]
|
||||
[Description("Timeout in milliseconds before the downloader times out.")]
|
||||
public Int32 Timeout { get; set; } = 5000;
|
||||
|
||||
[DisplayName("Proxy Server (only used for the Internal Downloader)")]
|
||||
[Description("Address of a proxy server to download through (only used for the Internal Downloader).")]
|
||||
public String? ProxyServer { get; set; } = null;
|
||||
|
||||
[DisplayName("Aria2c URL (only used for the Aria2c Downloader)")]
|
||||
[Description(@"This is the URL to your Aria2c instance. It must end in /jsonrpc. A common URL is
|
||||
http://127.0.0.1:6800/jsonrpc.")]
|
||||
|
|
@ -95,18 +111,6 @@ http://127.0.0.1:6800/jsonrpc.")]
|
|||
[DisplayName("Aria2c Secret (only used for the Aria2c Downloader)")]
|
||||
[Description("The secret of your Aria2c instance. Optional.")]
|
||||
public String Aria2cSecret { get; set; } = "mysecret123";
|
||||
|
||||
[DisplayName("Temp Download path (only used for the Multi Downloader)")]
|
||||
[Description("Path in the docker container to temporarily download to (i.e. /data/temp). Make sure the docker container has enough disk space if using a path inside the container.")]
|
||||
public String TempPath { get; set; } = "/data/downloads";
|
||||
|
||||
[DisplayName("Parallel connections per download (only used for the Multi Downloader)")]
|
||||
[Description("Maximum amount of parallel threads that are used to download a single torrent to your host. If set to 1 no parallel downloading will be done.")]
|
||||
public Int32 ChunkCount { get; set; } = 8;
|
||||
|
||||
[DisplayName("Proxy Server")]
|
||||
[Description("Address of a proxy server to download through (only used for the Multi Downloader).")]
|
||||
public String? ProxyServer { get; set; } = null;
|
||||
}
|
||||
|
||||
public class DbSettingsProvider
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ public class Startup : IHostedService
|
|||
var settings = scope.ServiceProvider.GetRequiredService<Settings>();
|
||||
await settings.Seed();
|
||||
await settings.ResetCache();
|
||||
await Settings.Clean();
|
||||
|
||||
Ready = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="AllDebrid.NET" Version="1.0.10" />
|
||||
<PackageReference Include="Aria2.NET" Version="1.0.5" />
|
||||
<PackageReference Include="Downloader" Version="2.4.1" />
|
||||
<PackageReference Include="Downloader" Version="3.0.1" />
|
||||
<PackageReference Include="MonoTorrent" Version="2.0.6" />
|
||||
<PackageReference Include="RD.NET" Version="2.1.3" />
|
||||
<PackageReference Include="Serilog" Version="2.12.0" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using RdtClient.Data.Models.Data;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.Helpers;
|
||||
using RdtClient.Service.Services.Downloaders;
|
||||
|
||||
|
|
@ -31,7 +30,7 @@ public class DownloadClient
|
|||
_destinationPath = destinationPath;
|
||||
}
|
||||
|
||||
public async Task<String?> Start(DbSettings settings)
|
||||
public async Task<String?> Start()
|
||||
{
|
||||
BytesDone = 0;
|
||||
BytesTotal = 0;
|
||||
|
|
@ -53,14 +52,13 @@ public class DownloadClient
|
|||
|
||||
await FileHelper.Delete(filePath);
|
||||
|
||||
Type = settings.DownloadClient.Client;
|
||||
Type = Settings.Get.DownloadClient.Client;
|
||||
|
||||
Downloader = settings.DownloadClient.Client switch
|
||||
Downloader = Settings.Get.DownloadClient.Client switch
|
||||
{
|
||||
Data.Enums.DownloadClient.Simple => new SimpleDownloader(_download.Link, filePath),
|
||||
Data.Enums.DownloadClient.MultiPart => new MultiDownloader(_download.Link, filePath, settings),
|
||||
Data.Enums.DownloadClient.Aria2c => new Aria2cDownloader(_download.RemoteId, _download.Link, filePath, settings),
|
||||
_ => throw new Exception($"Unknown download client {settings.DownloadClient}")
|
||||
Data.Enums.DownloadClient.Internal => new InternalDownloader(_download.Link, filePath),
|
||||
Data.Enums.DownloadClient.Aria2c => new Aria2cDownloader(_download.RemoteId, _download.Link, filePath),
|
||||
_ => throw new Exception($"Unknown download client {Settings.Get.DownloadClient}")
|
||||
};
|
||||
|
||||
Downloader.DownloadComplete += (_, args) =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Aria2NET;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using Serilog;
|
||||
|
||||
namespace RdtClient.Service.Services.Downloaders;
|
||||
|
|
@ -19,7 +18,7 @@ public class Aria2cDownloader : IDownloader
|
|||
|
||||
private String? _gid;
|
||||
|
||||
public Aria2cDownloader(String? gid, String uri, String filePath, DbSettings settings)
|
||||
public Aria2cDownloader(String? gid, String uri, String filePath)
|
||||
{
|
||||
_logger = Log.ForContext<Aria2cDownloader>();
|
||||
_gid = gid;
|
||||
|
|
@ -31,7 +30,7 @@ public class Aria2cDownloader : IDownloader
|
|||
Timeout = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
_aria2NetClient = new Aria2NetClient(settings.DownloadClient.Aria2cUrl, settings.DownloadClient.Aria2cSecret, httpClient, 10);
|
||||
_aria2NetClient = new Aria2NetClient(Settings.Get.DownloadClient.Aria2cUrl, Settings.Get.DownloadClient.Aria2cSecret, httpClient, 10);
|
||||
}
|
||||
|
||||
public async Task<String?> Download()
|
||||
|
|
|
|||
|
|
@ -1,63 +1,43 @@
|
|||
using System.Net;
|
||||
using Downloader;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using Serilog;
|
||||
|
||||
namespace RdtClient.Service.Services.Downloaders;
|
||||
|
||||
public class MultiDownloader : IDownloader
|
||||
public class InternalDownloader : IDownloader
|
||||
{
|
||||
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
|
||||
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
|
||||
|
||||
private readonly DownloadService _downloadService;
|
||||
private readonly DownloadConfiguration _downloadConfiguration;
|
||||
|
||||
private readonly String _filePath;
|
||||
private readonly String _uri;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public MultiDownloader(String uri, String filePath, DbSettings settings)
|
||||
private Boolean _finished;
|
||||
|
||||
public InternalDownloader(String uri, String filePath)
|
||||
{
|
||||
_logger = Log.ForContext<MultiDownloader>();
|
||||
_logger = Log.ForContext<InternalDownloader>();
|
||||
|
||||
_uri = uri;
|
||||
_filePath = filePath;
|
||||
|
||||
var settingTempPath = settings.DownloadClient.TempPath;
|
||||
var settingProxyServer = Settings.Get.DownloadClient.ProxyServer;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(settingTempPath))
|
||||
// For all options, see https://github.com/bezzad/Downloader
|
||||
_downloadConfiguration = new DownloadConfiguration
|
||||
{
|
||||
settingTempPath = Path.GetTempPath();
|
||||
}
|
||||
|
||||
var settingDownloadChunkCount = settings.DownloadClient.ChunkCount;
|
||||
|
||||
if (settingDownloadChunkCount <= 0)
|
||||
{
|
||||
settingDownloadChunkCount = 1;
|
||||
}
|
||||
|
||||
var settingDownloadMaxSpeed = settings.DownloadClient.MaxSpeed;
|
||||
|
||||
if (settingDownloadMaxSpeed <= 0)
|
||||
{
|
||||
settingDownloadMaxSpeed = 0;
|
||||
}
|
||||
|
||||
settingDownloadMaxSpeed = settingDownloadMaxSpeed * 1024 * 1024;
|
||||
|
||||
var settingProxyServer = settings.DownloadClient.ProxyServer;
|
||||
|
||||
var downloadOpt = new DownloadConfiguration
|
||||
{
|
||||
MaxTryAgainOnFailover = Int32.MaxValue,
|
||||
ParallelDownload = settingDownloadChunkCount > 1,
|
||||
ChunkCount = settingDownloadChunkCount,
|
||||
Timeout = 1000,
|
||||
OnTheFlyDownload = false,
|
||||
BufferBlockSize = 1024 * 8,
|
||||
MaximumBytesPerSecond = settingDownloadMaxSpeed,
|
||||
TempDirectory = settingTempPath,
|
||||
MaxTryAgainOnFailover = 5,
|
||||
RangeDownload = false,
|
||||
ClearPackageOnCompletionWithFailure = false,
|
||||
MinimumSizeOfChunking = 1024,
|
||||
ReserveStorageSpaceBeforeStartingDownload = false,
|
||||
CheckDiskSizeBeforeDownload = true,
|
||||
RequestConfiguration =
|
||||
{
|
||||
Accept = "*/*",
|
||||
|
|
@ -69,12 +49,14 @@ public class MultiDownloader : IDownloader
|
|||
}
|
||||
};
|
||||
|
||||
SetSettings();
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(settingProxyServer))
|
||||
{
|
||||
downloadOpt.RequestConfiguration.Proxy = new WebProxy(new Uri(settingProxyServer), false);
|
||||
_downloadConfiguration.RequestConfiguration.Proxy = new WebProxy(new Uri(settingProxyServer), false);
|
||||
}
|
||||
|
||||
_downloadService = new DownloadService(downloadOpt);
|
||||
_downloadService = new DownloadService(_downloadConfiguration);
|
||||
|
||||
_downloadService.DownloadProgressChanged += (_, args) =>
|
||||
{
|
||||
|
|
@ -110,16 +92,23 @@ public class MultiDownloader : IDownloader
|
|||
{
|
||||
Error = error
|
||||
});
|
||||
|
||||
_finished = true;
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<String?> Download()
|
||||
public Task<String?> Download()
|
||||
{
|
||||
_logger.Debug($"Starting download of {_uri}, writing to path: {_filePath}");
|
||||
|
||||
await _downloadService.DownloadFileTaskAsync(_uri, _filePath);
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await _downloadService.DownloadFileTaskAsync(_uri, _filePath);
|
||||
});
|
||||
|
||||
return null;
|
||||
Task.Run(StartTimer);
|
||||
|
||||
return Task.FromResult<String?>(null);
|
||||
}
|
||||
|
||||
public Task Cancel()
|
||||
|
|
@ -140,4 +129,58 @@ public class MultiDownloader : IDownloader
|
|||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void SetSettings()
|
||||
{
|
||||
var settingDownloadParallelCount = Settings.Get.DownloadClient.ParallelCount;
|
||||
|
||||
if (settingDownloadParallelCount <= 0)
|
||||
{
|
||||
settingDownloadParallelCount = 1;
|
||||
}
|
||||
|
||||
var settingDownloadChunkCount = Settings.Get.DownloadClient.ChunkCount;
|
||||
|
||||
if (settingDownloadChunkCount <= 0)
|
||||
{
|
||||
settingDownloadChunkCount = 1;
|
||||
}
|
||||
|
||||
var settingDownloadMaxSpeed = Settings.Get.DownloadClient.MaxSpeed;
|
||||
|
||||
if (settingDownloadMaxSpeed <= 0)
|
||||
{
|
||||
settingDownloadMaxSpeed = 0;
|
||||
}
|
||||
|
||||
settingDownloadMaxSpeed = settingDownloadMaxSpeed * 1024 * 1024;
|
||||
|
||||
var settingDownloadTimeout = Settings.Get.DownloadClient.Timeout;
|
||||
|
||||
if (settingDownloadTimeout <= 0)
|
||||
{
|
||||
settingDownloadTimeout = 1000;
|
||||
}
|
||||
|
||||
_downloadConfiguration.ChunkCount = settingDownloadChunkCount;
|
||||
_downloadConfiguration.MaximumBytesPerSecond = settingDownloadMaxSpeed;
|
||||
_downloadConfiguration.ParallelDownload = settingDownloadChunkCount > 1;
|
||||
_downloadConfiguration.ParallelCount = settingDownloadParallelCount;
|
||||
_downloadConfiguration.Timeout = settingDownloadTimeout;
|
||||
}
|
||||
|
||||
private async Task StartTimer()
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
|
||||
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
{
|
||||
if (_finished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
using System.Net;
|
||||
using Serilog;
|
||||
|
||||
namespace RdtClient.Service.Services.Downloaders;
|
||||
|
||||
public class SimpleDownloader : IDownloader
|
||||
{
|
||||
private const Int32 BufferSize = 8 * 1024;
|
||||
|
||||
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
|
||||
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
|
||||
|
||||
private readonly String _uri;
|
||||
private readonly String _filePath;
|
||||
|
||||
private Int64 _bytesTotal;
|
||||
private Int64 _bytesDone;
|
||||
|
||||
private Boolean _cancelled;
|
||||
|
||||
private DateTime _nextUpdate;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SimpleDownloader(String uri, String filePath)
|
||||
{
|
||||
_logger = Log.ForContext<SimpleDownloader>();
|
||||
|
||||
_uri = uri;
|
||||
_filePath = filePath;
|
||||
}
|
||||
|
||||
public Task<String?> Download()
|
||||
{
|
||||
_logger.Debug($"Starting download of {_uri}, writing to path: {_filePath}");
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await StartDownloadTask();
|
||||
});
|
||||
|
||||
return Task.FromResult<String?>(null);
|
||||
}
|
||||
|
||||
public Task Cancel()
|
||||
{
|
||||
_logger.Debug($"Cancelling download {_uri}");
|
||||
|
||||
_cancelled = true;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task StartDownloadTask()
|
||||
{
|
||||
try
|
||||
{
|
||||
_nextUpdate = DateTime.UtcNow.AddSeconds(1);
|
||||
|
||||
_bytesTotal = await GetContentSize();
|
||||
|
||||
var timeout = DateTimeOffset.UtcNow.AddHours(1);
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
|
||||
while (timeout > DateTimeOffset.UtcNow && !_cancelled)
|
||||
{
|
||||
try
|
||||
{
|
||||
var responseStream = await httpClient.GetStreamAsync(_uri);
|
||||
|
||||
if (responseStream == null)
|
||||
{
|
||||
throw new IOException("No stream");
|
||||
}
|
||||
|
||||
var speedLimit = Settings.Get.DownloadClient.MaxSpeed;
|
||||
|
||||
await using var destinationStream = new ThrottledStream(responseStream, speedLimit * 1000L * 1000L);
|
||||
|
||||
await using var fileStream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.Write);
|
||||
|
||||
var readSize = 1;
|
||||
var buffer = new Byte[BufferSize * 8];
|
||||
|
||||
while (readSize > 0 && !_cancelled)
|
||||
{
|
||||
// ReSharper disable once ConvertToUsingDeclaration
|
||||
#pragma warning disable IDE0063 // Use simple 'using' statement
|
||||
using (var innerCts = new CancellationTokenSource(1000))
|
||||
{
|
||||
readSize = await destinationStream.ReadAsync(buffer.AsMemory(0, buffer.Length), innerCts.Token).ConfigureAwait(false);
|
||||
|
||||
await fileStream.WriteAsync(buffer.AsMemory(0, readSize), innerCts.Token);
|
||||
|
||||
_bytesDone = fileStream.Length;
|
||||
|
||||
if (DateTime.UtcNow > _nextUpdate)
|
||||
{
|
||||
_nextUpdate = DateTime.UtcNow.AddSeconds(1);
|
||||
|
||||
timeout = DateTimeOffset.UtcNow.AddHours(1);
|
||||
|
||||
DownloadProgress?.Invoke(this,
|
||||
new DownloadProgressEventArgs
|
||||
{
|
||||
Speed = destinationStream.Speed,
|
||||
BytesDone = _bytesDone,
|
||||
BytesTotal = _bytesTotal
|
||||
});
|
||||
|
||||
if (Settings.Get.DownloadClient.MaxSpeed != speedLimit)
|
||||
{
|
||||
speedLimit = Settings.Get.DownloadClient.MaxSpeed;
|
||||
destinationStream.BandwidthLimit = speedLimit * 1000L * 1000L;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore IDE0063 // Use simple 'using' statement
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
catch (WebException)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (_cancelled)
|
||||
{
|
||||
throw new Exception("Download cancelled");
|
||||
}
|
||||
|
||||
if (timeout <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
throw new Exception($"Download timed out");
|
||||
}
|
||||
|
||||
DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Int64> GetContentSize()
|
||||
{
|
||||
var httpClient = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(5)
|
||||
};
|
||||
|
||||
var responseHeaders = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _uri));
|
||||
|
||||
if (!responseHeaders.IsSuccessStatusCode)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return responseHeaders.Content.Headers.ContentLength ?? -1;
|
||||
}
|
||||
|
||||
public Task Pause()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Resume()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using RdtClient.Data.Data;
|
||||
using RdtClient.Data.Enums;
|
||||
using RdtClient.Data.Models.Internal;
|
||||
using RdtClient.Service.Helpers;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
|
|
@ -45,28 +44,6 @@ public class Settings
|
|||
await _settingData.Update(settingId, value);
|
||||
}
|
||||
|
||||
public static async Task Clean()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempPath = Get.DownloadClient.TempPath;
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(tempPath))
|
||||
{
|
||||
var files = Directory.GetFiles(tempPath, "*.dsc", SearchOption.TopDirectoryOnly);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
await FileHelper.Delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Seed()
|
||||
{
|
||||
await _settingData.Seed();
|
||||
|
|
|
|||
|
|
@ -1,214 +0,0 @@
|
|||
namespace RdtClient.Service.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Class for streaming data with throttling support.
|
||||
/// Taken from Downloader: https://github.com/bezzad/Downloader
|
||||
/// </summary>
|
||||
public class ThrottledStream : Stream
|
||||
{
|
||||
public Int64 Speed => (Int64) (_bandwidth?.AverageSpeed ?? 0);
|
||||
|
||||
private Bandwidth? _bandwidth;
|
||||
private Int64 _bandwidthLimit;
|
||||
private readonly Stream _baseStream;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="T:ThrottledStream" /> class.
|
||||
/// </summary>
|
||||
/// <param name="baseStream">The base stream.</param>
|
||||
/// <param name="bandwidthLimit">The maximum bytes per second that can be transferred through the base stream.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <see cref="baseStream" /> is a null reference.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <see cref="BandwidthLimit" /> is a negative value.</exception>
|
||||
public ThrottledStream(Stream baseStream, Int64 bandwidthLimit)
|
||||
{
|
||||
if (bandwidthLimit < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bandwidthLimit),
|
||||
bandwidthLimit,
|
||||
"The maximum number of bytes per second can't be negative.");
|
||||
}
|
||||
|
||||
_baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
|
||||
BandwidthLimit = bandwidthLimit;
|
||||
}
|
||||
|
||||
public static Int64 Infinite => Int64.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Bandwidth Limit (in B/s)
|
||||
/// </summary>
|
||||
/// <value>The maximum bytes per second.</value>
|
||||
public Int64 BandwidthLimit
|
||||
{
|
||||
get => _bandwidthLimit;
|
||||
set
|
||||
{
|
||||
_bandwidthLimit = value <= 0 ? Infinite : value;
|
||||
_bandwidth ??= new Bandwidth();
|
||||
_bandwidth.BandwidthLimit = _bandwidthLimit;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Boolean CanRead => _baseStream.CanRead;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Boolean CanSeek => _baseStream.CanSeek;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Boolean CanWrite => _baseStream.CanWrite;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Int64 Length => _baseStream.Length;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Int64 Position
|
||||
{
|
||||
get => _baseStream.Position;
|
||||
set => _baseStream.Position = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Flush()
|
||||
{
|
||||
_baseStream.Flush();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Int64 Seek(Int64 offset, SeekOrigin origin)
|
||||
{
|
||||
return _baseStream.Seek(offset, origin);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetLength(Int64 value)
|
||||
{
|
||||
_baseStream.SetLength(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count)
|
||||
{
|
||||
Throttle(count).Wait();
|
||||
return _baseStream.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override async Task<Int32> ReadAsync(Byte[] buffer,
|
||||
Int32 offset,
|
||||
Int32 count,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await Throttle(count).ConfigureAwait(false);
|
||||
return await _baseStream.ReadAsync(new Memory<Byte>(buffer, offset, count), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async ValueTask<Int32> ReadAsync(Memory<Byte> buffer, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
await Throttle(buffer.Length).ConfigureAwait(false);
|
||||
return await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Byte[] buffer, Int32 offset, Int32 count)
|
||||
{
|
||||
Throttle(count).Wait();
|
||||
_baseStream.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task WriteAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
|
||||
{
|
||||
await Throttle(count).ConfigureAwait(false);
|
||||
await _baseStream.WriteAsync(new ReadOnlyMemory<Byte>(buffer, offset, count), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async ValueTask WriteAsync(ReadOnlyMemory<Byte> buffer, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
await Throttle(buffer.Length).ConfigureAwait(false);
|
||||
await base.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task Throttle(Int32 transmissionVolume)
|
||||
{
|
||||
// Make sure the buffer isn't empty.
|
||||
if (BandwidthLimit > 0 && transmissionVolume > 0)
|
||||
{
|
||||
// Calculate the time to sleep.
|
||||
_bandwidth!.CalculateSpeed(transmissionVolume);
|
||||
await Sleep(_bandwidth!.PopSpeedRetrieveTime()).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task Sleep(Int32 time)
|
||||
{
|
||||
if (time > 0)
|
||||
{
|
||||
await Task.Delay(time).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override String? ToString()
|
||||
{
|
||||
return _baseStream.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
internal class Bandwidth
|
||||
{
|
||||
private const Double OneSecond = 1000; // millisecond
|
||||
private Int64 _count;
|
||||
private Int32 _lastSecondCheckpoint;
|
||||
private Int64 _lastTransferredBytesCount;
|
||||
private Int32 _speedRetrieveTime;
|
||||
|
||||
public Bandwidth()
|
||||
{
|
||||
BandwidthLimit = Int64.MaxValue;
|
||||
Reset();
|
||||
}
|
||||
|
||||
public Double Speed { get; private set; }
|
||||
public Double AverageSpeed { get; private set; }
|
||||
public Int64 BandwidthLimit { get; set; }
|
||||
|
||||
public void CalculateSpeed(Int64 receivedBytesCount)
|
||||
{
|
||||
var elapsedTime = (Environment.TickCount - _lastSecondCheckpoint) + 1;
|
||||
receivedBytesCount = Interlocked.Add(ref _lastTransferredBytesCount, receivedBytesCount);
|
||||
var momentSpeed = (receivedBytesCount * OneSecond) / elapsedTime; // B/s
|
||||
|
||||
if (OneSecond < elapsedTime)
|
||||
{
|
||||
Speed = momentSpeed;
|
||||
AverageSpeed = ((AverageSpeed * _count) + Speed) / (_count + 1);
|
||||
_count++;
|
||||
SecondCheckpoint();
|
||||
}
|
||||
|
||||
if (momentSpeed >= BandwidthLimit)
|
||||
{
|
||||
var expectedTime = (receivedBytesCount * OneSecond) / BandwidthLimit;
|
||||
Interlocked.Add(ref _speedRetrieveTime, (Int32) expectedTime - elapsedTime);
|
||||
}
|
||||
}
|
||||
|
||||
public Int32 PopSpeedRetrieveTime()
|
||||
{
|
||||
return Interlocked.Exchange(ref _speedRetrieveTime, 0);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
SecondCheckpoint();
|
||||
_count = 0;
|
||||
Speed = 0;
|
||||
AverageSpeed = 0;
|
||||
}
|
||||
|
||||
private void SecondCheckpoint()
|
||||
{
|
||||
Interlocked.Exchange(ref _lastSecondCheckpoint, Environment.TickCount);
|
||||
Interlocked.Exchange(ref _lastTransferredBytesCount, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -368,7 +368,7 @@ public class TorrentRunner
|
|||
{
|
||||
Log($"Starting download", download, torrent);
|
||||
|
||||
var remoteId = await downloadClient.Start(Settings.Get);
|
||||
var remoteId = await downloadClient.Start();
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(remoteId) && download.RemoteId != remoteId)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ public class SettingsController : Controller
|
|||
|
||||
var downloadClient = new DownloadClient(download, download.Torrent, downloadPath);
|
||||
|
||||
await downloadClient.Start(Settings.Get);
|
||||
await downloadClient.Start();
|
||||
|
||||
var httpClient = new HttpClient
|
||||
{
|
||||
|
|
@ -140,8 +140,6 @@ public class SettingsController : Controller
|
|||
|
||||
await FileHelper.Delete(testFilePath);
|
||||
|
||||
await Settings.Clean();
|
||||
|
||||
return Ok(downloadClient.Speed);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:4201",
|
||||
"applicationUrl": "http://localhost:6500",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue