Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve stalled and failed imports #37

Merged
merged 10 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/ISSUE_TEMPLATE/1-bug.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Bug report
description: File a bug report if something is not working right.
title: "[BUG]: "
title: "[BUG] "
labels: ["bug"]
body:
- type: markdown
Expand Down Expand Up @@ -40,6 +40,7 @@ body:
- Windows
- Linux
- MacOS
- Unraid
validations:
required: true
- type: dropdown
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/2-feature.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Feature request
description: File a feature request.
title: "[FEATURE]: "
title: "[FEATURE] "
labels: ["enhancement"]
body:
- type: markdown
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/3-help.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Help request
description: Ask a question to receive help.
title: "[HELP]: "
title: "[HELP] "
labels: ["question"]
body:
- type: markdown
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ This tool is actively developed and still a work in progress. Join the Discord s

1. Set `QUEUECLEANER__ENABLED` to `true`.
2. Set `QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES` to a desired value.
3. Set `DOWNLOAD_CLIENT` to `none`.
3. Optionally set failed import message patterns to ignore using `QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__<NUMBER>`.
4. Set `DOWNLOAD_CLIENT` to `none`.

**No other action involving a download client would work (e.g. content blocking, removing stalled downloads etc.).**
**No other action involving a download client would work (e.g. content blocking, removing stalled downloads, excluding private trackers).**

## Usage

Expand All @@ -99,7 +100,11 @@ services:
- QUEUECLEANER__ENABLED=true
- QUEUECLEANER__RUNSEQUENTIALLY=true
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=false
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=title mismatch
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1=manual import required
- QUEUECLEANER__STALLED_MAX_STRIKES=5
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=false

