Skip to content

Add category change for downloads with no additional hardlinks #65

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

Open
wants to merge 50 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
017e25f
added category change for downloads with no additional hardlinks
Flaminel Feb 19, 2025
2d6f166
trying to fix Unix stat
Flaminel Feb 21, 2025
1650b0e
trying to fix Unix stat #2
Flaminel Feb 21, 2025
d454a09
fixed qbit file path
Flaminel Feb 21, 2025
a63bae0
added debug logs
Flaminel Feb 21, 2025
268ede8
trying to fix Unix stat again
Flaminel Feb 21, 2025
fbe6eba
trying to fix Unix stat yet again
Flaminel Feb 21, 2025
f91e856
removed debug logs
Flaminel Feb 21, 2025
19ac8cb
trying to account for cross-seed
Flaminel Feb 21, 2025
029f255
added debug logs
Flaminel Feb 21, 2025
8c8d412
refactored hardlink service
Flaminel Feb 21, 2025
9b68792
added handling for windows files
Flaminel Feb 21, 2025
1ad07b1
refactored names; fixed return values for hard links service
Flaminel Feb 21, 2025
e006521
refactored method names; fixed qbit category creation
Flaminel Feb 21, 2025
5bd2a9c
added trace logs
Flaminel Feb 21, 2025
bd81f2f
fixed using root directory instead of root path
Flaminel Feb 22, 2025
c65c85a
fixed categories not being filtered when fetching downloads; fixed in…
Flaminel Feb 22, 2025
e8d287d
fixed missing notification configuration
Flaminel Feb 22, 2025
d27562a
explicit dispose and clean on some objects
Flaminel Feb 23, 2025
c27ee32
updated test data
Flaminel Feb 23, 2025
6b33075
fixed merge conflicts
Flaminel Feb 26, 2025
1243da3
streamlined downloads processing after category changed
Flaminel Feb 26, 2025
a994bc4
fixed missing methods
Flaminel Feb 27, 2025
bc642d8
fixed typo
Flaminel Feb 27, 2025
3c8ef3d
fixed service type
Flaminel Feb 27, 2025
b834a8b
fixed downloads to change category value
Flaminel Feb 27, 2025
46ac50c
fixed hard links check
Flaminel Feb 27, 2025
b1d98c2
fixed category changing
Flaminel Feb 27, 2025
6bc59c8
added implementation for Deluge and Transmission
Flaminel Mar 24, 2025
d993cd3
qbit test
Flaminel Mar 24, 2025
a83809e
Merge branch 'main' into add_cleanup_on_no_hardlinks
Flaminel Mar 24, 2025
4a1e0f6
renamed vars; added root dir; fixed root dir file counts; fixed qbit …
Flaminel Mar 24, 2025
3b63d1b
fixed some logs and comments
Flaminel Mar 25, 2025
5e362d4
added Deluge label creation
Flaminel Mar 25, 2025
7639b07
added set torrent label for Deluge
Flaminel Mar 25, 2025
c86e9c9
removed commented and unused code
Flaminel Mar 25, 2025
f2130ad
fixed label being init only
Flaminel Mar 25, 2025
ab8fbc4
fixed Deluge get labels
Flaminel Mar 25, 2025
7b95ec5
fixed category change notification
Flaminel Mar 25, 2025
60e838c
fixed Deluge processing for changing category
Flaminel Mar 25, 2025
4b38a6f
Merge branch 'main' into add_cleanup_on_no_hardlinks
Flaminel Mar 25, 2025
ac086fc
fixed missing download location
Flaminel Mar 26, 2025
1a89822
fixed some logs; updated test files; fixed deluge not working after f…
Flaminel Mar 26, 2025
874351a
added dry run for category creation
Flaminel Mar 26, 2025
2d3ff04
fixed Deluge failing when WebUI is not connected
Flaminel Mar 26, 2025
2b76630
fixed processing files marked as skipped
Flaminel Mar 26, 2025
6dd9016
made Striker transient
Flaminel Mar 28, 2025
0b43e1b
added Transmission implementation
Flaminel Mar 28, 2025
926a2bb
Merge branch 'main' into add_cleanup_on_no_hardlinks
Flaminel Apr 6, 2025
d864c1a
updated docs
Flaminel Apr 6, 2025
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
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ cleanuperr was created primarily to address malicious files, such as `*.lnk` or
> - Remove and block downloads that reached a maximum number of strikes.
> - Remove downloads blocked by qBittorrent or by cleanuperr's **content blocker**.
> - Trigger a search for downloads removed from the *arrs.
> - Clean up downloads that have been seeding for a certain amount of time.
> - Remove downloads that have been seeding for a certain amount of time.
> - Remove downloads that have no hardlinks (have been upgraded by the *arrs).
> - Notify on strike or download removal.
> - Ignore certain torrent hashes, categories, tags or trackers from processing.

