Added lifetime setting to automatically expire torrents after a certain amount of time.

This commit is contained in:
Roger Far 2022-02-06 16:03:12 -07:00
parent 25f8c70b17
commit 6a2533aa3b
19 changed files with 582 additions and 7 deletions

View file

@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2.0.6] - 2022-02-06
### Added
- Added setting to automatically delete torrents in the state of error after a certain amount of time.
- Added lifetime setting to automatically expire torrents after a certain amount of time.
## [2.0.5] - 2022-01-11
### Changed

View file

@ -131,13 +131,23 @@
<div class="field">
<label class="label">Delete download when in error</label>
<div class="control">
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="torrentDeleteOnError" />
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentDeleteOnError" />
</div>
<p class="help">
When a download has been in error for this many minutes, delete it from the provider and the client. 0 to
disable.
</p>
</div>
<div class="field">
<label class="label">Torrent maximum lifetime</label>
<div class="control">
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="torrentLifetime" />
</div>
<p class="help">
The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the torrent
is completed and has downloads, the lifetime setting will not apply. 0 to disable.
</p>
</div>
</div>
<div fxFlex *ngIf="provider === 'RealDebrid'">
<div class="field">

View file

@ -27,6 +27,7 @@ export class AddNewTorrentComponent implements OnInit {
public downloadRetryAttempts: number = 3;
public torrentRetryAttempts: number = 1;
public torrentDeleteOnError: number = 0;
public torrentLifetime: number = 0;
public availableFiles: TorrentFileAvailability[];
public downloadFiles: { [key: string]: boolean } = {};

View file

@ -20,6 +20,7 @@ export class Torrent {
public downloadRetryAttempts: number;
public torrentRetryAttempts: number;
public deleteOnError: number;
public lifetime: number;
public priority: number;
public error: string;

View file

@ -291,12 +291,23 @@
<div class="field">
<label class="label">Delete download when in error</label>
<div class="control">
<input class="input" type="number" max="1000" min="0" step="1" [(ngModel)]="settingDeleteOnError" />
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="settingDeleteOnError" />
</div>
<p class="help">
When a download has been in error for this many minutes, delete it from the provider and the client. 0 to disable.
</p>
</div>
<div class="field">
<label class="label">Torrent maximum lifetime</label>
<div class="control">
<input class="input" type="number" max="100000" min="0" step="1" [(ngModel)]="settingTorrentLifetime" />
</div>
<p class="help">
The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the torrent
is completed and has downloads, the lifetime setting will not apply. 0 to disable.
</p>
</div>
</div>
<div *ngIf="activeTab === 3">

View file

@ -46,6 +46,7 @@ export class SettingsComponent implements OnInit {
public settingDownloadRetryAttempts: number;
public settingTorrentRetryAttempts: number;
public settingDeleteOnError: number;
public settingTorrentLifetime: number;
constructor(private settingsService: SettingsService) {}
@ -80,6 +81,7 @@ export class SettingsComponent implements OnInit {
this.settingDownloadRetryAttempts = parseInt(this.getSetting(results, 'DownloadRetryAttempts'), 10);
this.settingTorrentRetryAttempts = parseInt(this.getSetting(results, 'TorrentRetryAttempts'), 10);
this.settingDeleteOnError = parseInt(this.getSetting(results, 'DeleteOnError'), 10);
this.settingTorrentLifetime = parseInt(this.getSetting(results, 'TorrentLifetime'), 10);
},
(err) => {
this.error = err.error;
@ -176,6 +178,10 @@ export class SettingsComponent implements OnInit {
settingId: 'DeleteOnError',
value: (this.settingDeleteOnError ?? 0).toString(),
},
{
settingId: 'TorrentLifetime',
value: (this.settingTorrentLifetime ?? 0).toString(),
},
];
this.settingsService.update(settings).subscribe(

View file

@ -511,6 +511,23 @@
disable.
</p>
</div>
<div class="field">
<label class="label">Torrent maximum lifetime</label>
<div class="control">
<input
class="input"
type="number"
max="100000"
min="0"
step="1"
[(ngModel)]="updateSettingsTorrentLifetime"
/>
</div>
<p class="help">
The maximum lifetime of a torrent in minutes. When this time has passed, mark the torrent as error. If the
torrent is completed and has downloads, the lifetime setting will not apply. 0 to disable.
</p>
</div>
</section>
<footer class="modal-card-foot">
<button
@ -519,7 +536,7 @@
[disabled]="updating"
[ngClass]="{ 'is-loading': updating }"
>
Retry
Save
</button>
<button
class="button"

View file

@ -39,6 +39,7 @@ export class TorrentComponent implements OnInit {
public updateSettingsDownloadRetryAttempts: number;
public updateSettingsTorrentRetryAttempts: number;
public updateSettingsDeleteOnError: number;
public updateSettingsTorrentLifetime: number;
public updating: boolean;
@ -177,6 +178,7 @@ export class TorrentComponent implements OnInit {
this.updateSettingsDownloadRetryAttempts = this.torrent.downloadRetryAttempts;
this.updateSettingsTorrentRetryAttempts = this.torrent.torrentRetryAttempts;
this.updateSettingsDeleteOnError = this.torrent.deleteOnError;
this.updateSettingsTorrentLifetime = this.torrent.lifetime;
this.isUpdateSettingsModalActive = true;
}
@ -192,6 +194,7 @@ export class TorrentComponent implements OnInit {
this.torrent.downloadRetryAttempts = this.updateSettingsDownloadRetryAttempts;
this.torrent.torrentRetryAttempts = this.updateSettingsTorrentRetryAttempts;
this.torrent.deleteOnError = this.updateSettingsDeleteOnError;
this.torrent.lifetime = this.updateSettingsTorrentLifetime;
this.torrentService.update(this.torrent).subscribe(
() => {

View file

@ -65,6 +65,12 @@ namespace RdtClient.Data.Data
Value = "0"
},
new Setting
{
SettingId = "TorrentLifetime",
Type = "Int32",
Value = "0"
},
new Setting
{
SettingId = "RealDebridApiKey",
Type = "String",

View file

@ -70,6 +70,7 @@ namespace RdtClient.Data.Data
DownloadRetryAttempts = GetInt32("DownloadRetryAttempts"),
TorrentRetryAttempts = GetInt32("TorrentRetryAttempts"),
DeleteOnError = GetInt32("DeleteOnError"),
TorrentLifetime = GetInt32("TorrentLifetime"),
};
}

View file

@ -101,7 +101,8 @@ namespace RdtClient.Data.Data
Priority = torrent.Priority,
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
DeleteOnError = torrent.DeleteOnError
DeleteOnError = torrent.DeleteOnError,
Lifetime = torrent.Lifetime
};
await _dataContext.Torrents.AddAsync(newTorrent);
@ -157,6 +158,7 @@ namespace RdtClient.Data.Data
dbTorrent.DownloadRetryAttempts = torrent.DownloadRetryAttempts;
dbTorrent.TorrentRetryAttempts = torrent.TorrentRetryAttempts;
dbTorrent.DeleteOnError = torrent.DeleteOnError;
dbTorrent.Lifetime = torrent.Lifetime;
await _dataContext.SaveChangesAsync();

View file

@ -0,0 +1,461 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RdtClient.Data.Data;
#nullable disable
namespace RdtClient.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20220206225658_Torrents_Add_Lifetime")]
partial class Torrents_Add_Lifetime
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.Property<Guid>("DownloadId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<string>("Path")
.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>("DeleteOnError")
.HasColumnType("INTEGER");
b.Property<int>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<int>("DownloadRetryAttempts")
.HasColumnType("INTEGER");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("FilesSelected")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.HasColumnType("TEXT");
b.Property<bool>("IsFile")
.HasColumnType("INTEGER");
b.Property<int>("Lifetime")
.HasColumnType("INTEGER");
b.Property<int?>("Priority")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RdAdded")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("RdEnded")
.HasColumnType("TEXT");
b.Property<string>("RdFiles")
.HasColumnType("TEXT");
b.Property<string>("RdHost")
.HasColumnType("TEXT");
b.Property<string>("RdId")
.HasColumnType("TEXT");
b.Property<string>("RdName")
.HasColumnType("TEXT");
b.Property<long>("RdProgress")
.HasColumnType("INTEGER");
b.Property<long?>("RdSeeders")
.HasColumnType("INTEGER");
b.Property<long>("RdSize")
.HasColumnType("INTEGER");
b.Property<long?>("RdSpeed")
.HasColumnType("INTEGER");
b.Property<long>("RdSplit")
.HasColumnType("INTEGER");
b.Property<int>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Retry")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<int>("TorrentRetryAttempts")
.HasColumnType("INTEGER");
b.HasKey("TorrentId");
b.ToTable("Torrents");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
.WithMany("Downloads")
.HasForeignKey("TorrentId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Torrent");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Navigation("Downloads");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RdtClient.Data.Migrations
{
public partial class Torrents_Add_Lifetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Lifetime",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Lifetime",
table: "Torrents");
}
}
}

View file

@ -328,6 +328,9 @@ namespace RdtClient.Data.Migrations
b.Property<bool>("IsFile")
.HasColumnType("INTEGER");
b.Property<int>("Lifetime")
.HasColumnType("INTEGER");
b.Property<int?>("Priority")
.HasColumnType("INTEGER");

View file

@ -35,6 +35,7 @@ namespace RdtClient.Data.Models.Data
public Int32 DownloadRetryAttempts { get; set; }
public Int32 TorrentRetryAttempts { get; set; }
public Int32 DeleteOnError { get; set; }
public Int32 Lifetime { get; set; }
public String Error { get; set; }

View file

@ -26,5 +26,6 @@ namespace RdtClient.Data.Models.Internal
public Int32 DownloadRetryAttempts { get; set; }
public Int32 TorrentRetryAttempts { get; set; }
public Int32 DeleteOnError { get; set; }
public Int32 TorrentLifetime { get; set; }
}
}

View file

@ -444,6 +444,7 @@ namespace RdtClient.Service.Services
TorrentRetryAttempts = Settings.Get.TorrentRetryAttempts,
DownloadRetryAttempts = Settings.Get.DownloadRetryAttempts,
DeleteOnError = Settings.Get.DeleteOnError,
Lifetime = Settings.Get.TorrentLifetime,
Priority = priority
};
@ -461,6 +462,7 @@ namespace RdtClient.Service.Services
TorrentRetryAttempts = Settings.Get.TorrentRetryAttempts,
DownloadRetryAttempts = Settings.Get.DownloadRetryAttempts,
DeleteOnError = Settings.Get.DeleteOnError,
Lifetime = Settings.Get.TorrentLifetime,
Priority = priority
};