- CONTENTBLOCKER__ENABLED=true
- CONTENTBLOCKER__BLACKLIST__ENABLED=true
Expand Down Expand Up @@ -155,7 +160,10 @@ services:
| QUEUECLEANER__ENABLED | No | Enable or disable the queue cleaner | true |
| QUEUECLEANER__RUNSEQUENTIALLY | No | If set to true, the queue cleaner will run after the content blocker instead of running in parallel, streamlining the cleaning process | true |
| QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES | No | After how many strikes should a failed import be removed<br>0 means never | 0 |
| QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE | No | Whether to ignore failed imports from private trackers | false |
| QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0 | No | First pattern to look for when an import is failed<br>If the specified message pattern is found, the item is skipped | empty |
| QUEUECLEANER__STALLED_MAX_STRIKES | No | After how many strikes should a stalled download be removed<br>0 means never | 0 |
| QUEUECLEANER__STALLED_IGNORE_PRIVATE | No | Whether to ignore stalled downloads from private trackers | false |
|||||
| CONTENTBLOCKER__ENABLED | No | Enable or disable the content blocker | false |
| CONTENTBLOCKER__BLACKLIST__ENABLED | Yes if content blocker is enabled and whitelist is not enabled | Enable or disable the blacklist | false |
Expand Down Expand Up @@ -203,6 +211,10 @@ regex:<ANY_REGEX> // regex that needs to be marked at the start of the line wi
SONARR__INSTANCES__<NUMBER>__URL
SONARR__INSTANCES__<NUMBER>__APIKEY
```
6. Multiple failed import patterns can be specified using this format, where `<NUMBER>` starts from 0:
```
QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__<NUMBER>
```

#

Expand Down
9 changes: 9 additions & 0 deletions code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ public sealed record QueueCleanerConfig : IJobConfig
[ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")]
public ushort ImportFailedMaxStrikes { get; init; }

[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PRIVATE")]
public bool ImportFailedIgnorePrivate { get; init; }

[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PATTERNS")]
public List<string>? ImportFailedIgnorePatterns { get; init; }

[ConfigurationKeyName("STALLED_MAX_STRIKES")]
public ushort StalledMaxStrikes { get; init; }

[ConfigurationKeyName("STALLED_IGNORE_PRIVATE")]
public bool StalledIgnorePrivate { get; init; }

public void Validate()
{
Expand Down
5 changes: 3 additions & 2 deletions code/Domain/Models/Arr/Queue/QueueRecord.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
namespace Domain.Models.Arr.Queue;
namespace Domain.Models.Arr.Queue;

public record QueueRecord
public sealed record QueueRecord
{
public int SeriesId { get; init; }
public int EpisodeId { get; init; }
public int SeasonNumber { get; init; }
public int MovieId { get; init; }
public required string Title { get; init; }
public string Status { get; init; }

Check warning on line 10 in code/Domain/Models/Arr/Queue/QueueRecord.cs

View workflow job for this annotation

GitHub Actions / release / build

Non-nullable property 'Status' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string TrackedDownloadStatus { get; init; }

Check warning on line 11 in code/Domain/Models/Arr/Queue/QueueRecord.cs

View workflow job for this annotation

GitHub Actions / release / build

Non-nullable property 'TrackedDownloadStatus' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string TrackedDownloadState { get; init; }

Check warning on line 12 in code/Domain/Models/Arr/Queue/QueueRecord.cs

View workflow job for this annotation

GitHub Actions / release / build

Non-nullable property 'TrackedDownloadState' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public List<TrackedDownloadStatusMessage>? StatusMessages { get; init; }
public required string DownloadId { get; init; }
public required string Protocol { get; init; }
public required int Id { get; init; }
Expand Down
8 changes: 8 additions & 0 deletions code/Domain/Models/Arr/Queue/TrackedDownloadStatusMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Domain.Models.Arr.Queue;

public sealed record TrackedDownloadStatusMessage
{
public string Title { get; set; }

Check warning on line 5 in code/Domain/Models/Arr/Queue/TrackedDownloadStatusMessage.cs

View workflow job for this annotation

GitHub Actions / release / build

Non-nullable property 'Title' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

public List<string>? Messages { get; set; }
}
10 changes: 6 additions & 4 deletions code/Domain/Models/Deluge/Response/TorrentStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

public sealed record TorrentStatus
{
public string? Hash { get; set; }
public string? Hash { get; init; }

public string? State { get; set; }
public string? State { get; init; }

public string? Name { get; set; }
public string? Name { get; init; }

public ulong Eta { get; set; }
public ulong Eta { get; init; }

public bool Private { get; init; }
}
7 changes: 6 additions & 1 deletion code/Executable/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
"Enabled": true,
"RunSequentially": true,
"IMPORT_FAILED_MAX_STRIKES": 5,
"STALLED_MAX_STRIKES": 5
"IMPORT_FAILED_IGNORE_PRIVATE": true,
"IMPORT_FAILED_IGNORE_PATTERNS": [
"file is a sample"
],
"STALLED_MAX_STRIKES": 5,
"STALLED_IGNORE_PRIVATE": true
},
"DOWNLOAD_CLIENT": "qbittorrent",
"qBittorrent": {
Expand Down
5 changes: 4 additions & 1 deletion code/Executable/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
"Enabled": true,
"RunSequentially": true,
"IMPORT_FAILED_MAX_STRIKES": 5,
"STALLED_MAX_STRIKES": 5
"IMPORT_FAILED_IGNORE_PRIVATE": false,
"IMPORT_FAILED_IGNORE_PATTERNS": [],
"STALLED_MAX_STRIKES": 5,
"STALLED_IGNORE_PRIVATE": false
},
"DOWNLOAD_CLIENT": "qbittorrent",
"qBittorrent": {
Expand Down
45 changes: 43 additions & 2 deletions code/Infrastructure/Verticals/Arr/ArrClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,30 @@ public virtual async Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrI
return queueResponse;
}

public virtual bool ShouldRemoveFromQueue(QueueRecord record)
public virtual bool ShouldRemoveFromQueue(QueueRecord record, bool isPrivateDownload)
{
if (_queueCleanerConfig.ImportFailedIgnorePrivate && isPrivateDownload)
{
// ignore private trackers
_logger.LogDebug("skip failed import check | download is private | {name}", record.Title);
return false;
}

bool hasWarn() => record.TrackedDownloadStatus
.Equals("warning", StringComparison.InvariantCultureIgnoreCase);
bool isImportBlocked() => record.TrackedDownloadState
.Equals("importBlocked", StringComparison.InvariantCultureIgnoreCase);
bool isImportPending() => record.TrackedDownloadState
.Equals("importPending", StringComparison.InvariantCultureIgnoreCase);

if (hasWarn() && (isImportBlocked() || isImportPending()))
{
if (HasIgnoredPatterns(record))
{
_logger.LogDebug("skip failed import check | contains ignored pattern | {name}", record.Title);
return false;
}

return _striker.StrikeAndCheckLimit(
record.DownloadId,
record.Title,
Expand Down Expand Up @@ -134,4 +147,32 @@ protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
{
request.Headers.Add("x-api-key", apiKey);
}

private bool HasIgnoredPatterns(QueueRecord record)
{
if (_queueCleanerConfig.ImportFailedIgnorePatterns?.Count is null or 0)
{
// no patterns are configured
return false;
}

if (record.StatusMessages?.Count is null or 0)
{
// no status message found
return false;
}

HashSet<string> messages = record.StatusMessages
.SelectMany(x => x.Messages ?? Enumerable.Empty<string>())
.ToHashSet();
record.StatusMessages.Select(x => x.Title)
.ToList()
.ForEach(x => messages.Add(x));

return messages.Any(
m => _queueCleanerConfig.ImportFailedIgnorePatterns.Any(
p => !string.IsNullOrWhiteSpace(p.Trim()) && m.Contains(p, StringComparison.InvariantCultureIgnoreCase)
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,19 @@ public override async Task LoginAsync()
await _client.LoginAsync();
}

public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
{
hash = hash.ToLowerInvariant();

DelugeContents? contents = null;
RemoveResult result = new();

TorrentStatus? status = await GetTorrentStatus(hash);

if (status?.Hash is null)
{
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
return false;
return result;
}

try
Expand All @@ -63,7 +64,10 @@ public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
}
});

return shouldRemove || IsItemStuckAndShouldRemove(status);
result.ShouldRemove = shouldRemove || IsItemStuckAndShouldRemove(status);
result.IsPrivate = status.Private;

return result;
}

public override async Task BlockUnwantedFilesAsync(string hash)
Expand Down Expand Up @@ -128,6 +132,18 @@ public override async Task BlockUnwantedFilesAsync(string hash)

private bool IsItemStuckAndShouldRemove(TorrentStatus status)
{
if (_queueCleanerConfig.StalledMaxStrikes is 0)
{
return false;
}

if (_queueCleanerConfig.StalledIgnorePrivate && status.Private)
{
// ignore private trackers
_logger.LogDebug("skip stalled check | download is private | {name}", status.Name);
return false;
}

if (status.State is null || !status.State.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase))
{
return false;
Expand All @@ -146,7 +162,7 @@ private bool IsItemStuckAndShouldRemove(TorrentStatus status)
return await _client.SendRequest<TorrentStatus?>(
"web.get_torrent_status",
hash,
new[] { "hash", "state", "name", "eta" }
new[] { "hash", "state", "name", "eta", "private" }
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Striker striker

public abstract Task LoginAsync();

public abstract Task<bool> ShouldRemoveFromArrQueueAsync(string hash);
public abstract Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash);

public abstract Task BlockUnwantedFilesAsync(string hash);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public override Task LoginAsync()
return Task.CompletedTask;
}

public override Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
public override Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
{
throw new NotImplementedException();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public interface IDownloadService : IDisposable
{
public Task LoginAsync();

public Task<bool> ShouldRemoveFromArrQueueAsync(string hash);
public Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash);

public Task BlockUnwantedFilesAsync(string hash);
}
Loading
Loading