mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-01-20 03:38:03 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75b001cf6a | ||
|
|
479ca7884e | ||
|
|
00d8910118 |
18
README.md
18
README.md
@@ -131,10 +131,14 @@ I've seen a few discussions on this type of naming and I've decided that I didn'
|
||||
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`.
|
||||
4. Set `DOWNLOAD_CLIENT` to `none`(works only for usenet) or `disabled` (works for both usenet and torrent).
|
||||
|
||||
> [!WARNING]
|
||||
> When `DOWNLOAD_CLIENT=none`, no other action involving a download client would work (e.g. content blocking, removing stalled downloads, excluding private trackers).
|
||||
> [!IMPORTANT]
|
||||
> When `DOWNLOAD_CLIENT=disabled`, no other action involving a download client would work (e.g. content blocking, removing stalled downloads, excluding private trackers).
|
||||
>
|
||||
> When the download client is set to `disabled`, the queue cleaner will be able to remove items that are failed to be imported even if there is no download client configured. This means that all downloads, including private ones, will be completely removed.
|
||||
>
|
||||
> Setting `DOWNLOAD_CLIENT=disabled` means you don't care about seeding, ratio, H&R and potentially losing your private tracker account.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -287,6 +291,14 @@ services:
|
||||
- NOTIFIARR__ON_DOWNLOAD_CLEANED=true
|
||||
- NOTIFIARR__API_KEY=notifiarr_secret
|
||||
- NOTIFIARR__CHANNEL_ID=discord_channel_id
|
||||
|
||||
- APPRISE__ON_IMPORT_FAILED_STRIKE=true
|
||||
- APPRISE__ON_STALLED_STRIKE=true
|
||||
- APPRISE__ON_SLOW_STRIKE=true
|
||||
- APPRISE__ON_QUEUE_ITEM_DELETED=true
|
||||
- APPRISE__ON_DOWNLOAD_CLEANED=true
|
||||
- APPRISE__URL=http://apprise:8000
|
||||
- APPRISE__KEY=myConfigKey
|
||||
```
|
||||
|
||||
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/windows.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Windows</span>
|
||||
|
||||
@@ -35,5 +35,5 @@ public sealed record DownloadStatus
|
||||
|
||||
public sealed record Tracker
|
||||
{
|
||||
public required Uri Url { get; init; }
|
||||
public required string Url { get; init; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Infrastructure.Verticals.Notifications;
|
||||
using Infrastructure.Verticals.Notifications.Apprise;
|
||||
using Infrastructure.Verticals.Notifications.Notifiarr;
|
||||
|
||||
namespace Executable.DependencyInjection;
|
||||
@@ -8,8 +9,11 @@ public static class NotificationsDI
|
||||
public static IServiceCollection AddNotifications(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.Configure<NotifiarrConfig>(configuration.GetSection(NotifiarrConfig.SectionName))
|
||||
.Configure<AppriseConfig>(configuration.GetSection(AppriseConfig.SectionName))
|
||||
.AddTransient<INotifiarrProxy, NotifiarrProxy>()
|
||||
.AddTransient<INotificationProvider, NotifiarrProvider>()
|
||||
.AddTransient<IAppriseProxy, AppriseProxy>()
|
||||
.AddTransient<INotificationProvider, AppriseProvider>()
|
||||
.AddTransient<INotificationPublisher, NotificationPublisher>()
|
||||
.AddTransient<INotificationFactory, NotificationFactory>()
|
||||
.AddTransient<NotificationService>();
|
||||
|
||||
@@ -122,5 +122,14 @@
|
||||
"ON_DOWNLOAD_CLEANED": true,
|
||||
"API_KEY": "",
|
||||
"CHANNEL_ID": ""
|
||||
},
|
||||
"Apprise": {
|
||||
"ON_IMPORT_FAILED_STRIKE": true,
|
||||
"ON_STALLED_STRIKE": true,
|
||||
"ON_SLOW_STRIKE": true,
|
||||
"ON_QUEUE_ITEM_DELETED": true,
|
||||
"ON_DOWNLOAD_CLEANED": true,
|
||||
"URL": "http://localhost:8000",
|
||||
"KEY": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,5 +105,14 @@
|
||||
"ON_DOWNLOAD_CLEANED": false,
|
||||
"API_KEY": "",
|
||||
"CHANNEL_ID": ""
|
||||
},
|
||||
"Apprise": {
|
||||
"ON_IMPORT_FAILED_STRIKE": false,
|
||||
"ON_STALLED_STRIKE": false,
|
||||
"ON_SLOW_STRIKE": false,
|
||||
"ON_QUEUE_ITEM_DELETED": false,
|
||||
"ON_DOWNLOAD_CLEANED": false,
|
||||
"URL": "",
|
||||
"KEY": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Domain.Models.Deluge.Response;
|
||||
using Infrastructure.Helpers;
|
||||
|
||||
namespace Infrastructure.Extensions;
|
||||
|
||||
@@ -18,7 +19,7 @@ public static class DelugeExtensions
|
||||
return true;
|
||||
}
|
||||
|
||||
if (download.Trackers.Any(x => x.Url.Host.EndsWith(value, StringComparison.InvariantCultureIgnoreCase)))
|
||||
if (download.Trackers.Any(x => UriService.GetDomain(x.Url)?.EndsWith(value, StringComparison.InvariantCultureIgnoreCase) ?? false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using QBittorrent.Client;
|
||||
using Infrastructure.Helpers;
|
||||
using QBittorrent.Client;
|
||||
|
||||
namespace Infrastructure.Extensions;
|
||||
|
||||
@@ -29,9 +30,16 @@ public static class QBitExtensions
|
||||
|
||||
public static bool ShouldIgnore(this TorrentTracker tracker, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
string? trackerUrl = UriService.GetDomain(tracker.Url);
|
||||
|
||||
if (trackerUrl is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (string value in ignoredDownloads)
|
||||
{
|
||||
if (tracker.Url.Host.EndsWith(value, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (trackerUrl.EndsWith(value, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Transmission.API.RPC.Entity;
|
||||
using Infrastructure.Helpers;
|
||||
using Transmission.API.RPC.Entity;
|
||||
|
||||
namespace Infrastructure.Extensions;
|
||||
|
||||
@@ -19,7 +20,7 @@ public static class TransmissionExtensions
|
||||
}
|
||||
|
||||
bool? hasIgnoredTracker = download.Trackers?
|
||||
.Any(x => new Uri(x.Announce!).Host.EndsWith(value, StringComparison.InvariantCultureIgnoreCase));
|
||||
.Any(x => UriService.GetDomain(x.Announce)?.EndsWith(value, StringComparison.InvariantCultureIgnoreCase) ?? false);
|
||||
|
||||
if (hasIgnoredTracker is true)
|
||||
{
|
||||
|
||||
37
code/Infrastructure/Helpers/UriService.cs
Normal file
37
code/Infrastructure/Helpers/UriService.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Infrastructure.Helpers;
|
||||
|
||||
public static class UriService
|
||||
{
|
||||
public static string? GetDomain(string? input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// add "http://" if scheme is missing to help Uri.TryCreate
|
||||
if (!input.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
input = "http://" + input;
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(input, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return uri.Host;
|
||||
}
|
||||
|
||||
// url might be malformed
|
||||
var regex = new Regex(@"^(?:https?:\/\/)?([^\/\?:]+)", RegexOptions.IgnoreCase);
|
||||
var match = regex.Match(input);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
// could not extract
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FLM.QBittorrent" Version="1.0.0" />
|
||||
<PackageReference Include="FLM.QBittorrent" Version="1.0.1" />
|
||||
<PackageReference Include="FLM.Transmission" Version="1.0.3" />
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
<PackageReference Include="MassTransit" Version="8.3.6" />
|
||||
|
||||
@@ -428,7 +428,7 @@ public class QBitService : DownloadService, IQBitService
|
||||
private async Task<IReadOnlyList<TorrentTracker>> GetTrackersAsync(string hash)
|
||||
{
|
||||
return (await _client.GetTorrentTrackersAsync(hash))
|
||||
.Where(x => !x.Url.ToString().Contains("**"))
|
||||
.Where(x => x.Url.Contains("**"))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Common.Configuration.Notification;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Apprise;
|
||||
|
||||
public sealed record AppriseConfig : NotificationConfig
|
||||
{
|
||||
public const string SectionName = "Apprise";
|
||||
|
||||
public Uri? Url { get; init; }
|
||||
|
||||
public string? Key { get; init; }
|
||||
|
||||
public override bool IsValid()
|
||||
{
|
||||
if (Url is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Key?.Trim()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Apprise;
|
||||
|
||||
public sealed record ApprisePayload
|
||||
{
|
||||
[Required]
|
||||
public string Title { get; init; }
|
||||
|
||||
[Required]
|
||||
public string Body { get; init; }
|
||||
|
||||
public string Type { get; init; } = NotificationType.Info.ToString().ToLowerInvariant();
|
||||
|
||||
public string Format { get; init; } = FormatType.Text.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
public enum NotificationType
|
||||
{
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Failure
|
||||
}
|
||||
|
||||
public enum FormatType
|
||||
{
|
||||
Text,
|
||||
Markdown,
|
||||
Html
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Text;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Apprise;
|
||||
|
||||
public sealed class AppriseProvider : NotificationProvider
|
||||
{
|
||||
private readonly AppriseConfig _config;
|
||||
private readonly IAppriseProxy _proxy;
|
||||
|
||||
public AppriseProvider(IOptions<AppriseConfig> config, IAppriseProxy proxy)
|
||||
: base(config)
|
||||
{
|
||||
_config = config.Value;
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
public override string Name => "Apprise";
|
||||
|
||||
public override async Task OnFailedImportStrike(FailedImportStrikeNotification notification)
|
||||
{
|
||||
await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), _config);
|
||||
}
|
||||
|
||||
public override async Task OnStalledStrike(StalledStrikeNotification notification)
|
||||
{
|
||||
await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), _config);
|
||||
}
|
||||
|
||||
public override async Task OnSlowStrike(SlowStrikeNotification notification)
|
||||
{
|
||||
await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), _config);
|
||||
}
|
||||
|
||||
public override async Task OnQueueItemDeleted(QueueItemDeletedNotification notification)
|
||||
{
|
||||
await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), _config);
|
||||
}
|
||||
|
||||
public override async Task OnDownloadCleaned(DownloadCleanedNotification notification)
|
||||
{
|
||||
await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), _config);
|
||||
}
|
||||
|
||||
private static ApprisePayload BuildPayload(ArrNotification notification, NotificationType notificationType)
|
||||
{
|
||||
StringBuilder body = new();
|
||||
body.AppendLine(notification.Description);
|
||||
body.AppendLine();
|
||||
body.AppendLine($"Instance type: {notification.InstanceType.ToString()}");
|
||||
body.AppendLine($"Url: {notification.InstanceUrl}");
|
||||
body.AppendLine($"Download hash: {notification.Hash}");
|
||||
|
||||
foreach (NotificationField field in notification.Fields ?? [])
|
||||
{
|
||||
body.AppendLine($"{field.Title}: {field.Text}");
|
||||
}
|
||||
|
||||
ApprisePayload payload = new()
|
||||
{
|
||||
Title = notification.Title,
|
||||
Body = body.ToString(),
|
||||
Type = notificationType.ToString().ToLowerInvariant(),
|
||||
};
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
private static ApprisePayload BuildPayload(Notification notification, NotificationType notificationType)
|
||||
{
|
||||
StringBuilder body = new();
|
||||
body.AppendLine(notification.Description);
|
||||
body.AppendLine();
|
||||
|
||||
foreach (NotificationField field in notification.Fields ?? [])
|
||||
{
|
||||
body.AppendLine($"{field.Title}: {field.Text}");
|
||||
}
|
||||
|
||||
ApprisePayload payload = new()
|
||||
{
|
||||
Title = notification.Title,
|
||||
Body = body.ToString(),
|
||||
Type = notificationType.ToString().ToLowerInvariant(),
|
||||
};
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Text;
|
||||
using Common.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Apprise;
|
||||
|
||||
public sealed class AppriseProxy : IAppriseProxy
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public AppriseProxy(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClient = httpClientFactory.CreateClient(Constants.HttpClientWithRetryName);
|
||||
}
|
||||
|
||||
public async Task SendNotification(ApprisePayload payload, AppriseConfig config)
|
||||
{
|
||||
string content = JsonConvert.SerializeObject(payload, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
});
|
||||
|
||||
UriBuilder uriBuilder = new(config.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/notify/{config.Key}";
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri);
|
||||
request.Method = HttpMethod.Post;
|
||||
request.Content = new StringContent(content, Encoding.UTF8, "application/json");
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Apprise;
|
||||
|
||||
public interface IAppriseProxy
|
||||
{
|
||||
Task SendNotification(ApprisePayload payload, AppriseConfig config);
|
||||
}
|
||||
@@ -12,6 +12,8 @@ public class NotifiarrProvider : NotificationProvider
|
||||
private const string WarningColor = "f0ad4e";
|
||||
private const string ImportantColor = "bb2124";
|
||||
private const string Logo = "https://github.com/flmorg/cleanuperr/blob/main/Logo/48.png?raw=true";
|
||||
|
||||
public override string Name => "Notifiarr";
|
||||
|
||||
public NotifiarrProvider(IOptions<NotifiarrConfig> config, INotifiarrProxy proxy)
|
||||
: base(config)
|
||||
@@ -20,8 +22,6 @@ public class NotifiarrProvider : NotificationProvider
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
public override string Name => "Notifiarr";
|
||||
|
||||
public override async Task OnFailedImportStrike(FailedImportStrikeNotification notification)
|
||||
{
|
||||
await _proxy.SendNotification(BuildPayload(notification, WarningColor), _config);
|
||||
|
||||
@@ -5,7 +5,7 @@ using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Notifiarr;
|
||||
|
||||
public class NotifiarrProxy : INotifiarrProxy
|
||||
public sealed class NotifiarrProxy : INotifiarrProxy
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
|
||||
@@ -262,13 +262,21 @@ services:
|
||||
- LIDARR__INSTANCES__0__URL=http://lidarr:8686
|
||||
- LIDARR__INSTANCES__0__APIKEY=7f677cfdc074414397af53dd633860c5
|
||||
|
||||
- NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true
|
||||
- NOTIFIARR__ON_STALLED_STRIKE=true
|
||||
- NOTIFIARR__ON_SLOW_STRIKE=true
|
||||
- NOTIFIARR__ON_QUEUE_ITEM_DELETED=true
|
||||
- NOTIFIARR__ON_DOWNLOAD_CLEANED=true
|
||||
- NOTIFIARR__API_KEY=notifiarr_secret
|
||||
- NOTIFIARR__CHANNEL_ID=discord_channel_id
|
||||
# - NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true
|
||||
# - NOTIFIARR__ON_STALLED_STRIKE=true
|
||||
# - NOTIFIARR__ON_SLOW_STRIKE=true
|
||||
# - NOTIFIARR__ON_QUEUE_ITEM_DELETED=true
|
||||
# - NOTIFIARR__ON_DOWNLOAD_CLEANED=true
|
||||
# - NOTIFIARR__API_KEY=notifiarr_secret
|
||||
# - NOTIFIARR__CHANNEL_ID=discord_channel_id
|
||||
|
||||
# - APPRISE__ON_IMPORT_FAILED_STRIKE=true
|
||||
# - APPRISE__ON_STALLED_STRIKE=true
|
||||
# - APPRISE__ON_SLOW_STRIKE=true
|
||||
# - APPRISE__ON_QUEUE_ITEM_DELETED=true
|
||||
# - APPRISE__ON_DOWNLOAD_CLEANED=true
|
||||
# - APPRISE__URL=http://localhost:8000
|
||||
# - APPRISE__KEY=mykey
|
||||
volumes:
|
||||
- ./data/cleanuperr/logs:/var/logs
|
||||
- ./data/cleanuperr/ignored_downloads:/ignored
|
||||
|
||||
49
variables.md
49
variables.md
@@ -6,6 +6,8 @@
|
||||
- [Download Client settings](#download-client-settings)
|
||||
- [Arr settings](#arr-settings)
|
||||
- [Notification settings](#notification-settings)
|
||||
- [Notifiarr settings](#notifiarr__api_key)
|
||||
- [Apprise settings](#apprise__url)
|
||||
- [Advanced settings](#advanced-settings)
|
||||
|
||||
#
|
||||
@@ -702,6 +704,53 @@
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
#### **`APPRISE__URL`**
|
||||
- [Apprise url](https://github.com/caronc/apprise-api) where to send notifications.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
#### **`APPRISE__KEY`**
|
||||
- [Apprise configuration key](https://github.com/caronc/apprise-api?tab=readme-ov-file#screenshots) containing all 3rd party notification providers which Cleanuperr would notify.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
#### **`APPRISE__ON_IMPORT_FAILED_STRIKE`**
|
||||
- Controls whether to notify when an item receives a failed import strike.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
#### **`APPRISE__ON_STALLED_STRIKE`**
|
||||
- Controls whether to notify when an item receives a stalled download strike. This includes strikes for being stuck while downloading metadata.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
#### **`APPRISE__ON_SLOW_STRIKE`**
|
||||
- Controls whether to notify when an item receives a slow download strike. This includes strikes for having a low download speed or slow estimated finish time.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
#### **`APPRISE__ON_QUEUE_ITEM_DELETED`**
|
||||
- Controls whether to notify when a queue item is deleted.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
#### **`APPRISE__ON_DOWNLOAD_CLEANED`**
|
||||
- Controls whether to notify when a download is cleaned.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
#
|
||||
|
||||
### Advanced settings
|
||||
|
||||
Reference in New Issue
Block a user