diff --git a/client/src/app/add-new-torrent/add-new-torrent.component.html b/client/src/app/add-new-torrent/add-new-torrent.component.html index 7f7b275..99f539a 100644 --- a/client/src/app/add-new-torrent/add-new-torrent.component.html +++ b/client/src/app/add-new-torrent/add-new-torrent.component.html @@ -29,6 +29,7 @@ [(ngModel)]="magnetLink" [disabled]="saving" (blur)="checkFiles()" + (paste)="onPaste()" > @@ -79,6 +80,16 @@

The category becomes a sub-folder in your main download path.

+ +
+ +
+ +
+

+ Set the priority for this torrent where 1 is the highest. When empty it will be assigned the lowest priority. +

+
diff --git a/client/src/app/add-new-torrent/add-new-torrent.component.ts b/client/src/app/add-new-torrent/add-new-torrent.component.ts index e0c0abe..0353c96 100644 --- a/client/src/app/add-new-torrent/add-new-torrent.component.ts +++ b/client/src/app/add-new-torrent/add-new-torrent.component.ts @@ -14,6 +14,7 @@ export class AddNewTorrentComponent implements OnInit { private currentTorrentFile: string; public category: string; + public priority: number; public downloadAction: number = 0; public finishedAction: number = 0; @@ -98,7 +99,8 @@ export class AddNewTorrentComponent implements OnInit { this.downloadAction, this.finishedAction, this.downloadMinSize, - downloadManualFiles + downloadManualFiles, + this.priority ) .subscribe( () => { @@ -117,7 +119,8 @@ export class AddNewTorrentComponent implements OnInit { this.downloadAction, this.finishedAction, this.downloadMinSize, - downloadManualFiles + downloadManualFiles, + this.priority ) .subscribe( () => { @@ -134,6 +137,12 @@ export class AddNewTorrentComponent implements OnInit { } } + public onPaste(): void { + setTimeout(() => { + this.checkFiles(); + }, 100); + } + public checkFiles(): void { if (this.magnetLink && this.magnetLink === this.currentTorrentFile) { return; diff --git a/client/src/app/models/torrent.model.ts b/client/src/app/models/torrent.model.ts index b640ffb..eaad6a7 100644 --- a/client/src/app/models/torrent.model.ts +++ b/client/src/app/models/torrent.model.ts @@ -17,6 +17,7 @@ export class Torrent { public isFile: boolean; public retryCount: number; + public priority: number; public rdId: string; public rdName: string; diff --git a/client/src/app/torrent-table/torrent-table.component.html b/client/src/app/torrent-table/torrent-table.component.html index 4f37dc2..032e235 100644 --- a/client/src/app/torrent-table/torrent-table.component.html +++ b/client/src/app/torrent-table/torrent-table.component.html @@ -8,6 +8,7 @@ Name Category + Priority Files Downloads Size @@ -22,6 +23,9 @@ {{ torrent.category }} + + {{ torrent.priority }} + {{ torrent.files.length | number }} diff --git a/client/src/app/torrent.service.ts b/client/src/app/torrent.service.ts index 0c883e0..d342d72 100644 --- a/client/src/app/torrent.service.ts +++ b/client/src/app/torrent.service.ts @@ -43,7 +43,8 @@ export class TorrentService { downloadAction: number, finishedAction: number, downloadMinSize: number, - downloadManualFiles: string + downloadManualFiles: string, + priority: number ): Observable { return this.http.post(`/Api/Torrents/UploadMagnet`, { magnetLink, @@ -52,6 +53,7 @@ export class TorrentService { finishedAction, downloadMinSize, downloadManualFiles, + priority, }); } @@ -61,13 +63,14 @@ export class TorrentService { downloadAction: number, finishedAction: number, downloadMinSize: number, - downloadManualFiles: string + downloadManualFiles: string, + priority: number ): Observable { const formData: FormData = new FormData(); formData.append('file', file); formData.append( 'formData', - JSON.stringify({ category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles }) + JSON.stringify({ category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, priority }) ); return this.http.post(`/Api/Torrents/UploadFile`, formData); } @@ -104,4 +107,8 @@ export class TorrentService { public retryDownload(downloadId: string): Observable { return this.http.post(`/Api/Torrents/RetryDownload/${downloadId}`, {}); } + + public update(torrent: Torrent): Observable { + return this.http.put(`/Api/Torrents/Update`, torrent); + } } diff --git a/client/src/app/torrent/torrent.component.html b/client/src/app/torrent/torrent.component.html index 1d4b6f7..ad475f6 100644 --- a/client/src/app/torrent/torrent.component.html +++ b/client/src/app/torrent/torrent.component.html @@ -27,6 +27,9 @@
+
+ +
@@ -40,6 +43,10 @@ {{ torrent.hash }}
+
+ + {{ torrent.priority || "" }} +
{{ torrent.category || "(no category set)" }} @@ -445,3 +452,42 @@
+ + diff --git a/client/src/app/torrent/torrent.component.ts b/client/src/app/torrent/torrent.component.ts index 9d304a8..5a78b82 100644 --- a/client/src/app/torrent/torrent.component.ts +++ b/client/src/app/torrent/torrent.component.ts @@ -34,6 +34,10 @@ export class TorrentComponent implements OnInit { public downloadRetrying: boolean; public downloadRetryId: string; + public isUpdateSettingsModalActive: boolean; + public updateSettingsPriority: number; + public updating: boolean; + constructor(private activatedRoute: ActivatedRoute, private router: Router, private torrentService: TorrentService) {} ngOnInit(): void { @@ -163,4 +167,31 @@ export class TorrentComponent implements OnInit { } ); } + + public showUpdateSettingsModal(): void { + this.updateSettingsPriority = this.torrent.priority; + + this.isUpdateSettingsModalActive = true; + } + + public updateSettingsCancel(): void { + this.isUpdateSettingsModalActive = false; + } + + public updateSettingsOk(): void { + this.updating = true; + + this.torrent.priority = this.updateSettingsPriority; + + this.torrentService.update(this.torrent).subscribe( + () => { + this.isUpdateSettingsModalActive = false; + this.updating = false; + }, + () => { + this.isUpdateSettingsModalActive = false; + this.updating = false; + } + ); + } } diff --git a/server/RdtClient.Data/Data/DownloadData.cs b/server/RdtClient.Data/Data/DownloadData.cs index 6a7f566..7521f43 100644 --- a/server/RdtClient.Data/Data/DownloadData.cs +++ b/server/RdtClient.Data/Data/DownloadData.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -15,6 +16,14 @@ namespace RdtClient.Data.Data _dataContext = dataContext; } + public async Task> GetForTorrent(Guid torrentId) + { + return await _dataContext.Downloads + .AsNoTracking() + .Where(m => m.TorrentId == torrentId) + .ToListAsync(); + } + public async Task GetById(Guid downloadId) { return await _dataContext.Downloads diff --git a/server/RdtClient.Data/Data/TorrentData.cs b/server/RdtClient.Data/Data/TorrentData.cs index 4880a6e..91a1f21 100644 --- a/server/RdtClient.Data/Data/TorrentData.cs +++ b/server/RdtClient.Data/Data/TorrentData.cs @@ -27,15 +27,12 @@ namespace RdtClient.Data.Data try { - if (_torrentCache == null) - { - _torrentCache = await _dataContext.Torrents - .AsNoTracking() - .Include(m => m.Downloads) - .ToListAsync(); - } + _torrentCache ??= await _dataContext.Torrents + .AsNoTracking() + .Include(m => m.Downloads) + .ToListAsync(); - return _torrentCache.OrderByDescending(m => m.Added).ToList(); + return _torrentCache.OrderBy(m => m.Priority ?? 9999).ThenBy(m => m.Added).ToList(); } finally { @@ -91,7 +88,8 @@ namespace RdtClient.Data.Data Int32 downloadMinSize, String downloadManualFiles, String fileOrMagnetContents, - Boolean isFile) + Boolean isFile, + Int32? priority) { var torrent = new Torrent { @@ -105,7 +103,8 @@ namespace RdtClient.Data.Data DownloadMinSize = downloadMinSize, DownloadManualFiles = downloadManualFiles, FileOrMagnet = fileOrMagnetContents, - IsFile = isFile + IsFile = isFile, + Priority = priority }; await _dataContext.Torrents.AddAsync(torrent); @@ -148,6 +147,22 @@ namespace RdtClient.Data.Data await VoidCache(); } + public async Task Update(Torrent torrent) + { + var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrent.TorrentId); + + if (dbTorrent == null) + { + return; + } + + dbTorrent.Priority = torrent.Priority; + + await _dataContext.SaveChangesAsync(); + + await VoidCache(); + } + public async Task UpdateCategory(Guid torrentId, String category) { var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); @@ -212,6 +227,22 @@ namespace RdtClient.Data.Data await VoidCache(); } + public async Task UpdatePriority(Guid torrentId, Int32? priority) + { + var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); + + if (dbTorrent == null) + { + return; + } + + dbTorrent.Priority = priority; + + await _dataContext.SaveChangesAsync(); + + await VoidCache(); + } + public async Task Delete(Guid torrentId) { var dbTorrent = await _dataContext.Torrents.FirstOrDefaultAsync(m => m.TorrentId == torrentId); diff --git a/server/RdtClient.Data/Migrations/20211024161405_Torrents_Add_Priority.Designer.cs b/server/RdtClient.Data/Migrations/20211024161405_Torrents_Add_Priority.Designer.cs new file mode 100644 index 0000000..b981741 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20211024161405_Torrents_Add_Priority.Designer.cs @@ -0,0 +1,442 @@ +// +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("20211024161405_Torrents_Add_Priority")] + partial class Torrents_Add_Priority + { + 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("Priority") + .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.Property("RetryCount") + .HasColumnType("INTEGER"); + + 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/20211024161405_Torrents_Add_Priority.cs b/server/RdtClient.Data/Migrations/20211024161405_Torrents_Add_Priority.cs new file mode 100644 index 0000000..c7d6f51 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20211024161405_Torrents_Add_Priority.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace RdtClient.Data.Migrations +{ + public partial class Torrents_Add_Priority : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Priority", + table: "Torrents", + type: "INTEGER", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Priority", + table: "Torrents"); + } + } +} diff --git a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs index bf25f46..bc062cb 100644 --- a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs +++ b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs @@ -318,6 +318,9 @@ namespace RdtClient.Data.Migrations b.Property("IsFile") .HasColumnType("INTEGER"); + b.Property("Priority") + .HasColumnType("INTEGER"); + b.Property("RdAdded") .HasColumnType("TEXT"); diff --git a/server/RdtClient.Data/Models/Data/Torrent.cs b/server/RdtClient.Data/Models/Data/Torrent.cs index 6775574..4b114e9 100644 --- a/server/RdtClient.Data/Models/Data/Torrent.cs +++ b/server/RdtClient.Data/Models/Data/Torrent.cs @@ -28,7 +28,8 @@ namespace RdtClient.Data.Models.Data public String FileOrMagnet { get; set; } public Boolean IsFile { get; set; } - + + public Int32? Priority { get; set; } public Int32 RetryCount { get; set; } [InverseProperty("Torrent")] diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs index 646b0df..6278375 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -84,9 +84,31 @@ namespace RdtClient.Service.Services } } - public void Cancel() + public async Task Cancel() { - _downloader?.Cancel(); + if (_downloader == null) + { + return; + } + await _downloader.Cancel(); + } + + public async Task Pause() + { + if (_downloader == null) + { + return; + } + await _downloader.Pause(); + } + + public async Task Resume() + { + if (_downloader == null) + { + return; + } + await _downloader.Resume(); } } } diff --git a/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs b/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs index b87b945..f67f877 100644 --- a/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/Aria2cDownloader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; using System.Threading.Tasks; using System.Timers; using Aria2NET; @@ -29,7 +30,12 @@ namespace RdtClient.Service.Services.Downloaders _uri = uri; _filePath = filePath; - _aria2NetClient = new Aria2NetClient(settings.Aria2cUrl, settings.Aria2cSecret); + var httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(1) + }; + + _aria2NetClient = new Aria2NetClient(settings.Aria2cUrl, settings.Aria2cSecret, httpClient); _timer = new Timer(); @@ -126,6 +132,40 @@ namespace RdtClient.Service.Services.Downloaders } } + public async Task Pause() + { + if (String.IsNullOrWhiteSpace(_gid)) + { + return; + } + + try + { + await _aria2NetClient.Pause(_gid); + } + catch + { + // ignored + } + } + + public async Task Resume() + { + if (String.IsNullOrWhiteSpace(_gid)) + { + return; + } + + try + { + await _aria2NetClient.Unpause(_gid); + } + catch + { + // ignored + } + } + private async void OnTimedEvent(Object source, ElapsedEventArgs e) { if (_gid == null) diff --git a/server/RdtClient.Service/Services/Downloaders/IDownloader.cs b/server/RdtClient.Service/Services/Downloaders/IDownloader.cs index 2ca0099..e740b70 100644 --- a/server/RdtClient.Service/Services/Downloaders/IDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/IDownloader.cs @@ -21,5 +21,7 @@ namespace RdtClient.Service.Services.Downloaders event EventHandler DownloadProgress; Task Download(); Task Cancel(); + Task Pause(); + Task Resume(); } } diff --git a/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs b/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs index 5f53502..8ce5a16 100644 --- a/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/MultiDownloader.cs @@ -124,5 +124,15 @@ namespace RdtClient.Service.Services.Downloaders return Task.CompletedTask; } + + public Task Pause() + { + return Task.CompletedTask; + } + + public Task Resume() + { + return Task.CompletedTask; + } } } diff --git a/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs b/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs index 6ed1ddf..f0101e0 100644 --- a/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/SimpleDownloader.cs @@ -143,5 +143,15 @@ namespace RdtClient.Service.Services.Downloaders }); } } + + public Task Pause() + { + return Task.CompletedTask; + } + + public Task Resume() + { + return Task.CompletedTask; + } } } diff --git a/server/RdtClient.Service/Services/Downloads.cs b/server/RdtClient.Service/Services/Downloads.cs index aa50b49..61007ac 100644 --- a/server/RdtClient.Service/Services/Downloads.cs +++ b/server/RdtClient.Service/Services/Downloads.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using RdtClient.Data.Data; using Download = RdtClient.Data.Models.Data.Download; @@ -13,7 +14,12 @@ namespace RdtClient.Service.Services { _downloadData = downloadData; } - + + public async Task> GetForTorrent(Guid torrentId) + { + return await _downloadData.GetForTorrent(torrentId); + } + public async Task GetById(Guid downloadId) { return await _downloadData.GetById(downloadId); diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index 995f8fc..fbb6be8 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -14,12 +14,14 @@ namespace RdtClient.Service.Services private readonly Authentication _authentication; private readonly Settings _settings; private readonly Torrents _torrents; + private readonly Downloads _downloads; - public QBittorrent(Settings settings, Authentication authentication, Torrents torrents) + public QBittorrent(Settings settings, Authentication authentication, Torrents torrents, Downloads downloads) { _settings = settings; _authentication = authentication; _torrents = torrents; + _downloads = downloads; } public async Task AuthLogin(String userName, String password) @@ -291,7 +293,7 @@ namespace RdtClient.Service.Services Upspeed = speed }; - if (torrent.Completed.HasValue) + if (torrent.Completed.HasValue && torrent.Downloads.All(m => m.Completed.HasValue)) { // Indicates completed torrent and not seeding anymore. result.State = "pausedUP"; @@ -329,7 +331,7 @@ namespace RdtClient.Service.Services return null; } - foreach (var file in torrent.Files) + foreach (var file in torrent.Files.Where(m => m.Selected)) { var result = new TorrentFileItem { @@ -421,18 +423,18 @@ namespace RdtClient.Service.Services await _torrents.Delete(torrent.TorrentId, true, true, deleteFiles); } - public async Task TorrentsAddMagnet(String magnetLink, String category) + public async Task TorrentsAddMagnet(String magnetLink, String category, Int32? priority) { var downloadAction = Settings.Get.OnlyDownloadAvailableFiles == 1 ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll; - await _torrents.UploadMagnet(magnetLink, category, downloadAction, TorrentFinishedAction.None, Settings.Get.MinFileSize, null); + await _torrents.UploadMagnet(magnetLink, category, downloadAction, TorrentFinishedAction.None, Settings.Get.MinFileSize, null, priority); } - public async Task TorrentsAddFile(Byte[] fileBytes, String category) + public async Task TorrentsAddFile(Byte[] fileBytes, String category, Int32? priority) { var downloadAction = Settings.Get.OnlyDownloadAvailableFiles == 1 ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll; - await _torrents.UploadFile(fileBytes, category, downloadAction, TorrentFinishedAction.None, Settings.Get.MinFileSize, null); + await _torrents.UploadFile(fileBytes, category, downloadAction, TorrentFinishedAction.None, Settings.Get.MinFileSize, null, priority); } public async Task TorrentsSetCategory(String hash, String category) @@ -515,5 +517,50 @@ namespace RdtClient.Service.Services await _settings.UpdateString("Categories", categoriesSetting); } + + public async Task TorrentsTopPrio(String hash) + { + await _torrents.UpdatePriority(hash, 1); + } + + public async Task TorrentPause(String hash) + { + var torrent = await _torrents.GetByHash(hash); + + if (torrent == null) + { + return; + } + + var downloads = await _downloads.GetForTorrent(torrent.TorrentId); + + foreach (var download in downloads) + { + if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) + { + downloadClient.Pause(); + } + } + } + + public async Task TorrentResume(String hash) + { + var torrent = await _torrents.GetByHash(hash); + + if (torrent == null) + { + return; + } + + var downloads = await _downloads.GetForTorrent(torrent.TorrentId); + + foreach (var download in downloads) + { + if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) + { + downloadClient.Resume(); + } + } + } } } diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index d1b0638..d35217a 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -194,7 +194,7 @@ namespace RdtClient.Service.Services _nextUpdate = DateTime.UtcNow.AddSeconds(updateTime); - await _torrents.Update(); + await _torrents.UpdateRdData(); // Re-get torrents to account for updated info torrents = await _torrents.Get(); @@ -297,6 +297,7 @@ namespace RdtClient.Service.Services if (download.Link == null) { await _downloads.UpdateError(download.DownloadId, "Download Link cannot be null"); + await _downloads.UpdateCompleted(download.DownloadId, DateTimeOffset.UtcNow); continue; } diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index 8664bbd..8fcd8d8 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -83,7 +83,7 @@ namespace RdtClient.Service.Services if (torrent != null) { - await Update(torrent); + await UpdateRdData(torrent); } return torrent; @@ -106,7 +106,8 @@ namespace RdtClient.Service.Services TorrentDownloadAction downloadAction, TorrentFinishedAction finishedAction, Int32 downloadMinSize, - String downloadManualFiles) + String downloadManualFiles, + Int32? priority) { MagnetLink magnet; @@ -121,7 +122,7 @@ namespace RdtClient.Service.Services var rdTorrent = await GetRdNetClient().Torrents.AddMagnetAsync(magnetLink); - return await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, magnetLink, false); + return await Add(rdTorrent.Id, magnet.InfoHash.ToHex(), category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, magnetLink, false, priority); } public async Task UploadFile(Byte[] bytes, @@ -129,7 +130,8 @@ namespace RdtClient.Service.Services TorrentDownloadAction downloadAction, TorrentFinishedAction finishedAction, Int32 downloadMinSize, - String downloadManualFiles) + String downloadManualFiles, + Int32? priority) { MonoTorrent.Torrent torrent; @@ -146,7 +148,7 @@ namespace RdtClient.Service.Services var rdTorrent = await GetRdNetClient().Torrents.AddFileAsync(bytes); - return await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, fileAsBase64, true); + return await Add(rdTorrent.Id, torrent.InfoHash.ToHex(), category, downloadAction, finishedAction, downloadMinSize, downloadManualFiles, fileAsBase64, true, priority); } public async Task> GetAvailableFiles(String hash) @@ -373,7 +375,7 @@ namespace RdtClient.Service.Services return profile; } - public async Task Update() + public async Task UpdateRdData() { await RealDebridUpdateLock.WaitAsync(); @@ -392,7 +394,7 @@ namespace RdtClient.Service.Services continue; } - await Update(torrent); + await UpdateRdData(torrent); } foreach (var torrent in torrents) @@ -457,7 +459,7 @@ namespace RdtClient.Service.Services { var bytes = Convert.FromBase64String(torrent.FileOrMagnet); - newTorrent = await UploadFile(bytes, torrent.Category, torrent.DownloadAction, torrent.FinishedAction, torrent.DownloadMinSize, torrent.DownloadManualFiles); + newTorrent = await UploadFile(bytes, torrent.Category, torrent.DownloadAction, torrent.FinishedAction, torrent.DownloadMinSize, torrent.DownloadManualFiles, torrent.Priority); } else { @@ -466,7 +468,8 @@ namespace RdtClient.Service.Services torrent.DownloadAction, torrent.FinishedAction, torrent.DownloadMinSize, - torrent.DownloadManualFiles); + torrent.DownloadManualFiles, + torrent.Priority); } await _torrentData.UpdateRetryCount(newTorrent.TorrentId, newRetryCount); @@ -527,6 +530,18 @@ namespace RdtClient.Service.Services await _torrentData.UpdateFilesSelected(torrentId, datetime); } + public async Task UpdatePriority(String hash, Int32 priority) + { + var torrent = await _torrentData.GetByHash(hash); + + if (torrent == null) + { + return; + } + + await _torrentData.UpdatePriority(torrent.TorrentId, priority); + } + public async Task GetById(Guid torrentId) { var torrent = await _torrentData.GetById(torrentId); @@ -536,7 +551,7 @@ namespace RdtClient.Service.Services return null; } - await Update(torrent); + await UpdateRdData(torrent); foreach (var download in torrent.Downloads) { @@ -577,7 +592,8 @@ namespace RdtClient.Service.Services Int32 downloadMinSize, String downloadManualFiles, String fileOrMagnetContents, - Boolean isFile) + Boolean isFile, + Int32? priority) { await RealDebridUpdateLock.WaitAsync(); @@ -598,9 +614,10 @@ namespace RdtClient.Service.Services downloadMinSize, downloadManualFiles, fileOrMagnetContents, - isFile); + isFile, + priority); - await Update(newTorrent); + await UpdateRdData(newTorrent); return newTorrent; } @@ -609,8 +626,13 @@ namespace RdtClient.Service.Services RealDebridUpdateLock.Release(); } } + + public async Task Update(Torrent torrent) + { + await _torrentData.Update(torrent); + } - private async Task Update(Torrent torrent) + private async Task UpdateRdData(Torrent torrent) { var originalTorrent = JsonConvert.SerializeObject(torrent, new JsonSerializerSettings diff --git a/server/RdtClient.Web/Controllers/QBittorrentController.cs b/server/RdtClient.Web/Controllers/QBittorrentController.cs index fb7f421..1619bec 100644 --- a/server/RdtClient.Web/Controllers/QBittorrentController.cs +++ b/server/RdtClient.Web/Controllers/QBittorrentController.cs @@ -190,21 +190,49 @@ namespace RdtClient.Web.Controllers [Authorize] [Route("torrents/pause")] [HttpGet] - [HttpPost] - public ActionResult TorrentsPause() + public async Task TorrentsPause([FromQuery] QBTorrentsHashesRequest request) { + var hashes = request.Hashes.Split("|"); + + foreach (var hash in hashes) + { + await _qBittorrent.TorrentPause(hash); + } + return Ok(); } + [Authorize] + [Route("torrents/topPrio")] + [HttpPost] + public async Task TorrentsPausePost([FromForm] QBTorrentsHashesRequest request) + { + return await TorrentsPause(request); + } + [Authorize] [Route("torrents/resume")] [HttpGet] - [HttpPost] - public ActionResult TorrentsResume() + public async Task TorrentsResume([FromQuery] QBTorrentsHashesRequest request) { + var hashes = request.Hashes.Split("|"); + + foreach (var hash in hashes) + { + await _qBittorrent.TorrentResume(hash); + } + return Ok(); } + [Authorize] + [Route("torrents/topPrio")] + [HttpPost] + public async Task TorrentsResumePost([FromForm] QBTorrentsHashesRequest request) + { + return await TorrentsResume(request); + } + [Authorize] [Route("torrents/setShareLimits")] [HttpGet] @@ -248,13 +276,13 @@ namespace RdtClient.Web.Controllers { if (url.StartsWith("magnet")) { - await _qBittorrent.TorrentsAddMagnet(url.Trim(), request.Category); + await _qBittorrent.TorrentsAddMagnet(url.Trim(), request.Category, null); } else if (url.StartsWith("http")) { var httpClient = new HttpClient(); var result = await httpClient.GetByteArrayAsync(url); - await _qBittorrent.TorrentsAddFile(result, request.Category); + await _qBittorrent.TorrentsAddFile(result, request.Category, null); } else { @@ -279,7 +307,7 @@ namespace RdtClient.Web.Controllers await file.CopyToAsync(target); var fileBytes = target.ToArray(); - await _qBittorrent.TorrentsAddFile(fileBytes, request.Category); + await _qBittorrent.TorrentsAddFile(fileBytes, request.Category, request.Priority); } } @@ -370,6 +398,29 @@ namespace RdtClient.Web.Controllers { return Ok(); } + + [Authorize] + [Route("torrents/topPrio")] + [HttpGet] + public async Task TorrentsTopPrio([FromQuery] QBTorrentsHashesRequest request) + { + var hashes = request.Hashes.Split("|"); + + foreach (var hash in hashes) + { + await _qBittorrent.TorrentsTopPrio(hash); + } + + return Ok(); + } + + [Authorize] + [Route("torrents/topPrio")] + [HttpPost] + public async Task TorrentsTopPrioPost([FromForm] QBTorrentsHashesRequest request) + { + return await TorrentsTopPrio(request); + } } public class QBAuthLoginRequest @@ -393,6 +444,7 @@ namespace RdtClient.Web.Controllers { public String Urls { get; set; } public String Category { get; set; } + public Int32? Priority { get; set; } } public class QBTorrentsSetCategoryRequest @@ -410,4 +462,9 @@ namespace RdtClient.Web.Controllers { public String Categories { get; set; } } + + public class QBTorrentsHashesRequest + { + public String Hashes { get; set; } + } } \ No newline at end of file diff --git a/server/RdtClient.Web/Controllers/TorrentsController.cs b/server/RdtClient.Web/Controllers/TorrentsController.cs index 21d48b3..62b734c 100644 --- a/server/RdtClient.Web/Controllers/TorrentsController.cs +++ b/server/RdtClient.Web/Controllers/TorrentsController.cs @@ -91,7 +91,7 @@ namespace RdtClient.Web.Controllers var bytes = memoryStream.ToArray(); - await _torrents.UploadFile(bytes, formData.Category, formData.DownloadAction, formData.FinishedAction, formData.DownloadMinSize, formData.DownloadManualFiles); + await _torrents.UploadFile(bytes, formData.Category, formData.DownloadAction, formData.FinishedAction, formData.DownloadMinSize, formData.DownloadManualFiles, formData.Priority); return Ok(); } @@ -105,7 +105,8 @@ namespace RdtClient.Web.Controllers request.DownloadAction, request.FinishedAction, request.DownloadMinSize, - request.DownloadManualFiles); + request.DownloadManualFiles, + request.Priority); return Ok(); } @@ -171,6 +172,15 @@ namespace RdtClient.Web.Controllers return Ok(); } + + [HttpPut] + [Route("Update")] + public async Task Update([FromBody] Torrent torrent) + { + await _torrents.Update(torrent); + + return Ok(); + } } public class TorrentControllerUploadFileRequest @@ -180,6 +190,7 @@ namespace RdtClient.Web.Controllers public TorrentFinishedAction FinishedAction { get; set; } public Int32 DownloadMinSize { get; set; } public String DownloadManualFiles { get; set; } + public Int32? Priority { get; set; } } public class TorrentControllerUploadMagnetRequest @@ -190,6 +201,7 @@ namespace RdtClient.Web.Controllers public TorrentFinishedAction FinishedAction { get; set; } public Int32 DownloadMinSize { get; set; } public String DownloadManualFiles { get; set; } + public Int32? Priority { get; set; } } public class TorrentControllerDeleteRequest