View file

@ -248,12 +248,12 @@ namespace RdtClient.Service.Services
{
try
{
_logger.LogDebug($"Retrying torrent {torrent.RetryCount}/{torrent.TorrentRetryAttempts}", torrent);
Log($"Retrying torrent {torrent.RetryCount}/{torrent.TorrentRetryAttempts}", torrent);
if (torrent.RetryCount > torrent.TorrentRetryAttempts)
{
await _torrents.UpdateRetry(torrent.TorrentId, null, torrent.RetryCount);
_logger.LogDebug($"Torrent reach max retry count", torrent);
Log($"Torrent reach max retry count");
continue;
}
@ -267,15 +267,36 @@ namespace RdtClient.Service.Services
}
// Process torrent errors
foreach (var torrent in torrents.Where(m => m.Error != null && m.Completed != null && m.DeleteOnError > 0))
foreach (var torrent in torrents.Where(m => m.Error != null && m.DeleteOnError > 0))
{
if (torrent.Completed == null)
{
continue;
}
if (torrent.Completed.Value.AddMinutes(torrent.DeleteOnError) > DateTime.UtcNow)
{
continue;
}
Log($"Removing torrent because it has been {torrent.DeleteOnError} minutes in the error state", torrent);
await _torrents.Delete(torrent.TorrentId, true, true, true);
}
// Process torrent lifetime
foreach (var torrent in torrents.Where(m => m.Downloads.Count == 0 && m.Completed == null && m.Lifetime > 0))
{
if (torrent.Added.AddMinutes(torrent.Lifetime) > DateTime.UtcNow)
{
continue;
}
Log($"Torrent has reached its {torrent.Lifetime} minutes lifetime, marking as error", torrent);
await _torrents.UpdateRetry(torrent.TorrentId, null, 99);
await _torrents.UpdateError(torrent.TorrentId, "Torrent expired in RealDebrid Client");
}
torrents = torrents.Where(m => m.Completed == null).ToList();

View file

@ -367,6 +367,7 @@ namespace RdtClient.Service.Services
TorrentRetryAttempts = 0,
DownloadRetryAttempts = Settings.Get.DownloadRetryAttempts,
DeleteOnError = Settings.Get.DeleteOnError,
Lifetime = Settings.Get.TorrentLifetime,
Priority = 0,
RdId = rdTorrent.Id
};