Expand Down Expand Up @@ -176,31 +177,41 @@ services:
- LOGGING__FILE__PATH=/var/logs/
- LOGGING__ENHANCED=true

# triggers
- TRIGGERS__QUEUECLEANER=0 0/5 * * * ?
- TRIGGERS__CONTENTBLOCKER=0 0/5 * * * ?
- TRIGGERS__DOWNLOADCLEANER=0 0 * * * ?

# queue cleaner
- QUEUECLEANER__ENABLED=true
- QUEUECLEANER__IGNORED_DOWNLOADS_PATH=/ignored.txt
- QUEUECLEANER__RUNSEQUENTIALLY=true

# failed imports
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=false
- QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=false
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=title mismatch
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1=manual import required

# stalled downloads
- QUEUECLEANER__STALLED_MAX_STRIKES=5
- QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS=false
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=false
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false

# content blocker
- CONTENTBLOCKER__ENABLED=true
- CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH=/ignored.txt
- CONTENTBLOCKER__IGNORE_PRIVATE=false
- CONTENTBLOCKER__DELETE_PRIVATE=false

# download cleaner
- DOWNLOADCLEANER__ENABLED=true
- DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH=/ignored.txt
- DOWNLOADCLEANER__DELETE_PRIVATE=false

# remove seeding downloads
- DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr
- DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1
- DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME=0
Expand All @@ -209,6 +220,17 @@ services:
- DOWNLOADCLEANER__CATEGORIES__1__MAX_RATIO=-1
- DOWNLOADCLEANER__CATEGORIES__1__MIN_SEED_TIME=0
- DOWNLOADCLEANER__CATEGORIES__1__MAX_SEED_TIME=240
# remove downloads with no hardlinks
- DOWNLOADCLEANER__CATEGORIES__2__NAME=cleanuperr-unlinked
- DOWNLOADCLEANER__CATEGORIES__2__MAX_RATIO=-1
- DOWNLOADCLEANER__CATEGORIES__2__MIN_SEED_TIME=0
- DOWNLOADCLEANER__CATEGORIES__2__MAX_SEED_TIME=0

# change category for downloads with no hardlinks
- DOWNLOADCLEANER__UNLINKED_TARGET_CATEGORY=cleanuperr-unlinked
- DOWNLOADCLEANER__UNLINKED_IGNORED_ROOT_DIR=/downloads
- DOWNLOADCLEANER__UNLINKED_CATEGORIES__0=tv-sonarr
- DOWNLOADCLEANER__UNLINKED_CATEGORIES__1=radarr

- DOWNLOAD_CLIENT=none
# OR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Common.Configuration.DownloadCleaner;

public sealed record Category : IConfig
public sealed record CleanCategory : IConfig
{
public required string Name { get; init; }

Expand Down
40 changes: 37 additions & 3 deletions code/Common/Configuration/DownloadCleaner/DownloadCleanerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,24 @@ public sealed record DownloadCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
public const string SectionName = "DownloadCleaner";

public bool Enabled { get; init; }
public List<Category>? Categories { get; init; }

public List<CleanCategory>? Categories { get; init; }

[ConfigurationKeyName("DELETE_PRIVATE")]
public bool DeletePrivate { get; init; }

[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
public string? IgnoredDownloadsPath { get; init; }

[ConfigurationKeyName("UNLINKED_TARGET_CATEGORY")]
public string UnlinkedTargetCategory { get; init; } = "cleanuperr-unlinked";

[ConfigurationKeyName("UNLINKED_IGNORED_ROOT_DIR")]
public string UnlinkedIgnoredRootDir { get; init; } = string.Empty;

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

public void Validate()
{
if (!Enabled)
Expand All @@ -31,9 +40,34 @@ public void Validate()

if (Categories?.GroupBy(x => x.Name).Any(x => x.Count() > 1) is true)
{
throw new ValidationException("duplicated categories found");
throw new ValidationException("duplicated clean categories found");
}

Categories?.ForEach(x => x.Validate());

if (string.IsNullOrEmpty(UnlinkedTargetCategory))
{
return;
}

if (UnlinkedCategories?.Count is null or 0)
{
throw new ValidationException("no unlinked categories configured");
}

if (UnlinkedCategories.Contains(UnlinkedTargetCategory))
{
throw new ValidationException($"{SectionName.ToUpperInvariant()}__UNLINKED_TARGET_CATEGORY should not be present in {SectionName.ToUpperInvariant()}__UNLINKED_CATEGORIES");
}

if (UnlinkedCategories.Any(string.IsNullOrEmpty))
{
throw new ValidationException("empty unlinked category filter found");
}

if (!string.IsNullOrEmpty(UnlinkedIgnoredRootDir) && !Directory.Exists(UnlinkedIgnoredRootDir))
{
throw new ValidationException($"{UnlinkedIgnoredRootDir} root directory does not exist");
}
}
}
10 changes: 9 additions & 1 deletion code/Common/Configuration/Notification/NotificationConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ public abstract record NotificationConfig

[ConfigurationKeyName("ON_DOWNLOAD_CLEANED")]
public bool OnDownloadCleaned { get; init; }

[ConfigurationKeyName("ON_CATEGORY_CHANGED")]
public bool OnCategoryChanged { get; init; }

public bool IsEnabled => OnImportFailedStrike || OnStalledStrike || OnQueueItemDeleted || OnDownloadCleaned;
public bool IsEnabled =>
OnImportFailedStrike ||
OnStalledStrike ||
OnQueueItemDeleted ||
OnDownloadCleaned ||
OnCategoryChanged;

public abstract bool IsValid();
}
12 changes: 12 additions & 0 deletions code/Common/Exceptions/FatalException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Common.Exceptions;

