mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-05-18 19:45:36 -04:00
Add failed import safeguard for private torrents when download client is unavailable (#347)
This commit is contained in:
@@ -25,6 +25,7 @@ public partial class DelugeService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.IsPrivate = download.Private;
|
||||
result.Found = true;
|
||||
|
||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||
@@ -32,8 +33,6 @@ public partial class DelugeService
|
||||
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.IsPrivate = download.Private;
|
||||
|
||||
var malwareBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ public partial class DelugeService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Found = true;
|
||||
result.IsPrivate = download.Private;
|
||||
result.Found = true;
|
||||
|
||||
// Create ITorrentItem wrapper for consistent interface usage
|
||||
var torrentItem = new DelugeItem(download);
|
||||
|
||||
@@ -23,8 +23,6 @@ public partial class QBitService
|
||||
_logger.LogDebug("failed to find torrent {hash} in the {name} download client", hash, _downloadClientConfig.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Found = true;
|
||||
|
||||
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(hash);
|
||||
|
||||
@@ -48,6 +46,7 @@ public partial class QBitService
|
||||
&& boolValue;
|
||||
|
||||
result.IsPrivate = isPrivate;
|
||||
result.Found = true;
|
||||
|
||||
var malwareBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ public partial class QBitService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Found = true;
|
||||
|
||||
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(hash);
|
||||
|
||||
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
||||
@@ -38,6 +36,8 @@ public partial class QBitService
|
||||
result.IsPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) &&
|
||||
bool.TryParse(dictValue?.ToString(), out bool boolValue)
|
||||
&& boolValue;
|
||||
|
||||
result.Found = true;
|
||||
|
||||
// Create ITorrentItem wrapper for consistent interface usage
|
||||
var torrentItem = new QBitItem(download, trackers, result.IsPrivate);
|
||||
|
||||
@@ -23,8 +23,6 @@ public partial class TransmissionService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Found = true;
|
||||
|
||||
if (download.Files is null)
|
||||
{
|
||||
_logger.LogDebug("torrent {hash} has no files", hash);
|
||||
@@ -39,6 +37,7 @@ public partial class TransmissionService
|
||||
|
||||
bool isPrivate = download.IsPrivate ?? false;
|
||||
result.IsPrivate = isPrivate;
|
||||
result.Found = true;
|
||||
|
||||
var malwareBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
|
||||
|
||||
|
||||
@@ -25,10 +25,9 @@ public partial class TransmissionService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Found = true;
|
||||
|
||||
bool isPrivate = download.IsPrivate ?? false;
|
||||
result.IsPrivate = isPrivate;
|
||||
result.Found = true;
|
||||
|
||||
// Create ITorrentItem wrapper for consistent interface usage
|
||||
var torrentItem = new TransmissionItem(download);
|
||||
|
||||
@@ -26,10 +26,9 @@ public partial class UTorrentService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Found = true;
|
||||
|
||||
var properties = await _client.GetTorrentPropertiesAsync(hash);
|
||||
result.IsPrivate = properties.IsPrivate;
|
||||
result.Found = true;
|
||||
|
||||
if (ignoredDownloads.Count > 0 &&
|
||||
(download.ShouldIgnore(ignoredDownloads) || properties.TrackerList.Any(x => x.ShouldIgnore(ignoredDownloads))))
|
||||
|
||||
@@ -25,10 +25,9 @@ public partial class UTorrentService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Found = true;
|
||||
|
||||
var properties = await _client.GetTorrentPropertiesAsync(hash);
|
||||
result.IsPrivate = properties.IsPrivate;
|
||||
result.Found = true;
|
||||
|
||||
// Create ITorrentItem wrapper for consistent interface usage
|
||||
var torrentItem = new UTorrentItemWrapper(download, properties);
|
||||
|
||||
@@ -237,7 +237,7 @@ public abstract class GenericHandler : IHandler
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadServices.Count == 0)
|
||||
if (downloadServices.Count is 0)
|
||||
{
|
||||
_logger.LogDebug("No valid download clients found");
|
||||
}
|
||||
@@ -246,11 +246,6 @@ public abstract class GenericHandler : IHandler
|
||||
_logger.LogDebug("Initialized {count} download clients", downloadServices.Count);
|
||||
}
|
||||
|
||||
foreach (var downloadService in downloadServices)
|
||||
{
|
||||
await downloadService.LoginAsync();
|
||||
}
|
||||
|
||||
return downloadServices;
|
||||
}
|
||||
}
|
||||
@@ -146,8 +146,9 @@ public sealed class MalwareBlocker : GenericHandler
|
||||
ContextProvider.Set(nameof(QueueRecord), record);
|
||||
|
||||
BlockFilesResult result = new();
|
||||
bool isTorrent = record.Protocol.Contains("torrent", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
if (record.Protocol is "torrent")
|
||||
if (isTorrent)
|
||||
{
|
||||
var torrentClients = downloadServices
|
||||
.Where(x => x.ClientConfig.Type is DownloadClientType.Torrent)
|
||||
|
||||
@@ -7,6 +7,7 @@ using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
@@ -94,6 +95,10 @@ public sealed class QueueCleaner : GenericHandler
|
||||
ContextProvider.Set(nameof(InstanceType), instanceType);
|
||||
|
||||
IReadOnlyList<IDownloadService> downloadServices = await GetInitializedDownloadServicesAsync();
|
||||
bool hasEnabledTorrentClients = ContextProvider
|
||||
.Get<List<DownloadClientConfig>>(nameof(DownloadClientConfig))
|
||||
.Where(x => x.Type == DownloadClientType.Torrent)
|
||||
.Any(x => x.Enabled);
|
||||
|
||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||
{
|
||||
@@ -135,8 +140,9 @@ public sealed class QueueCleaner : GenericHandler
|
||||
ContextProvider.Set(nameof(QueueRecord), record);
|
||||
|
||||
DownloadCheckResult downloadCheckResult = new();
|
||||
bool isTorrent = record.Protocol.Contains("torrent", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
if (record.Protocol.Contains("torrent", StringComparison.InvariantCultureIgnoreCase))
|
||||
if (isTorrent)
|
||||
{
|
||||
var torrentClients = downloadServices
|
||||
.Where(x => x.ClientConfig.Type is DownloadClientType.Torrent)
|
||||
@@ -170,16 +176,12 @@ public sealed class QueueCleaner : GenericHandler
|
||||
_logger.LogWarning("Download not found in any torrent client | {title}", record.Title);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No torrent clients enabled");
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadCheckResult.ShouldRemove)
|
||||
{
|
||||
bool removeFromClient = !downloadCheckResult.IsPrivate || downloadCheckResult.DeleteFromClient;
|
||||
|
||||
|
||||
await PublishQueueItemRemoveRequest(
|
||||
downloadRemovalKey,
|
||||
instanceType,
|
||||
@@ -189,11 +191,18 @@ public sealed class QueueCleaner : GenericHandler
|
||||
removeFromClient,
|
||||
downloadCheckResult.DeleteReason
|
||||
);
|
||||
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// failed import check
|
||||
|
||||
// Skip failed import check if torrent is not found in client and skipIfNotFoundInClient is enabled
|
||||
if (isTorrent && hasEnabledTorrentClients && !downloadCheckResult.Found && queueCleanerConfig.FailedImport.SkipIfNotFoundInClient)
|
||||
{
|
||||
_logger.LogInformation("skip | torrent not found in any torrent client | {title}", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Failed import check
|
||||
bool shouldRemoveFromArr = await arrClient
|
||||
.ShouldRemoveFromQueue(instanceType, record, downloadCheckResult.IsPrivate, instance.ArrConfig.FailedImportMaxStrikes);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFailedImportPrivateSafeguardOption : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "failed_import_skip_if_not_found_in_client",
|
||||
table: "queue_cleaner_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "failed_import_skip_if_not_found_in_client",
|
||||
table: "queue_cleaner_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -717,6 +717,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("failed_import_patterns");
|
||||
|
||||
b1.Property<bool>("SkipIfNotFoundInClient")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_skip_if_not_found_in_client");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
|
||||
@@ -9,11 +9,13 @@ namespace Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
public sealed record FailedImportConfig
|
||||
{
|
||||
public ushort MaxStrikes { get; init; }
|
||||
|
||||
|
||||
public bool IgnorePrivate { get; init; }
|
||||
|
||||
|
||||
public bool DeletePrivate { get; init; }
|
||||
|
||||
public bool SkipIfNotFoundInClient { get; init; } = true;
|
||||
|
||||
public IReadOnlyList<string> Patterns { get; init; } = [];
|
||||
|
||||
public PatternMode PatternMode { get; init; } = PatternMode.Include;
|
||||
|
||||
@@ -198,8 +198,8 @@
|
||||
|
||||
<div class="field-row" formGroupName="failedImport">
|
||||
<label class="field-label">
|
||||
<i class="pi pi-question-circle field-info-icon"
|
||||
(click)="openFieldDocs('failedImport.deletePrivate')"
|
||||
<i class="pi pi-question-circle field-info-icon"
|
||||
(click)="openFieldDocs('failedImport.deletePrivate')"
|
||||
title="Click for documentation"></i>
|
||||
Delete Private from client
|
||||
</label>
|
||||
@@ -209,6 +209,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row" formGroupName="failedImport">
|
||||
<label class="field-label">
|
||||
<i class="pi pi-question-circle field-info-icon"
|
||||
(click)="openFieldDocs('failedImport.skipIfNotFoundInClient')"
|
||||
title="Click for documentation"></i>
|
||||
Skip If Not Found In Client
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="skipIfNotFoundInClient" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">Skip failed import check for torrents not found in any enabled torrent client</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row" formGroupName="failedImport">
|
||||
<label class="field-label">
|
||||
<i class="pi pi-question-circle field-info-icon"
|
||||
|
||||
@@ -557,6 +557,7 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
maxStrikes: [0, [Validators.required, Validators.min(0), Validators.max(5000)]],
|
||||
ignorePrivate: [{ value: false, disabled: true }],
|
||||
deletePrivate: [{ value: false, disabled: true }],
|
||||
skipIfNotFoundInClient: [{ value: true, disabled: true }],
|
||||
patterns: [{ value: [], disabled: true }],
|
||||
patternMode: [{ value: PatternMode.Include, disabled: true }],
|
||||
}),
|
||||
@@ -1020,13 +1021,14 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
|
||||
if (enable) {
|
||||
this.queueCleanerForm.get("failedImport")?.get("ignorePrivate")?.enable(options);
|
||||
this.queueCleanerForm.get("failedImport")?.get("skipIfNotFoundInClient")?.enable(options);
|
||||
this.queueCleanerForm.get("failedImport")?.get("patterns")?.enable(options);
|
||||
this.queueCleanerForm.get("failedImport")?.get("patternMode")?.enable(options);
|
||||
|
||||
|
||||
// Only enable deletePrivate if ignorePrivate is false
|
||||
const ignorePrivate = this.queueCleanerForm.get("failedImport.ignorePrivate")?.value || false;
|
||||
const deletePrivateControl = this.queueCleanerForm.get("failedImport.deletePrivate");
|
||||
|
||||
|
||||
if (!ignorePrivate && deletePrivateControl) {
|
||||
deletePrivateControl.enable(options);
|
||||
} else if (deletePrivateControl) {
|
||||
@@ -1035,6 +1037,7 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
} else {
|
||||
this.queueCleanerForm.get("failedImport")?.get("ignorePrivate")?.disable(options);
|
||||
this.queueCleanerForm.get("failedImport")?.get("deletePrivate")?.disable(options);
|
||||
this.queueCleanerForm.get("failedImport")?.get("skipIfNotFoundInClient")?.disable(options);
|
||||
this.queueCleanerForm.get("failedImport")?.get("patterns")?.disable(options);
|
||||
this.queueCleanerForm.get("failedImport")?.get("patternMode")?.disable(options);
|
||||
}
|
||||
@@ -1068,6 +1071,7 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
maxStrikes: formValue.failedImport?.maxStrikes || 0,
|
||||
ignorePrivate: formValue.failedImport?.ignorePrivate || false,
|
||||
deletePrivate: formValue.failedImport?.deletePrivate || false,
|
||||
skipIfNotFoundInClient: formValue.failedImport?.skipIfNotFoundInClient ?? true,
|
||||
patterns: formValue.failedImport?.patterns || [],
|
||||
patternMode: formValue.failedImport?.patternMode || PatternMode.Include,
|
||||
},
|
||||
@@ -1134,6 +1138,7 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
maxStrikes: 0,
|
||||
ignorePrivate: false,
|
||||
deletePrivate: false,
|
||||
skipIfNotFoundInClient: true,
|
||||
patterns: [],
|
||||
patternMode: PatternMode.Include,
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface FailedImportConfig {
|
||||
maxStrikes: number;
|
||||
ignorePrivate: boolean;
|
||||
deletePrivate: boolean;
|
||||
skipIfNotFoundInClient: boolean;
|
||||
patterns: string[];
|
||||
patternMode?: PatternMode;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user