mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-05-18 11:34:59 -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;
|
||||
}
|
||||
|
||||
@@ -127,6 +127,19 @@ This setting needs a download client to be configured.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Skip If Not Found In Client"
|
||||
icon="🔍"
|
||||
>
|
||||
|
||||
When enabled, torrents that are not found in any enabled torrent client will skip the failed import check. This is useful when the connection between Cleanuparr and your download client is temporarily broken or when the download client is down, preventing accidental removal of private torrents when their privacy status cannot be determined.
|
||||
|
||||
<Important>
|
||||
This setting needs a download client to be configured.
|
||||
</Important>
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Pattern Mode"
|
||||
icon="🎭"
|
||||
|
||||
Reference in New Issue
Block a user