Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 058507ac39 | |||
| f0dc51f10b | |||
| c7ad1c5ee6 | |||
| d7913ae2b8 |
@@ -1,6 +1,6 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: File a bug report if something is not working right.
|
description: File a bug report if something is not working right.
|
||||||
title: "[BUG]: "
|
title: "[BUG] "
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
@@ -40,6 +40,7 @@ body:
|
|||||||
- Windows
|
- Windows
|
||||||
- Linux
|
- Linux
|
||||||
- MacOS
|
- MacOS
|
||||||
|
- Unraid
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Feature request
|
name: Feature request
|
||||||
description: File a feature request.
|
description: File a feature request.
|
||||||
title: "[FEATURE]: "
|
title: "[FEATURE] "
|
||||||
labels: ["enhancement"]
|
labels: ["enhancement"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Help request
|
name: Help request
|
||||||
description: Ask a question to receive help.
|
description: Ask a question to receive help.
|
||||||
title: "[HELP]: "
|
title: "[HELP] "
|
||||||
labels: ["question"]
|
labels: ["question"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
@@ -63,12 +63,21 @@ This tool is actively developed and still a work in progress. Join the Discord s
|
|||||||
|
|
||||||
## Using cleanuperr's blocklist (works with all supported download clients)
|
## Using cleanuperr's blocklist (works with all supported download clients)
|
||||||
|
|
||||||
1. Set both `QUEUECLEANER_ENABLED` and `CONTENTBLOCKER_ENABLED` to `true` in your environment variables.
|
1. Set both `QUEUECLEANER__ENABLED` and `CONTENTBLOCKER_ENABLED` to `true` in your environment variables.
|
||||||
2. Configure and enable either a **blacklist** or a **whitelist** as described in the [Environment variables](#Environment-variables) section.
|
2. Configure and enable either a **blacklist** or a **whitelist** as described in the [Environment variables](#Environment-variables) section.
|
||||||
3. Once configured, cleanuperr will perform the following tasks:
|
3. Once configured, cleanuperr will perform the following tasks:
|
||||||
- Execute the **content blocker** job, as explained in the [How it works](#how-it-works) section.
|
- Execute the **content blocker** job, as explained in the [How it works](#how-it-works) section.
|
||||||
- Execute the **queue cleaner** job, as explained in the [How it works](#how-it-works) section.
|
- Execute the **queue cleaner** job, as explained in the [How it works](#how-it-works) section.
|
||||||
|
|
||||||
|
## Using cleanuperr just for failed *arr imports (works for Usenet users as well)
|
||||||
|
|
||||||
|
1. Set `QUEUECLEANER__ENABLED` to `true`.
|
||||||
|
2. Set `QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES` to a desired value.
|
||||||
|
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, excluding private trackers).**
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Docker compose yaml
|
### Docker compose yaml
|
||||||
@@ -91,9 +100,14 @@ services:
|
|||||||
- QUEUECLEANER__ENABLED=true
|
- QUEUECLEANER__ENABLED=true
|
||||||
- QUEUECLEANER__RUNSEQUENTIALLY=true
|
- QUEUECLEANER__RUNSEQUENTIALLY=true
|
||||||
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
|
- 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_MAX_STRIKES=5
|
||||||
|
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=false
|
||||||
|
|
||||||
- CONTENTBLOCKER__ENABLED=true
|
- CONTENTBLOCKER__ENABLED=true
|
||||||
|
- CONTENTBLOCKER__IGNORE_PRIVATE=true
|
||||||
- CONTENTBLOCKER__BLACKLIST__ENABLED=true
|
- CONTENTBLOCKER__BLACKLIST__ENABLED=true
|
||||||
- CONTENTBLOCKER__BLACKLIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
- CONTENTBLOCKER__BLACKLIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
||||||
# OR
|
# OR
|
||||||
@@ -113,6 +127,8 @@ services:
|
|||||||
# - TRANSMISSION__URL=http://localhost:9091
|
# - TRANSMISSION__URL=http://localhost:9091
|
||||||
# - TRANSMISSION__USERNAME=test
|
# - TRANSMISSION__USERNAME=test
|
||||||
# - TRANSMISSION__PASSWORD=testing
|
# - TRANSMISSION__PASSWORD=testing
|
||||||
|
# OR
|
||||||
|
# - DOWNLOAD_CLIENT=none
|
||||||
|
|
||||||
- SONARR__ENABLED=true
|
- SONARR__ENABLED=true
|
||||||
- SONARR__SEARCHTYPE=Episode
|
- SONARR__SEARCHTYPE=Episode
|
||||||
@@ -145,15 +161,19 @@ services:
|
|||||||
| QUEUECLEANER__ENABLED | No | Enable or disable the queue cleaner | true |
|
| 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__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_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_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__ENABLED | No | Enable or disable the content blocker | false |
|
||||||
|
| CONTENTBLOCKER__IGNORE_PRIVATE | No | Whether to ignore downloads from private trackers | false |
|
||||||
| CONTENTBLOCKER__BLACKLIST__ENABLED | Yes if content blocker is enabled and whitelist is not enabled | Enable or disable the blacklist | false |
|
| CONTENTBLOCKER__BLACKLIST__ENABLED | Yes if content blocker is enabled and whitelist is not enabled | Enable or disable the blacklist | false |
|
||||||
| CONTENTBLOCKER__BLACKLIST__PATH | Yes if blacklist is enabled | Path to the blacklist (local file or url)<br>Needs to be json compatible | empty |
|
| CONTENTBLOCKER__BLACKLIST__PATH | Yes if blacklist is enabled | Path to the blacklist (local file or url)<br>Needs to be json compatible | empty |
|
||||||
| CONTENTBLOCKER__WHITELIST__ENABLED | Yes if content blocker is enabled and blacklist is not enabled | Enable or disable the whitelist | false |
|
| CONTENTBLOCKER__WHITELIST__ENABLED | Yes if content blocker is enabled and blacklist is not enabled | Enable or disable the whitelist | false |
|
||||||
| CONTENTBLOCKER__WHITELIST__PATH | Yes if whitelist is enabled | Path to the whitelist (local file or url)<br>Needs to be json compatible | empty |
|
| CONTENTBLOCKER__WHITELIST__PATH | Yes if whitelist is enabled | Path to the whitelist (local file or url)<br>Needs to be json compatible | empty |
|
||||||
|||||
|
|||||
|
||||||
| DOWNLOAD_CLIENT | No | Download client that is used by *arrs<br>Can be `qbittorrent`, `deluge` or `transmission` | `qbittorrent` |
|
| DOWNLOAD_CLIENT | No | Download client that is used by *arrs<br>Can be `qbittorrent`, `deluge`, `transmission` or `none` | `qbittorrent` |
|
||||||
| QBITTORRENT__URL | No | qBittorrent instance url | http://localhost:8112 |
|
| QBITTORRENT__URL | No | qBittorrent instance url | http://localhost:8112 |
|
||||||
| QBITTORRENT__USERNAME | No | qBittorrent user | empty |
|
| QBITTORRENT__USERNAME | No | qBittorrent user | empty |
|
||||||
| QBITTORRENT__PASSWORD | No | qBittorrent password | empty |
|
| QBITTORRENT__PASSWORD | No | qBittorrent password | empty |
|
||||||
@@ -193,6 +213,10 @@ regex:<ANY_REGEX> // regex that needs to be marked at the start of the line wi
|
|||||||
SONARR__INSTANCES__<NUMBER>__URL
|
SONARR__INSTANCES__<NUMBER>__URL
|
||||||
SONARR__INSTANCES__<NUMBER>__APIKEY
|
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>
|
||||||
|
```
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Common.Configuration.ContentBlocker;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Common.Configuration.ContentBlocker;
|
||||||
|
|
||||||
public sealed record ContentBlockerConfig : IJobConfig
|
public sealed record ContentBlockerConfig : IJobConfig
|
||||||
{
|
{
|
||||||
@@ -6,6 +8,9 @@ public sealed record ContentBlockerConfig : IJobConfig
|
|||||||
|
|
||||||
public required bool Enabled { get; init; }
|
public required bool Enabled { get; init; }
|
||||||
|
|
||||||
|
[ConfigurationKeyName("IGNORE_PRIVATE")]
|
||||||
|
public bool IgnorePrivate { get; init; }
|
||||||
|
|
||||||
public PatternConfig? Blacklist { get; init; }
|
public PatternConfig? Blacklist { get; init; }
|
||||||
|
|
||||||
public PatternConfig? Whitelist { get; init; }
|
public PatternConfig? Whitelist { get; init; }
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Common.Configuration.DownloadClient;
|
||||||
|
|
||||||
|
public sealed record DownloadClientConfig
|
||||||
|
{
|
||||||
|
[ConfigurationKeyName("DOWNLOAD_CLIENT")]
|
||||||
|
public Enums.DownloadClient DownloadClient { get; init; } = Enums.DownloadClient.QBittorrent;
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Common.Configuration;
|
|
||||||
|
|
||||||
public static class EnvironmentVariables
|
|
||||||
{
|
|
||||||
public const string DownloadClient = "DOWNLOAD_CLIENT";
|
|
||||||
}
|
|
||||||
@@ -13,8 +13,17 @@ public sealed record QueueCleanerConfig : IJobConfig
|
|||||||
[ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")]
|
[ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")]
|
||||||
public ushort ImportFailedMaxStrikes { get; init; }
|
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")]
|
[ConfigurationKeyName("STALLED_MAX_STRIKES")]
|
||||||
public ushort StalledMaxStrikes { get; init; }
|
public ushort StalledMaxStrikes { get; init; }
|
||||||
|
|
||||||
|
[ConfigurationKeyName("STALLED_IGNORE_PRIVATE")]
|
||||||
|
public bool StalledIgnorePrivate { get; init; }
|
||||||
|
|
||||||
public void Validate()
|
public void Validate()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
namespace Domain.Enums;
|
namespace Common.Enums;
|
||||||
|
|
||||||
public enum DownloadClient
|
public enum DownloadClient
|
||||||
{
|
{
|
||||||
QBittorrent,
|
QBittorrent,
|
||||||
Deluge,
|
Deluge,
|
||||||
Transmission
|
Transmission,
|
||||||
|
None
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Domain.Models.Arr.Queue;
|
namespace Domain.Models.Arr.Queue;
|
||||||
|
|
||||||
public record QueueRecord
|
public sealed record QueueRecord
|
||||||
{
|
{
|
||||||
public int SeriesId { get; init; }
|
public int SeriesId { get; init; }
|
||||||
public int EpisodeId { get; init; }
|
public int EpisodeId { get; init; }
|
||||||
@@ -10,6 +10,7 @@ public record QueueRecord
|
|||||||
public string Status { get; init; }
|
public string Status { get; init; }
|
||||||
public string TrackedDownloadStatus { get; init; }
|
public string TrackedDownloadStatus { get; init; }
|
||||||
public string TrackedDownloadState { get; init; }
|
public string TrackedDownloadState { get; init; }
|
||||||
|
public List<TrackedDownloadStatusMessage>? StatusMessages { get; init; }
|
||||||
public required string DownloadId { get; init; }
|
public required string DownloadId { get; init; }
|
||||||
public required string Protocol { get; init; }
|
public required string Protocol { get; init; }
|
||||||
public required int Id { get; init; }
|
public required int Id { get; init; }
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Domain.Models.Arr.Queue;
|
||||||
|
|
||||||
|
public sealed record TrackedDownloadStatusMessage
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
public List<string>? Messages { get; set; }
|
||||||
|
}
|
||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
public sealed record TorrentStatus
|
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; }
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ public static class ConfigurationDI
|
|||||||
services
|
services
|
||||||
.Configure<QueueCleanerConfig>(configuration.GetSection(QueueCleanerConfig.SectionName))
|
.Configure<QueueCleanerConfig>(configuration.GetSection(QueueCleanerConfig.SectionName))
|
||||||
.Configure<ContentBlockerConfig>(configuration.GetSection(ContentBlockerConfig.SectionName))
|
.Configure<ContentBlockerConfig>(configuration.GetSection(ContentBlockerConfig.SectionName))
|
||||||
|
.Configure<DownloadClientConfig>(configuration)
|
||||||
.Configure<QBitConfig>(configuration.GetSection(QBitConfig.SectionName))
|
.Configure<QBitConfig>(configuration.GetSection(QBitConfig.SectionName))
|
||||||
.Configure<DelugeConfig>(configuration.GetSection(DelugeConfig.SectionName))
|
.Configure<DelugeConfig>(configuration.GetSection(DelugeConfig.SectionName))
|
||||||
.Configure<TransmissionConfig>(configuration.GetSection(TransmissionConfig.SectionName))
|
.Configure<TransmissionConfig>(configuration.GetSection(TransmissionConfig.SectionName))
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public static class ServicesDI
|
|||||||
.AddTransient<QueueCleaner>()
|
.AddTransient<QueueCleaner>()
|
||||||
.AddTransient<ContentBlocker>()
|
.AddTransient<ContentBlocker>()
|
||||||
.AddTransient<FilenameEvaluator>()
|
.AddTransient<FilenameEvaluator>()
|
||||||
|
.AddTransient<DummyDownloadService>()
|
||||||
.AddTransient<QBitService>()
|
.AddTransient<QBitService>()
|
||||||
.AddTransient<DelugeService>()
|
.AddTransient<DelugeService>()
|
||||||
.AddTransient<TransmissionService>()
|
.AddTransient<TransmissionService>()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"ContentBlocker": {
|
"ContentBlocker": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
|
"IGNORE_PRIVATE": true,
|
||||||
"Blacklist": {
|
"Blacklist": {
|
||||||
"Enabled": false,
|
"Enabled": false,
|
||||||
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
|
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
|
||||||
@@ -26,7 +27,12 @@
|
|||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"RunSequentially": true,
|
"RunSequentially": true,
|
||||||
"IMPORT_FAILED_MAX_STRIKES": 5,
|
"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",
|
"DOWNLOAD_CLIENT": "qbittorrent",
|
||||||
"qBittorrent": {
|
"qBittorrent": {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"ContentBlocker": {
|
"ContentBlocker": {
|
||||||
"Enabled": false,
|
"Enabled": false,
|
||||||
|
"IGNORE_PRIVATE": false,
|
||||||
"Blacklist": {
|
"Blacklist": {
|
||||||
"Enabled": false,
|
"Enabled": false,
|
||||||
"Path": ""
|
"Path": ""
|
||||||
@@ -26,7 +27,10 @@
|
|||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"RunSequentially": true,
|
"RunSequentially": true,
|
||||||
"IMPORT_FAILED_MAX_STRIKES": 5,
|
"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",
|
"DOWNLOAD_CLIENT": "qbittorrent",
|
||||||
"qBittorrent": {
|
"qBittorrent": {
|
||||||
|
|||||||
@@ -65,17 +65,30 @@ public abstract class ArrClient
|
|||||||
return queueResponse;
|
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
|
bool hasWarn() => record.TrackedDownloadStatus
|
||||||
.Equals("warning", StringComparison.InvariantCultureIgnoreCase);
|
.Equals("warning", StringComparison.InvariantCultureIgnoreCase);
|
||||||
bool isImportBlocked() => record.TrackedDownloadState
|
bool isImportBlocked() => record.TrackedDownloadState
|
||||||
.Equals("importBlocked", StringComparison.InvariantCultureIgnoreCase);
|
.Equals("importBlocked", StringComparison.InvariantCultureIgnoreCase);
|
||||||
bool isImportPending() => record.TrackedDownloadState
|
bool isImportPending() => record.TrackedDownloadState
|
||||||
.Equals("importPending", StringComparison.InvariantCultureIgnoreCase);
|
.Equals("importPending", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
|
||||||
if (hasWarn() && (isImportBlocked() || isImportPending()))
|
if (hasWarn() && (isImportBlocked() || isImportPending()))
|
||||||
{
|
{
|
||||||
|
if (HasIgnoredPatterns(record))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("skip failed import check | contains ignored pattern | {name}", record.Title);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return _striker.StrikeAndCheckLimit(
|
return _striker.StrikeAndCheckLimit(
|
||||||
record.DownloadId,
|
record.DownloadId,
|
||||||
record.Title,
|
record.Title,
|
||||||
@@ -134,4 +147,32 @@ public abstract class ArrClient
|
|||||||
{
|
{
|
||||||
request.Headers.Add("x-api-key", 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)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using Common.Configuration;
|
using Common.Configuration.Arr;
|
||||||
using Common.Configuration.Arr;
|
using Common.Configuration.DownloadClient;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
using Infrastructure.Verticals.Arr;
|
using Infrastructure.Verticals.Arr;
|
||||||
@@ -16,6 +16,7 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
|
|
||||||
public ContentBlocker(
|
public ContentBlocker(
|
||||||
ILogger<ContentBlocker> logger,
|
ILogger<ContentBlocker> logger,
|
||||||
|
IOptions<DownloadClientConfig> downloadClientConfig,
|
||||||
IOptions<SonarrConfig> sonarrConfig,
|
IOptions<SonarrConfig> sonarrConfig,
|
||||||
IOptions<RadarrConfig> radarrConfig,
|
IOptions<RadarrConfig> radarrConfig,
|
||||||
SonarrClient sonarrClient,
|
SonarrClient sonarrClient,
|
||||||
@@ -23,13 +24,19 @@ public sealed class ContentBlocker : GenericHandler
|
|||||||
ArrQueueIterator arrArrQueueIterator,
|
ArrQueueIterator arrArrQueueIterator,
|
||||||
BlocklistProvider blocklistProvider,
|
BlocklistProvider blocklistProvider,
|
||||||
DownloadServiceFactory downloadServiceFactory
|
DownloadServiceFactory downloadServiceFactory
|
||||||
) : base(logger, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
|
) : base(logger, downloadClientConfig, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
|
||||||
{
|
{
|
||||||
_blocklistProvider = blocklistProvider;
|
_blocklistProvider = blocklistProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task ExecuteAsync()
|
public override async Task ExecuteAsync()
|
||||||
{
|
{
|
||||||
|
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("download client is set to none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _blocklistProvider.LoadBlocklistAsync();
|
await _blocklistProvider.LoadBlocklistAsync();
|
||||||
await base.ExecuteAsync();
|
await base.ExecuteAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,18 +30,19 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
await _client.LoginAsync();
|
await _client.LoginAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||||
{
|
{
|
||||||
hash = hash.ToLowerInvariant();
|
hash = hash.ToLowerInvariant();
|
||||||
|
|
||||||
DelugeContents? contents = null;
|
DelugeContents? contents = null;
|
||||||
|
RemoveResult result = new();
|
||||||
|
|
||||||
TorrentStatus? status = await GetTorrentStatus(hash);
|
TorrentStatus? status = await GetTorrentStatus(hash);
|
||||||
|
|
||||||
if (status?.Hash is null)
|
if (status?.Hash is null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||||
return false;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -63,7 +64,10 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return shouldRemove || IsItemStuckAndShouldRemove(status);
|
result.ShouldRemove = shouldRemove || IsItemStuckAndShouldRemove(status);
|
||||||
|
result.IsPrivate = status.Private;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task BlockUnwantedFilesAsync(string hash)
|
public override async Task BlockUnwantedFilesAsync(string hash)
|
||||||
@@ -78,6 +82,13 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_queueCleanerConfig.StalledIgnorePrivate && status.Private)
|
||||||
|
{
|
||||||
|
// ignore private trackers
|
||||||
|
_logger.LogDebug("skip files check | download is private | {name}", status.Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
DelugeContents? contents = null;
|
DelugeContents? contents = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -128,6 +139,18 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
|
|
||||||
private bool IsItemStuckAndShouldRemove(TorrentStatus status)
|
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))
|
if (status.State is null || !status.State.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -146,7 +169,7 @@ public sealed class DelugeService : DownloadServiceBase
|
|||||||
return await _client.SendRequest<TorrentStatus?>(
|
return await _client.SendRequest<TorrentStatus?>(
|
||||||
"web.get_torrent_status",
|
"web.get_torrent_status",
|
||||||
hash,
|
hash,
|
||||||
new[] { "hash", "state", "name", "eta" }
|
new[] { "hash", "state", "name", "eta", "private" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public abstract class DownloadServiceBase : IDownloadService
|
|||||||
|
|
||||||
public abstract Task LoginAsync();
|
public abstract Task LoginAsync();
|
||||||
|
|
||||||
public abstract Task<bool> ShouldRemoveFromArrQueueAsync(string hash);
|
public abstract Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash);
|
||||||
|
|
||||||
public abstract Task BlockUnwantedFilesAsync(string hash);
|
public abstract Task BlockUnwantedFilesAsync(string hash);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
using Common.Configuration;
|
using Common.Configuration.DownloadClient;
|
||||||
using Common.Configuration.DownloadClient;
|
|
||||||
using Infrastructure.Verticals.DownloadClient.Deluge;
|
using Infrastructure.Verticals.DownloadClient.Deluge;
|
||||||
using Infrastructure.Verticals.DownloadClient.QBittorrent;
|
using Infrastructure.Verticals.DownloadClient.QBittorrent;
|
||||||
using Infrastructure.Verticals.DownloadClient.Transmission;
|
using Infrastructure.Verticals.DownloadClient.Transmission;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
@@ -12,24 +10,21 @@ namespace Infrastructure.Verticals.DownloadClient;
|
|||||||
public sealed class DownloadServiceFactory
|
public sealed class DownloadServiceFactory
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly Domain.Enums.DownloadClient _downloadClient;
|
private readonly Common.Enums.DownloadClient _downloadClient;
|
||||||
|
|
||||||
public DownloadServiceFactory(IServiceProvider serviceProvider, IConfiguration configuration)
|
public DownloadServiceFactory(IServiceProvider serviceProvider, IOptions<DownloadClientConfig> downloadClientConfig)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_downloadClient = (Domain.Enums.DownloadClient)Enum.Parse(
|
_downloadClient = downloadClientConfig.Value.DownloadClient;
|
||||||
typeof(Domain.Enums.DownloadClient),
|
|
||||||
configuration[EnvironmentVariables.DownloadClient] ?? Domain.Enums.DownloadClient.QBittorrent.ToString(),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDownloadService CreateDownloadClient() =>
|
public IDownloadService CreateDownloadClient() =>
|
||||||
_downloadClient switch
|
_downloadClient switch
|
||||||
{
|
{
|
||||||
Domain.Enums.DownloadClient.QBittorrent => _serviceProvider.GetRequiredService<QBitService>(),
|
Common.Enums.DownloadClient.QBittorrent => _serviceProvider.GetRequiredService<QBitService>(),
|
||||||
Domain.Enums.DownloadClient.Deluge => _serviceProvider.GetRequiredService<DelugeService>(),
|
Common.Enums.DownloadClient.Deluge => _serviceProvider.GetRequiredService<DelugeService>(),
|
||||||
Domain.Enums.DownloadClient.Transmission => _serviceProvider.GetRequiredService<TransmissionService>(),
|
Common.Enums.DownloadClient.Transmission => _serviceProvider.GetRequiredService<TransmissionService>(),
|
||||||
|
Common.Enums.DownloadClient.None => _serviceProvider.GetRequiredService<DummyDownloadService>(),
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Common.Configuration.QueueCleaner;
|
||||||
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Infrastructure.Verticals.DownloadClient;
|
||||||
|
|
||||||
|
public sealed class DummyDownloadService : DownloadServiceBase
|
||||||
|
{
|
||||||
|
public DummyDownloadService(ILogger<DownloadServiceBase> logger, IOptions<QueueCleanerConfig> queueCleanerConfig, FilenameEvaluator filenameEvaluator, Striker striker) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task LoginAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task BlockUnwantedFilesAsync(string hash)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ public interface IDownloadService : IDisposable
|
|||||||
{
|
{
|
||||||
public Task LoginAsync();
|
public Task LoginAsync();
|
||||||
|
|
||||||
public Task<bool> ShouldRemoveFromArrQueueAsync(string hash);
|
public Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash);
|
||||||
|
|
||||||
public Task BlockUnwantedFilesAsync(string hash);
|
public Task BlockUnwantedFilesAsync(string hash);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Common.Configuration.DownloadClient;
|
using Common.Configuration.DownloadClient;
|
||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.QueueCleaner;
|
||||||
using Infrastructure.Verticals.ContentBlocker;
|
using Infrastructure.Verticals.ContentBlocker;
|
||||||
using Infrastructure.Verticals.ItemStriker;
|
using Infrastructure.Verticals.ItemStriker;
|
||||||
@@ -36,21 +36,35 @@ public sealed class QBitService : DownloadServiceBase
|
|||||||
await _client.LoginAsync(_config.Username, _config.Password);
|
await _client.LoginAsync(_config.Username, _config.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||||
{
|
{
|
||||||
|
RemoveResult result = new();
|
||||||
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (torrent is null)
|
if (torrent is null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||||
return false;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
||||||
|
|
||||||
|
if (torrentProperties is null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("failed to find torrent properties {hash} in the download client", hash);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.IsPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) &&
|
||||||
|
bool.TryParse(dictValue?.ToString(), out bool boolValue)
|
||||||
|
&& boolValue;
|
||||||
|
|
||||||
// if all files were blocked by qBittorrent
|
// if all files were blocked by qBittorrent
|
||||||
if (torrent is { CompletionOn: not null, Downloaded: null or 0 })
|
if (torrent is { CompletionOn: not null, Downloaded: null or 0 })
|
||||||
{
|
{
|
||||||
return true;
|
result.ShouldRemove = true;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
||||||
@@ -58,14 +72,45 @@ public sealed class QBitService : DownloadServiceBase
|
|||||||
// if all files are marked as skip
|
// if all files are marked as skip
|
||||||
if (files?.Count is > 0 && files.All(x => x.Priority is TorrentContentPriority.Skip))
|
if (files?.Count is > 0 && files.All(x => x.Priority is TorrentContentPriority.Skip))
|
||||||
{
|
{
|
||||||
return true;
|
result.ShouldRemove = true;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsItemStuckAndShouldRemove(torrent);
|
result.ShouldRemove = IsItemStuckAndShouldRemove(torrent, result.IsPrivate);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task BlockUnwantedFilesAsync(string hash)
|
public override async Task BlockUnwantedFilesAsync(string hash)
|
||||||
{
|
{
|
||||||
|
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (torrent is null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
||||||
|
|
||||||
|
if (torrentProperties is null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("failed to find torrent properties {hash} in the download client", hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) &&
|
||||||
|
bool.TryParse(dictValue?.ToString(), out bool boolValue)
|
||||||
|
&& boolValue;
|
||||||
|
|
||||||
|
if (_queueCleanerConfig.StalledIgnorePrivate && isPrivate)
|
||||||
|
{
|
||||||
|
// ignore private trackers
|
||||||
|
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
||||||
|
|
||||||
if (files is null)
|
if (files is null)
|
||||||
@@ -95,8 +140,20 @@ public sealed class QBitService : DownloadServiceBase
|
|||||||
_client.Dispose();
|
_client.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsItemStuckAndShouldRemove(TorrentInfo torrent)
|
private bool IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate)
|
||||||
{
|
{
|
||||||
|
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_queueCleanerConfig.StalledIgnorePrivate && isPrivate)
|
||||||
|
{
|
||||||
|
// ignore private trackers
|
||||||
|
_logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (torrent.State is not TorrentState.StalledDownload and not TorrentState.FetchingMetadata
|
if (torrent.State is not TorrentState.StalledDownload and not TorrentState.FetchingMetadata
|
||||||
and not TorrentState.ForcedFetchingMetadata)
|
and not TorrentState.ForcedFetchingMetadata)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Infrastructure.Verticals.DownloadClient;
|
||||||
|
|
||||||
|
public sealed record RemoveResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True if the download should be removed; otherwise false.
|
||||||
|
/// </summary>
|
||||||
|
public bool ShouldRemove { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the download is private; otherwise false.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPrivate { get; set; }
|
||||||
|
}
|
||||||
@@ -38,17 +38,19 @@ public sealed class TransmissionService : DownloadServiceBase
|
|||||||
await _client.GetSessionInformationAsync();
|
await _client.GetSessionInformationAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||||
{
|
{
|
||||||
|
RemoveResult result = new();
|
||||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||||
|
|
||||||
if (torrent is null)
|
if (torrent is null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||||
return false;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool shouldRemove = torrent.FileStats?.Length > 0;
|
bool shouldRemove = torrent.FileStats?.Length > 0;
|
||||||
|
result.IsPrivate = torrent.IsPrivate ?? false;
|
||||||
|
|
||||||
foreach (TransmissionTorrentFileStats? stats in torrent.FileStats ?? [])
|
foreach (TransmissionTorrentFileStats? stats in torrent.FileStats ?? [])
|
||||||
{
|
{
|
||||||
@@ -65,8 +67,10 @@ public sealed class TransmissionService : DownloadServiceBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove if all files are unwanted
|
// remove if all files are unwanted or download is stuck
|
||||||
return shouldRemove || IsItemStuckAndShouldRemove(torrent);
|
result.ShouldRemove = shouldRemove || IsItemStuckAndShouldRemove(torrent);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task BlockUnwantedFilesAsync(string hash)
|
public override async Task BlockUnwantedFilesAsync(string hash)
|
||||||
@@ -77,6 +81,13 @@ public sealed class TransmissionService : DownloadServiceBase
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_queueCleanerConfig.StalledIgnorePrivate && (torrent.IsPrivate ?? false))
|
||||||
|
{
|
||||||
|
// ignore private trackers
|
||||||
|
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<long> unwantedFiles = [];
|
List<long> unwantedFiles = [];
|
||||||
|
|
||||||
@@ -116,6 +127,18 @@ public sealed class TransmissionService : DownloadServiceBase
|
|||||||
|
|
||||||
private bool IsItemStuckAndShouldRemove(TorrentInfo torrent)
|
private bool IsItemStuckAndShouldRemove(TorrentInfo torrent)
|
||||||
{
|
{
|
||||||
|
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_queueCleanerConfig.StalledIgnorePrivate && (torrent.IsPrivate ?? false))
|
||||||
|
{
|
||||||
|
// ignore private trackers
|
||||||
|
_logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (torrent.Status is not 4)
|
if (torrent.Status is not 4)
|
||||||
{
|
{
|
||||||
// not in downloading state
|
// not in downloading state
|
||||||
@@ -144,7 +167,8 @@ public sealed class TransmissionService : DownloadServiceBase
|
|||||||
TorrentFields.ID,
|
TorrentFields.ID,
|
||||||
TorrentFields.ETA,
|
TorrentFields.ETA,
|
||||||
TorrentFields.NAME,
|
TorrentFields.NAME,
|
||||||
TorrentFields.STATUS
|
TorrentFields.STATUS,
|
||||||
|
TorrentFields.IS_PRIVATE
|
||||||
];
|
];
|
||||||
|
|
||||||
// refresh cache
|
// refresh cache
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
using Common.Configuration.Arr;
|
using Common.Configuration.Arr;
|
||||||
|
using Common.Configuration.DownloadClient;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
using Domain.Models.Arr;
|
using Domain.Models.Arr;
|
||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
using Infrastructure.Verticals.Arr;
|
using Infrastructure.Verticals.Arr;
|
||||||
using Infrastructure.Verticals.DownloadClient;
|
using Infrastructure.Verticals.DownloadClient;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Infrastructure.Verticals.Jobs;
|
namespace Infrastructure.Verticals.Jobs;
|
||||||
|
|
||||||
public abstract class GenericHandler : IDisposable
|
public abstract class GenericHandler : IDisposable
|
||||||
{
|
{
|
||||||
protected readonly ILogger<GenericHandler> _logger;
|
protected readonly ILogger<GenericHandler> _logger;
|
||||||
|
protected readonly DownloadClientConfig _downloadClientConfig;
|
||||||
protected readonly SonarrConfig _sonarrConfig;
|
protected readonly SonarrConfig _sonarrConfig;
|
||||||
protected readonly RadarrConfig _radarrConfig;
|
protected readonly RadarrConfig _radarrConfig;
|
||||||
protected readonly SonarrClient _sonarrClient;
|
protected readonly SonarrClient _sonarrClient;
|
||||||
@@ -21,6 +23,7 @@ public abstract class GenericHandler : IDisposable
|
|||||||
|
|
||||||
protected GenericHandler(
|
protected GenericHandler(
|
||||||
ILogger<GenericHandler> logger,
|
ILogger<GenericHandler> logger,
|
||||||
|
IOptions<DownloadClientConfig> downloadClientConfig,
|
||||||
SonarrConfig sonarrConfig,
|
SonarrConfig sonarrConfig,
|
||||||
RadarrConfig radarrConfig,
|
RadarrConfig radarrConfig,
|
||||||
SonarrClient sonarrClient,
|
SonarrClient sonarrClient,
|
||||||
@@ -30,6 +33,7 @@ public abstract class GenericHandler : IDisposable
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_downloadClientConfig = downloadClientConfig.Value;
|
||||||
_sonarrConfig = sonarrConfig;
|
_sonarrConfig = sonarrConfig;
|
||||||
_radarrConfig = radarrConfig;
|
_radarrConfig = radarrConfig;
|
||||||
_sonarrClient = sonarrClient;
|
_sonarrClient = sonarrClient;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Common.Configuration.Arr;
|
using Common.Configuration.Arr;
|
||||||
using Common.Configuration.QueueCleaner;
|
using Common.Configuration.DownloadClient;
|
||||||
using Domain.Enums;
|
using Domain.Enums;
|
||||||
using Domain.Models.Arr;
|
using Domain.Models.Arr;
|
||||||
using Domain.Models.Arr.Queue;
|
using Domain.Models.Arr.Queue;
|
||||||
@@ -15,13 +15,14 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
{
|
{
|
||||||
public QueueCleaner(
|
public QueueCleaner(
|
||||||
ILogger<QueueCleaner> logger,
|
ILogger<QueueCleaner> logger,
|
||||||
|
IOptions<DownloadClientConfig> downloadClientConfig,
|
||||||
IOptions<SonarrConfig> sonarrConfig,
|
IOptions<SonarrConfig> sonarrConfig,
|
||||||
IOptions<RadarrConfig> radarrConfig,
|
IOptions<RadarrConfig> radarrConfig,
|
||||||
SonarrClient sonarrClient,
|
SonarrClient sonarrClient,
|
||||||
RadarrClient radarrClient,
|
RadarrClient radarrClient,
|
||||||
ArrQueueIterator arrArrQueueIterator,
|
ArrQueueIterator arrArrQueueIterator,
|
||||||
DownloadServiceFactory downloadServiceFactory
|
DownloadServiceFactory downloadServiceFactory
|
||||||
) : base(logger, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
|
) : base(logger, downloadClientConfig, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +57,16 @@ public sealed class QueueCleaner : GenericHandler
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!arrClient.ShouldRemoveFromQueue(record) && !await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId))
|
RemoveResult removeResult = new();
|
||||||
|
|
||||||
|
if (_downloadClientConfig.DownloadClient is not Common.Enums.DownloadClient.None)
|
||||||
|
{
|
||||||
|
removeResult = await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldRemoveFromArr = arrClient.ShouldRemoveFromQueue(record, removeResult.IsPrivate);
|
||||||
|
|
||||||
|
if (!shouldRemoveFromArr && !removeResult.ShouldRemove)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("skip | {title}", record.Title);
|
_logger.LogInformation("skip | {title}", record.Title);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -182,9 +182,13 @@ services:
|
|||||||
- QUEUECLEANER__ENABLED=true
|
- QUEUECLEANER__ENABLED=true
|
||||||
- QUEUECLEANER__RUNSEQUENTIALLY=true
|
- QUEUECLEANER__RUNSEQUENTIALLY=true
|
||||||
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
|
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
|
||||||
|
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=true
|
||||||
|
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=file is a sample
|
||||||
- QUEUECLEANER__STALLED_MAX_STRIKES=5
|
- QUEUECLEANER__STALLED_MAX_STRIKES=5
|
||||||
|
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=true
|
||||||
|
|
||||||
- CONTENTBLOCKER__ENABLED=true
|
- CONTENTBLOCKER__ENABLED=true
|
||||||
|
- CONTENTBLOCKER__IGNORE_PRIVATE=true
|
||||||
- CONTENTBLOCKER__BLACKLIST__ENABLED=true
|
- CONTENTBLOCKER__BLACKLIST__ENABLED=true
|
||||||
- CONTENTBLOCKER__BLACKLIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
- CONTENTBLOCKER__BLACKLIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
||||||
# OR
|
# OR
|
||||||
|
|||||||
Reference in New Issue
Block a user