public class FatalException : Exception
{
public FatalException()
{
}

public FatalException(string message) : base(message)
{
}
}
5 changes: 4 additions & 1 deletion code/Domain/Models/Deluge/Response/DownloadStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ public sealed record DownloadStatus
[JsonProperty("total_done")]
public long TotalDone { get; init; }

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

[JsonProperty("seeding_time")]
public long SeedingTime { get; init; }

public float Ratio { get; init; }

public required IReadOnlyList<Tracker> Trackers { get; init; }

[JsonProperty("download_location")]
public required string DownloadLocation { get; init; }
}

public sealed record Tracker
Expand Down
6 changes: 5 additions & 1 deletion code/Executable/DependencyInjection/MainDI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
.AddLogging(builder => builder.ClearProviders().AddConsole())
.AddHttpClients(configuration)
.AddConfiguration(configuration)
.AddMemoryCache()
.AddMemoryCache(options => {
options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);
})
.AddServices()
.AddQuartzServices(configuration)
.AddNotifications(configuration)
Expand All @@ -27,6 +29,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
config.AddConsumer<NotificationConsumer<StalledStrikeNotification>>();
config.AddConsumer<NotificationConsumer<QueueItemDeletedNotification>>();
config.AddConsumer<NotificationConsumer<DownloadCleanedNotification>>();
config.AddConsumer<NotificationConsumer<CategoryChangedNotification>>();

config.UsingInMemory((context, cfg) =>
{
Expand All @@ -36,6 +39,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
e.ConfigureConsumer<NotificationConsumer<StalledStrikeNotification>>(context);
e.ConfigureConsumer<NotificationConsumer<QueueItemDeletedNotification>>(context);
e.ConfigureConsumer<NotificationConsumer<DownloadCleanedNotification>>(context);
e.ConfigureConsumer<NotificationConsumer<CategoryChangedNotification>>(context);
e.ConcurrentMessageLimit = 1;
e.PrefetchCount = 1;
});
Expand Down
6 changes: 5 additions & 1 deletion code/Executable/DependencyInjection/ServicesDI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Infrastructure.Verticals.DownloadClient.Deluge;
using Infrastructure.Verticals.DownloadClient.QBittorrent;
using Infrastructure.Verticals.DownloadClient.Transmission;
using Infrastructure.Verticals.Files;
using Infrastructure.Verticals.ItemStriker;
using Infrastructure.Verticals.QueueCleaner;

