diff --git a/README.md b/README.md index b693b30..59b00ab 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,15 @@ It has the following options: - 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. +#### Aria2c downloader + +This will use an external Aria2c downloader client. You will need to install this client yourself on your host, it is not included in the docker image. + +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. + ### Troubleshooting - If you forgot your logins simply delete the `rdtclient.db` and restart the service. diff --git a/client/src/app/settings/settings.component.html b/client/src/app/settings/settings.component.html index deccb9f..6b1d404 100644 --- a/client/src/app/settings/settings.component.html +++ b/client/src/app/settings/settings.component.html @@ -75,6 +75,7 @@

@@ -112,6 +113,7 @@ parallel downloading will be done.

+
@@ -119,6 +121,7 @@

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

+
@@ -126,6 +129,27 @@

Address of a proxy server.

+ +
+ +
+ +
+

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

+
+ +
+ +
+ +
+

+ The secret of your Aria2c instance. Optional. +

+
diff --git a/client/src/app/settings/settings.component.ts b/client/src/app/settings/settings.component.ts index dcf0c73..31d7fbb 100644 --- a/client/src/app/settings/settings.component.ts +++ b/client/src/app/settings/settings.component.ts @@ -36,6 +36,9 @@ export class SettingsComponent implements OnInit { public settingOnlyDownloadAvailableFiles: boolean; public settingProxyServer: string; + public aria2cUrl: string; + public aria2cSecret: string; + constructor(private settingsService: SettingsService) {} ngOnInit(): void { @@ -61,6 +64,8 @@ export class SettingsComponent implements OnInit { this.settingMinFileSize = parseInt(this.getSetting(results, 'MinFileSize'), 10); this.settingOnlyDownloadAvailableFiles = this.getSetting(results, 'OnlyDownloadAvailableFiles') === '1'; this.settingProxyServer = this.getSetting(results, 'ProxyServer'); + this.aria2cUrl = this.getSetting(results, 'Aria2cUrl'); + this.aria2cSecret = this.getSetting(results, 'Aria2cSecret'); }, (err) => { this.error = err.error; @@ -125,6 +130,14 @@ export class SettingsComponent implements OnInit { settingId: 'ProxyServer', value: this.settingProxyServer, }, + { + settingId: 'Aria2cUrl', + value: this.aria2cUrl, + }, + { + settingId: 'Aria2cSecret', + value: this.aria2cSecret, + }, ]; this.settingsService.update(settings).subscribe( diff --git a/server/RdtClient.Data/Data/DataContext.cs b/server/RdtClient.Data/Data/DataContext.cs index b79e210..a920088 100644 --- a/server/RdtClient.Data/Data/DataContext.cs +++ b/server/RdtClient.Data/Data/DataContext.cs @@ -135,6 +135,18 @@ namespace RdtClient.Data.Data SettingId = "Categories", Type = "String", Value = "" + }, + new Setting + { + SettingId = "Aria2cUrl", + Type = "String", + Value = "http://127.0.0.1:6800/jsonrpc" + }, + new Setting + { + SettingId = "Aria2cSecret", + Type = "String", + Value = "" } }; diff --git a/server/RdtClient.Data/Data/DownloadData.cs b/server/RdtClient.Data/Data/DownloadData.cs index f5e423d..475d491 100644 --- a/server/RdtClient.Data/Data/DownloadData.cs +++ b/server/RdtClient.Data/Data/DownloadData.cs @@ -164,6 +164,16 @@ namespace RdtClient.Data.Data await TorrentData.VoidCache(); } + public async Task UpdateRemoteId(Guid downloadId, String remoteId) + { + var dbDownload = await _dataContext.Downloads + .FirstOrDefaultAsync(m => m.DownloadId == downloadId); + + dbDownload.RemoteId = remoteId; + + await _dataContext.SaveChangesAsync(); + } + public async Task DeleteForTorrent(Guid torrentId) { var downloads = await _dataContext.Downloads diff --git a/server/RdtClient.Data/Data/SettingData.cs b/server/RdtClient.Data/Data/SettingData.cs index f12054b..f710e98 100644 --- a/server/RdtClient.Data/Data/SettingData.cs +++ b/server/RdtClient.Data/Data/SettingData.cs @@ -44,7 +44,9 @@ namespace RdtClient.Data.Data DownloadMaxSpeed = GetInt32("DownloadMaxSpeed"), ProxyServer = GetString("ProxyServer"), LogLevel = GetString("LogLevel"), - Categories = GetString("Categories") + Categories = GetString("Categories"), + Aria2cUrl = GetString("Aria2cUrl"), + Aria2cSecret = GetString("Aria2cSecret"), }; } diff --git a/server/RdtClient.Data/Migrations/20211009221133_Downloads_Add_RemoteId.Designer.cs b/server/RdtClient.Data/Migrations/20211009221133_Downloads_Add_RemoteId.Designer.cs new file mode 100644 index 0000000..62b0424 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20211009221133_Downloads_Add_RemoteId.Designer.cs @@ -0,0 +1,436 @@ +// +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("20211009221133_Downloads_Add_RemoteId")] + partial class Downloads_Add_RemoteId + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.10"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + 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") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + 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("Completed") + .HasColumnType("TEXT"); + + b.Property("DownloadFinished") + .HasColumnType("TEXT"); + + b.Property("DownloadQueued") + .HasColumnType("TEXT"); + + b.Property("DownloadStarted") + .HasColumnType("TEXT"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("RemoteId") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("TorrentId") + .HasColumnType("TEXT"); + + b.Property("UnpackingFinished") + .HasColumnType("TEXT"); + + b.Property("UnpackingQueued") + .HasColumnType("TEXT"); + + b.Property("UnpackingStarted") + .HasColumnType("TEXT"); + + b.HasKey("DownloadId"); + + b.HasIndex("TorrentId"); + + b.ToTable("Downloads"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b => + { + b.Property("SettingId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Property("TorrentId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DownloadAction") + .HasColumnType("INTEGER"); + + b.Property("DownloadManualFiles") + .HasColumnType("TEXT"); + + b.Property("DownloadMinSize") + .HasColumnType("INTEGER"); + + b.Property("FileOrMagnet") + .HasColumnType("TEXT"); + + b.Property("FilesSelected") + .HasColumnType("TEXT"); + + b.Property("FinishedAction") + .HasColumnType("INTEGER"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("IsFile") + .HasColumnType("INTEGER"); + + b.Property("RdAdded") + .HasColumnType("TEXT"); + + b.Property("RdEnded") + .HasColumnType("TEXT"); + + b.Property("RdFiles") + .HasColumnType("TEXT"); + + b.Property("RdHost") + .HasColumnType("TEXT"); + + b.Property("RdId") + .HasColumnType("TEXT"); + + b.Property("RdName") + .HasColumnType("TEXT"); + + b.Property("RdProgress") + .HasColumnType("INTEGER"); + + b.Property("RdSeeders") + .HasColumnType("INTEGER"); + + b.Property("RdSize") + .HasColumnType("INTEGER"); + + b.Property("RdSpeed") + .HasColumnType("INTEGER"); + + b.Property("RdSplit") + .HasColumnType("INTEGER"); + + b.Property("RdStatus") + .HasColumnType("INTEGER"); + + b.Property("RdStatusRaw") + .HasColumnType("TEXT"); + + b.HasKey("TorrentId"); + + b.ToTable("Torrents"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent") + .WithMany("Downloads") + .HasForeignKey("TorrentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Torrent"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Navigation("Downloads"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/RdtClient.Data/Migrations/20211009221133_Downloads_Add_RemoteId.cs b/server/RdtClient.Data/Migrations/20211009221133_Downloads_Add_RemoteId.cs new file mode 100644 index 0000000..bae4101 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20211009221133_Downloads_Add_RemoteId.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RdtClient.Data.Migrations +{ + public partial class Downloads_Add_RemoteId : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RemoteId", + table: "Downloads", + type: "TEXT", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RemoteId", + table: "Downloads"); + } + } +} diff --git a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs index 9cc3b17..f4cf51b 100644 --- a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs +++ b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace RdtClient.Data.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.8"); + .HasAnnotation("ProductVersion", "5.0.10"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { @@ -238,6 +238,9 @@ namespace RdtClient.Data.Migrations b.Property("Path") .HasColumnType("TEXT"); + b.Property("RemoteId") + .HasColumnType("TEXT"); + b.Property("RetryCount") .HasColumnType("INTEGER"); diff --git a/server/RdtClient.Data/Models/Data/Download.cs b/server/RdtClient.Data/Models/Data/Download.cs index 9efed0e..31a3a78 100644 --- a/server/RdtClient.Data/Models/Data/Download.cs +++ b/server/RdtClient.Data/Models/Data/Download.cs @@ -35,6 +35,8 @@ namespace RdtClient.Data.Models.Data public String Error { get; set; } + public String RemoteId { get; set; } + [ForeignKey("TorrentId")] public Torrent Torrent { get; set; } diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs index d7e7d2e..0c89239 100644 --- a/server/RdtClient.Data/Models/Internal/DbSettings.cs +++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs @@ -18,5 +18,7 @@ namespace RdtClient.Data.Models.Internal public String ProxyServer { get; set; } public String LogLevel { get; set; } public String Categories { get; set; } + public String Aria2cUrl { get; set; } + public String Aria2cSecret { get; set; } } } diff --git a/server/RdtClient.Service/RdtClient.Service.csproj b/server/RdtClient.Service/RdtClient.Service.csproj index 3c9eee1..9b441ed 100644 --- a/server/RdtClient.Service/RdtClient.Service.csproj +++ b/server/RdtClient.Service/RdtClient.Service.csproj @@ -6,6 +6,7 @@ + diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs index 83a99b1..646b0df 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -1,11 +1,10 @@ using System; using System.IO; -using System.Net; using System.Threading.Tasks; -using Downloader; using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Internal; using RdtClient.Service.Helpers; +using RdtClient.Service.Services.Downloaders; namespace RdtClient.Service.Services { @@ -16,8 +15,7 @@ namespace RdtClient.Service.Services private readonly Download _download; private readonly Torrent _torrent; - private DownloadService _downloader; - private SimpleDownloader _simpleDownloader; + private IDownloader _downloader; public DownloadClient(Download download, Torrent torrent, String destinationPath) { @@ -34,7 +32,7 @@ namespace RdtClient.Service.Services public Int64 BytesTotal { get; private set; } public Int64 BytesDone { get; private set; } - public async Task Start(DbSettings settings) + public async Task Start(DbSettings settings) { BytesDone = 0; BytesTotal = 0; @@ -54,159 +52,41 @@ namespace RdtClient.Service.Services File.Delete(filePath); } - var uri = new Uri(_download.Link); + _downloader = settings.DownloadClient switch + { + "Simple" => new SimpleDownloader(_download.Link, filePath), + "MultiPart" => new MultiDownloader(_download.Link, filePath, settings), + "Aria2c" => new Aria2cDownloader(_download.RemoteId, _download.Link, filePath, settings), + _ => throw new Exception($"Unknown download client {settings.DownloadClient}") + }; -#pragma warning disable 4014 - Task.Run(async delegate -#pragma warning restore 4014 - { - switch (settings.DownloadClient) - { - case "Simple": - await DownloadSimple(uri, filePath); - break; - case "MultiPart": - await DownloadMultiPart(filePath, settings); - break; - default: - throw new Exception($"Unknown download client {settings.DownloadClient}"); - } - }); + _downloader.DownloadComplete += (_, args) => + { + Finished = true; + Error = args.Error; + }; + + _downloader.DownloadProgress += (_, args) => + { + Speed = args.Speed; + BytesDone = args.BytesDone; + BytesTotal = args.BytesTotal; + }; + + return await _downloader.Download(); } catch (Exception ex) { Error = $"An unexpected error occurred preparing download {_download.Link} for torrent {_torrent.RdName}: {ex.Message}"; Finished = true; + + return null; } } public void Cancel() { - _downloader?.CancelAsync(); - _simpleDownloader?.Cancel(); - } - - private async Task DownloadSimple(Uri uri, String filePath) - { - try - { - _simpleDownloader = new SimpleDownloader(); - - _simpleDownloader.DownloadProgressChanged += (_, args) => - { - Speed = (Int64) args.BytesPerSecondSpeed; - BytesDone = args.ReceivedBytesSize; - BytesTotal = args.TotalBytesToReceive; - }; - - _simpleDownloader.DownloadFileCompleted += (_, args) => - { - if (args.Cancelled) - { - Error = $"The download was cancelled"; - } - else if (args.Error != null) - { - Error = args.Error.Message; - } - - Finished = true; - }; - - Speed = 0; - BytesDone = 0; - BytesTotal = 0; - - await _simpleDownloader.Download(uri, filePath); - } - catch (Exception ex) - { - Error = $"An unexpected error occurred downloading {_download.Link} for torrent {_torrent.RdName}: {ex.Message}"; - Finished = true; - } - } - - private async Task DownloadMultiPart(String filePath, DbSettings settings) - { - try - { - var settingTempPath = settings.TempPath; - if (String.IsNullOrWhiteSpace(settingTempPath)) - { - settingTempPath = Path.GetTempPath(); - } - - var settingDownloadChunkCount = settings.DownloadChunkCount; - if (settingDownloadChunkCount <= 0) - { - settingDownloadChunkCount = 1; - } - - var settingDownloadMaxSpeed = settings.DownloadMaxSpeed; - if (settingDownloadMaxSpeed <= 0) - { - settingDownloadMaxSpeed = 0; - } - settingDownloadMaxSpeed = settingDownloadMaxSpeed * 1024 * 1024; - - var settingProxyServer = settings.ProxyServer; - - var downloadOpt = new DownloadConfiguration - { - MaxTryAgainOnFailover = Int32.MaxValue, - ParallelDownload = settingDownloadChunkCount > 1, - ChunkCount = settingDownloadChunkCount, - Timeout = 1000, - OnTheFlyDownload = false, - BufferBlockSize = 1024 * 8, - MaximumBytesPerSecond = settingDownloadMaxSpeed, - TempDirectory = settingTempPath, - RequestConfiguration = - { - Accept = "*/*", - UserAgent = $"rdt-client", - ProtocolVersion = HttpVersion.Version11, - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, - KeepAlive = true, - UseDefaultCredentials = false - } - }; - - if (!String.IsNullOrWhiteSpace(settingProxyServer)) - { - downloadOpt.RequestConfiguration.Proxy = new WebProxy(new Uri(settingProxyServer), false); - } - - _downloader = new DownloadService(downloadOpt); - - _downloader.DownloadProgressChanged += (_, args) => - { - Speed = (Int64) args.BytesPerSecondSpeed; - BytesDone = args.ReceivedBytesSize; - BytesTotal = args.TotalBytesToReceive; - }; - - _downloader.DownloadFileCompleted += (_, args) => - { - if (args.Cancelled) - { - Error = $"The download was cancelled"; - } - else if (args.Error != null) - { - Error = args.Error.Message; - } - - Finished = true; - }; - - await _downloader.DownloadFileTaskAsync(_download.Link, filePath); - } - catch (Exception ex) - { - Error = $"An unexpected error occurred downloading {_download.Link} for torrent {_torrent.RdName}: {ex.Message}"; - Finished = true; - } + _downloader?.Cancel(); } } } diff --git a/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs b/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs new file mode 100644 index 0000000..a909306 --- /dev/null +++ b/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Timers; +using Aria2NET; +using RdtClient.Data.Models.Internal; + +namespace RdtClient.Service.Services.Downloaders +{ + public class Aria2cDownloader : IDownloader + { + public event EventHandler DownloadComplete; + public event EventHandler DownloadProgress; + + private readonly String _uri; + private readonly String _filePath; + + private readonly Aria2NetClient _aria2NetClient; + private readonly Timer _timer; + + private String _gid; + + public Aria2cDownloader(String gid, String uri, String filePath, DbSettings settings) + { + _gid = gid; + _uri = uri; + _filePath = filePath; + + _aria2NetClient = new Aria2NetClient(settings.Aria2cUrl, settings.Aria2cSecret); + + _timer = new Timer(); + + _timer.Elapsed += OnTimedEvent; + + _timer.Interval = 1000; + _timer.Enabled = false; + } + + public async Task Download() + { + var path = Path.GetDirectoryName(_filePath); + var fileName = Path.GetFileName(_filePath); + + if (_gid != null) + { + try + { + await _aria2NetClient.TellStatus(_gid); + } + catch + { + _gid = null; + } + } + + _gid ??= await _aria2NetClient.AddUri(new List + { + _uri + }, + new Dictionary + { + { + "dir", path + }, + { + "out", fileName + } + }); + + _timer.Start(); + + return _gid; + } + + public async Task Cancel() + { + _timer.Stop(); + + if (String.IsNullOrWhiteSpace(_gid)) + { + return; + } + + try + { + await _aria2NetClient.ForceRemove(_gid); + } + catch + { + // ignored + } + + try + { + await _aria2NetClient.RemoveDownloadResult(_gid); + } + catch + { + // ignored + } + } + + private async void OnTimedEvent(Object source, ElapsedEventArgs e) + { + if (_gid == null) + { + return; + } + + try + { + var status = await _aria2NetClient.TellStatus(_gid); + + if (!String.IsNullOrWhiteSpace(status.ErrorMessage) || status.Status == "error") + { + await Cancel(); + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs + { + Error = $"{status.ErrorCode}: {status.ErrorMessage}" + }); + return; + } + + if (status.Status == "complete" || status.Status == "removed") + { + await Cancel(); + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs()); + return; + } + + DownloadProgress?.Invoke(this, new DownloadProgressEventArgs + { + BytesDone = status.CompletedLength, + BytesTotal = status.TotalLength, + Speed = status.DownloadSpeed + }); + } + catch + { + await Cancel(); + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs()); + } + } + } +} diff --git a/server/RdtClient.Service/Services/Downloaders/IDownloader.cs b/server/RdtClient.Service/Services/Downloaders/IDownloader.cs new file mode 100644 index 0000000..2ca0099 --- /dev/null +++ b/server/RdtClient.Service/Services/Downloaders/IDownloader.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; + +namespace RdtClient.Service.Services.Downloaders +{ + public class DownloadCompleteEventArgs + { + public String Error { get; set; } + } + + public class DownloadProgressEventArgs + { + public Int64 Speed { get; set; } + public Int64 BytesDone { get; set; } + public Int64 BytesTotal { get; set; } + } + + public interface IDownloader + { + event EventHandler DownloadComplete; + event EventHandler DownloadProgress; + Task Download(); + Task Cancel(); + } +} diff --git a/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs b/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs new file mode 100644 index 0000000..5f53502 --- /dev/null +++ b/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs @@ -0,0 +1,128 @@ +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Downloader; +using RdtClient.Data.Models.Internal; + +namespace RdtClient.Service.Services.Downloaders +{ + public class MultiDownloader : IDownloader + { + public event EventHandler DownloadComplete; + public event EventHandler DownloadProgress; + + private readonly DownloadService _downloadService; + private readonly String _filePath; + private readonly String _uri; + + public MultiDownloader(String uri, String filePath, DbSettings settings) + { + _uri = uri; + _filePath = filePath; + + var settingTempPath = settings.TempPath; + + if (String.IsNullOrWhiteSpace(settingTempPath)) + { + settingTempPath = Path.GetTempPath(); + } + + var settingDownloadChunkCount = settings.DownloadChunkCount; + + if (settingDownloadChunkCount <= 0) + { + settingDownloadChunkCount = 1; + } + + var settingDownloadMaxSpeed = settings.DownloadMaxSpeed; + + if (settingDownloadMaxSpeed <= 0) + { + settingDownloadMaxSpeed = 0; + } + + settingDownloadMaxSpeed = settingDownloadMaxSpeed * 1024 * 1024; + + var settingProxyServer = settings.ProxyServer; + + var downloadOpt = new DownloadConfiguration + { + MaxTryAgainOnFailover = Int32.MaxValue, + ParallelDownload = settingDownloadChunkCount > 1, + ChunkCount = settingDownloadChunkCount, + Timeout = 1000, + OnTheFlyDownload = false, + BufferBlockSize = 1024 * 8, + MaximumBytesPerSecond = settingDownloadMaxSpeed, + TempDirectory = settingTempPath, + RequestConfiguration = + { + Accept = "*/*", + UserAgent = $"rdt-client", + ProtocolVersion = HttpVersion.Version11, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + KeepAlive = true, + UseDefaultCredentials = false + } + }; + + if (!String.IsNullOrWhiteSpace(settingProxyServer)) + { + downloadOpt.RequestConfiguration.Proxy = new WebProxy(new Uri(settingProxyServer), false); + } + + _downloadService = new DownloadService(downloadOpt); + + _downloadService.DownloadProgressChanged += (_, args) => + { + if (DownloadProgress == null) + { + return; + } + + DownloadProgress?.Invoke(this, + new DownloadProgressEventArgs + { + Speed = (Int64)args.BytesPerSecondSpeed, + BytesDone = args.ReceivedBytesSize, + BytesTotal = args.TotalBytesToReceive + }); + }; + + _downloadService.DownloadFileCompleted += (_, args) => + { + String error = null; + + if (args.Cancelled) + { + error = $"The download was cancelled"; + } + else if (args.Error != null) + { + error = args.Error.Message; + } + + DownloadComplete?.Invoke(this, + new DownloadCompleteEventArgs + { + Error = error + }); + }; + } + + public async Task Download() + { + await _downloadService.DownloadFileTaskAsync(_uri, _filePath); + + return null; + } + + public Task Cancel() + { + _downloadService.CancelAsync(); + + return Task.CompletedTask; + } + } +} diff --git a/server/RdtClient.Service/Services/SimpleDownloader.cs b/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs similarity index 65% rename from server/RdtClient.Service/Services/SimpleDownloader.cs rename to server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs index 50f783a..7943d22 100644 --- a/server/RdtClient.Service/Services/SimpleDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs @@ -1,27 +1,51 @@ using System; -using System.ComponentModel; using System.IO; using System.Net; using System.Threading.Tasks; -using DownloadProgressChangedEventArgs = Downloader.DownloadProgressChangedEventArgs; -namespace RdtClient.Service.Services +namespace RdtClient.Service.Services.Downloaders { - public class SimpleDownloader + public class SimpleDownloader : IDownloader { - public Int64 Speed { get; private set; } - public Int64 BytesTotal { get; private set; } - public Int64 BytesDone { get; private set; } + public event EventHandler DownloadComplete; + public event EventHandler DownloadProgress; - public event EventHandler DownloadFileCompleted; - public event EventHandler DownloadProgressChanged; + private readonly String _uri; + private readonly String _filePath; - private Boolean _cancelled = false; + private Int64 Speed { get; set; } + private Int64 BytesTotal { get; set; } + private Int64 BytesDone { get; set; } + + private Boolean _cancelled; private Int64 _bytesLastUpdate; private DateTime _nextUpdate; - public async Task Download(Uri uri, String filePath) + public SimpleDownloader(String uri, String filePath) + { + _uri = uri; + _filePath = filePath; + } + + public Task Download() + { + Task.Run(async () => + { + await StartDownloadTask(); + }); + + return null; + } + + public Task Cancel() + { + _cancelled = true; + + return Task.CompletedTask; + } + + private async Task StartDownloadTask() { try { @@ -29,7 +53,7 @@ namespace RdtClient.Service.Services _nextUpdate = DateTime.UtcNow.AddSeconds(1); // Determine the file size - var webRequest = WebRequest.Create(uri); + var webRequest = WebRequest.Create(_uri); webRequest.Method = "HEAD"; webRequest.Timeout = 5000; Int64 responseLength; @@ -45,7 +69,7 @@ namespace RdtClient.Service.Services { try { - var request = WebRequest.Create(uri); + var request = WebRequest.Create(_uri); using var response = await request.GetResponseAsync(); await using var stream = response.GetResponseStream(); @@ -55,7 +79,7 @@ namespace RdtClient.Service.Services throw new IOException("No stream"); } - await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write); + 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) @@ -78,13 +102,11 @@ namespace RdtClient.Service.Services timeout = DateTimeOffset.UtcNow.AddHours(1); - DownloadProgressChanged?.Invoke(this, new DownloadProgressChangedEventArgs(null) + DownloadProgress?.Invoke(this, new DownloadProgressEventArgs { - BytesPerSecondSpeed = Speed, - ProgressedByteSize = _bytesLastUpdate, - TotalBytesToReceive = BytesTotal, - AverageBytesPerSecondSpeed = Speed, - ReceivedBytesSize = BytesDone, + Speed = Speed, + BytesDone = BytesDone, + BytesTotal = BytesTotal }); } } @@ -111,17 +133,15 @@ namespace RdtClient.Service.Services throw new Exception($"Download timed out"); } - DownloadFileCompleted?.Invoke(this, new AsyncCompletedEventArgs(null, false, null)); + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs()); } catch (Exception ex) { - DownloadFileCompleted?.Invoke(this, new AsyncCompletedEventArgs(ex, false, null)); + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs + { + Error = ex.Message + }); } } - - public void Cancel() - { - _cancelled = true; - } } } diff --git a/server/RdtClient.Service/Services/Downloads.cs b/server/RdtClient.Service/Services/Downloads.cs index e2c3aae..aa50b49 100644 --- a/server/RdtClient.Service/Services/Downloads.cs +++ b/server/RdtClient.Service/Services/Downloads.cs @@ -74,6 +74,11 @@ namespace RdtClient.Service.Services await _downloadData.UpdateRetryCount(downloadId, retryCount); } + public async Task UpdateRemoteId(Guid downloadId, String remoteId) + { + await _downloadData.UpdateRemoteId(downloadId, remoteId); + } + public async Task DeleteForTorrent(Guid torrentId) { await _downloadData.DeleteForTorrent(torrentId); diff --git a/server/RdtClient.Service/Services/Settings.cs b/server/RdtClient.Service/Services/Settings.cs index fc33774..df44408 100644 --- a/server/RdtClient.Service/Services/Settings.cs +++ b/server/RdtClient.Service/Services/Settings.cs @@ -78,7 +78,7 @@ namespace RdtClient.Service.Services var downloadClient = new DownloadClient(download, download.Torrent, downloadPath); - await downloadClient.Start(Get); + downloadClient.Start(Get); while (!downloadClient.Finished) { diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index 45d0640..bbfc106 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -249,7 +249,14 @@ namespace RdtClient.Service.Services { Log.Debug($"Added download {download.DownloadId} to active downloads"); - await downloadClient.Start(Settings.Get); + var remoteId = await downloadClient.Start(Settings.Get); + + if (!String.IsNullOrWhiteSpace(remoteId) && download.RemoteId != remoteId) + { + Log.Debug($"Received ID {remoteId} for download {download.DownloadId}"); + + await _downloads.UpdateRemoteId(download.DownloadId, remoteId); + } Log.Debug($"Download {download.DownloadId} started"); } @@ -335,7 +342,7 @@ namespace RdtClient.Service.Services { Log.Debug($"Added unpack {download.DownloadId} to active unpacks"); - await unpackClient.Start(); + unpackClient.Start(); Log.Debug($"Unpack {download.DownloadId} started"); } diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index 808ec20..bd2045b 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -514,6 +514,18 @@ namespace RdtClient.Service.Services return torrent; } + private static String DownloadPath(Torrent torrent) + { + var settingDownloadPath = Settings.Get.DownloadPath; + + if (!String.IsNullOrWhiteSpace(torrent.Category)) + { + settingDownloadPath = Path.Combine(settingDownloadPath, torrent.Category); + } + + return settingDownloadPath; + } + private async Task Add(String rdTorrentId, String infoHash, String category, @@ -553,18 +565,6 @@ namespace RdtClient.Service.Services } } - private String DownloadPath(Torrent torrent) - { - var settingDownloadPath = Settings.Get.DownloadPath; - - if (!String.IsNullOrWhiteSpace(torrent.Category)) - { - settingDownloadPath = Path.Combine(settingDownloadPath, torrent.Category); - } - - return settingDownloadPath; - } - private async Task Update(Torrent torrent) { var originalTorrent = JsonConvert.SerializeObject(torrent, diff --git a/server/RdtClient.Service/Services/UnpackClient.cs b/server/RdtClient.Service/Services/UnpackClient.cs index a79f12d..9dda73d 100644 --- a/server/RdtClient.Service/Services/UnpackClient.cs +++ b/server/RdtClient.Service/Services/UnpackClient.cs @@ -36,7 +36,7 @@ namespace RdtClient.Service.Services _torrent = download.Torrent; } - public async Task Start() + public void Start() { BytesDone = 0; BytesTotal = 0; @@ -50,9 +50,7 @@ namespace RdtClient.Service.Services throw new Exception("Invalid download path"); } -#pragma warning disable 4014 Task.Run(async delegate -#pragma warning restore 4014 { if (!_cancelled) { diff --git a/server/RdtClient.Web/RdtClient.Web.csproj b/server/RdtClient.Web/RdtClient.Web.csproj index 23ce87d..2a9bf6c 100644 --- a/server/RdtClient.Web/RdtClient.Web.csproj +++ b/server/RdtClient.Web/RdtClient.Web.csproj @@ -26,7 +26,6 @@ -