Add first Aria2c support.

This commit is contained in:
Roger Far 2021-10-09 20:35:03 -06:00
parent b8b50ca55a
commit c728e17bb3
23 changed files with 940 additions and 195 deletions

View file

@ -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.

View file

@ -75,6 +75,7 @@
<select [(ngModel)]="settingDownloadClient">
<option value="Simple">Simple Downloader</option>
<option value="MultiPart">Multi Part Downloader</option>
<option value="Aria2c">Aria2c</option>
</select>
</div>
<p class="help">
@ -112,6 +113,7 @@
parallel downloading will be done.
</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Download speed (in MB/s)</label>
<div class="control">
@ -119,6 +121,7 @@
</div>
<p class="help">Maximum download speed in Megabytes per second. When set to 0 unlimited speed is used.</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'MultiPart'">
<label class="label">Proxy Server</label>
<div class="control">
@ -126,6 +129,27 @@
</div>
<p class="help">Address of a proxy server.</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'Aria2c'">
<label class="label">Aria2c URL</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="aria2cUrl" />
</div>
<p class="help">
This is the URL to your Aria2c instance. It must end in /jsonrpc. A common URL is
http://192.168.10.2:6800/jsonrpc.
</p>
</div>
<div class="field" *ngIf="settingDownloadClient === 'Aria2c'">
<label class="label">Aria2c Secret</label>
<div class="control">
<input class="input" type="text" maxlength="1000" [(ngModel)]="aria2cSecret" />
</div>
<p class="help">
The secret of your Aria2c instance. Optional.
</p>
</div>
</div>
<div *ngIf="activeTab === 2">

View file

@ -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(

View file

@ -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 = ""
}
};

View file

@ -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

View file

@ -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"),
};
}

View file

@ -0,0 +1,436 @@
// <auto-generated />
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<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");
});
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");
});
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");
});
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");
});
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");
});
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");
});
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");
});
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")
.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>("Type")
.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>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("FilesSelected")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.HasColumnType("TEXT");
b.Property<bool>("IsFile")
.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.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
}
}
}

View file

@ -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<string>(
name: "RemoteId",
table: "Downloads",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RemoteId",
table: "Downloads");
}
}
}

View file

@ -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<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("RemoteId")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");

View file

@ -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; }

View file

@ -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; }
}
}

View file

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aria2.NET" Version="1.0.1" />
<PackageReference Include="Downloader" Version="2.2.9" />
<PackageReference Include="MonoTorrent" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />

View file

@ -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<String> 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();
}
}
}

View file

@ -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<DownloadCompleteEventArgs> DownloadComplete;
public event EventHandler<DownloadProgressEventArgs> 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<String> 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<String>
{
_uri
},
new Dictionary<String, Object>
{
{
"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());
}
}
}
}

View file

@ -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<DownloadCompleteEventArgs> DownloadComplete;
event EventHandler<DownloadProgressEventArgs> DownloadProgress;
Task<String> Download();
Task Cancel();
}
}

View file

@ -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<DownloadCompleteEventArgs> DownloadComplete;
public event EventHandler<DownloadProgressEventArgs> 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<String> Download()
{
await _downloadService.DownloadFileTaskAsync(_uri, _filePath);
return null;
}
public Task Cancel()
{
_downloadService.CancelAsync();
return Task.CompletedTask;
}
}
}

View file

@ -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<DownloadCompleteEventArgs> DownloadComplete;
public event EventHandler<DownloadProgressEventArgs> DownloadProgress;
public event EventHandler<AsyncCompletedEventArgs> DownloadFileCompleted;
public event EventHandler<DownloadProgressChangedEventArgs> 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<String> 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;
}
}
}

View file

@ -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);

View file

@ -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)
{

View file

@ -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");
}

View file

@ -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,

View file

@ -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)
{

View file

@ -26,7 +26,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aria2.NET" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.10" />