Expand All @@ -27,14 +28,17 @@ public static IServiceCollection AddServices(this IServiceCollection services) =
.AddTransient<ContentBlocker>()
.AddTransient<DownloadCleaner>()
.AddTransient<IFilenameEvaluator, FilenameEvaluator>()
.AddTransient<IHardLinkFileService, HardLinkFileService>()
.AddTransient<UnixHardLinkFileService>()
.AddTransient<WindowsHardLinkFileService>()
.AddTransient<DummyDownloadService>()
.AddTransient<QBitService>()
.AddTransient<DelugeService>()
.AddTransient<TransmissionService>()
.AddTransient<ArrQueueIterator>()
.AddTransient<DownloadServiceFactory>()
.AddTransient<IStriker, Striker>()
.AddSingleton<BlocklistProvider>()
.AddSingleton<IStriker, Striker>()
.AddSingleton<IgnoredDownloadsProvider<QueueCleanerConfig>>()
.AddSingleton<IgnoredDownloadsProvider<ContentBlockerConfig>>()
.AddSingleton<IgnoredDownloadsProvider<DownloadCleanerConfig>>();
Expand Down
12 changes: 9 additions & 3 deletions code/Executable/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
"Enabled": true,
"RunSequentially": true,
"IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads",
"IMPORT_FAILED_MAX_STRIKES": 5,
"IMPORT_FAILED_MAX_STRIKES": 3,
"IMPORT_FAILED_IGNORE_PRIVATE": true,
"IMPORT_FAILED_DELETE_PRIVATE": false,
"IMPORT_FAILED_IGNORE_PATTERNS": [
"file is a sample"
],
"STALLED_MAX_STRIKES": 5,
"STALLED_MAX_STRIKES": 3,
"STALLED_RESET_STRIKES_ON_PROGRESS": true,
"STALLED_IGNORE_PRIVATE": true,
"STALLED_DELETE_PRIVATE": false,
Expand All @@ -51,9 +51,15 @@
"Name": "tv-sonarr",
"MAX_RATIO": -1,
"MIN_SEED_TIME": 0,
"MAX_SEED_TIME": -1
"MAX_SEED_TIME": 240
}
],
"UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
"UNLINKED_IGNORED_ROOT_DIR": "",
"UNLINKED_CATEGORIES": [
"tv-sonarr",
"radarr"
],
"IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads"
},
"DOWNLOAD_CLIENT": "qbittorrent",
Expand Down
3 changes: 3 additions & 0 deletions code/Executable/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
"Enabled": false,
"DELETE_PRIVATE": false,
"CATEGORIES": [],
"UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
"UNLINKED_IGNORED_ROOT_DIR": "",
"UNLINKED_CATEGORIES": [],
"IGNORED_DOWNLOADS_PATH": ""
},
"DOWNLOAD_CLIENT": "none",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using Common.Configuration.ContentBlocker;
using Common.Configuration.ContentBlocker;
using Common.Configuration.DownloadCleaner;
using Common.Configuration.QueueCleaner;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.Files;
using Infrastructure.Verticals.ItemStriker;
using Infrastructure.Verticals.Notifications;
using Microsoft.Extensions.Caching.Memory;
Expand Down Expand Up @@ -56,6 +57,7 @@ public TestDownloadService CreateSut(
var filenameEvaluator = Substitute.For<IFilenameEvaluator>();
var notifier = Substitute.For<INotificationPublisher>();
var dryRunInterceptor = Substitute.For<IDryRunInterceptor>();
var hardlinkFileService = Substitute.For<IHardLinkFileService>();

return new TestDownloadService(
Logger,
Expand All @@ -66,7 +68,8 @@ public TestDownloadService CreateSut(
filenameEvaluator,
Striker,
notifier,
dryRunInterceptor
dryRunInterceptor,
hardlinkFileService
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public ShouldCleanDownloadTests(DownloadServiceFixture fixture) : base(fixture)
public void WhenRatioAndMinSeedTimeReached_ShouldReturnTrue()
{
// Arrange
Category category = new()
CleanCategory category = new()
{
Name = "test",
MaxRatio = 1.0,
Expand All @@ -137,7 +137,7 @@ public void WhenRatioAndMinSeedTimeReached_ShouldReturnTrue()
public void WhenRatioReachedAndMinSeedTimeNotReached_ShouldReturnFalse()
{
// Arrange
Category category = new()
CleanCategory category = new()
{
Name = "test",
MaxRatio = 1.0,
Expand All @@ -163,7 +163,7 @@ public void WhenRatioReachedAndMinSeedTimeNotReached_ShouldReturnFalse()
public void WhenMaxSeedTimeReached_ShouldReturnTrue()
{
// Arrange
Category category = new()
CleanCategory category = new()
{
Name = "test",
MaxRatio = -1,
Expand All @@ -189,7 +189,7 @@ public void WhenMaxSeedTimeReached_ShouldReturnTrue()
public void WhenNeitherConditionMet_ShouldReturnFalse()
{
// Arrange
Category category = new()
CleanCategory category = new()
{
Name = "test",
MaxRatio = 2.0,
Expand Down
Loading