mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-06-23 04:59:46 -04:00
Add Transmission label option for unlinked downloads (#626)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using NSubstitute;
|
||||
using Transmission.API.RPC.Arguments;
|
||||
using Transmission.API.RPC.Entity;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
@@ -364,6 +365,50 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
result.ShouldNotBeNull();
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExcludesAlreadyLabeled_WhenUseTag()
|
||||
{
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var downloads = new List<Domain.Entities.ITorrentItemWrapper>
|
||||
{
|
||||
new TransmissionItemWrapper(new TorrentInfo { HashString = "hash1", DownloadDir = "/downloads/movies", Labels = ["unlinked"] }),
|
||||
new TransmissionItemWrapper(new TorrentInfo { HashString = "hash2", DownloadDir = "/downloads/movies", Labels = [] })
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = sut.FilterDownloadsToChangeCategoryAsync(downloads,
|
||||
new UnlinkedConfig { Categories = ["movies"], TargetCategory = "unlinked", UseTag = true });
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.ShouldHaveSingleItem();
|
||||
result[0].Hash.ShouldBe("hash2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExcludesAlreadyLabeled_WhenUseTag_CaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var downloads = new List<Domain.Entities.ITorrentItemWrapper>
|
||||
{
|
||||
new TransmissionItemWrapper(new TorrentInfo { HashString = "hash1", DownloadDir = "/downloads/movies", Labels = ["UNLINKED"] }),
|
||||
new TransmissionItemWrapper(new TorrentInfo { HashString = "hash2", DownloadDir = "/downloads/movies", Labels = [] })
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = sut.FilterDownloadsToChangeCategoryAsync(downloads,
|
||||
new UnlinkedConfig { Categories = ["movies"], TargetCategory = "unlinked", UseTag = true });
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.ShouldHaveSingleItem();
|
||||
result[0].Hash.ShouldBe("hash2");
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateCategoryAsync_Tests : TransmissionServiceDCTests
|
||||
@@ -671,6 +716,92 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
.TorrentSetLocationAsync(Arg.Is<long[]>(ids => ids.Contains(123)), expectedNewLocation, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTag_SetsLabel_AndDoesNotChangeLocation()
|
||||
{
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var unlinkedConfig = new UnlinkedConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TargetCategory = "unlinked",
|
||||
UseTag = true
|
||||
};
|
||||
|
||||
var downloads = new List<Domain.Entities.ITorrentItemWrapper>
|
||||
{
|
||||
new TransmissionItemWrapper(new TorrentInfo
|
||||
{
|
||||
Id = 123,
|
||||
HashString = "hash1",
|
||||
Name = "Test",
|
||||
DownloadDir = Path.Combine("downloads", "movies"),
|
||||
Labels = ["existing"],
|
||||
Files = new[] { new TransmissionTorrentFiles { Name = "file1.mkv" } },
|
||||
FileStats = new[] { new TransmissionTorrentFileStats { Wanted = true } }
|
||||
})
|
||||
};
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.TorrentSetAsync(Arg.Is<TorrentSettings>(s =>
|
||||
s.Ids.Contains(123L)
|
||||
&& s.Labels.Contains("existing")
|
||||
&& s.Labels.Contains("unlinked")));
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTag_DoesNotDuplicateLabel_WhenAlreadyPresentWithDifferentCase()
|
||||
{
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var unlinkedConfig = new UnlinkedConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TargetCategory = "unlinked",
|
||||
UseTag = true
|
||||
};
|
||||
|
||||
var downloads = new List<Domain.Entities.ITorrentItemWrapper>
|
||||
{
|
||||
new TransmissionItemWrapper(new TorrentInfo
|
||||
{
|
||||
Id = 123,
|
||||
HashString = "hash1",
|
||||
Name = "Test",
|
||||
DownloadDir = Path.Combine("downloads", "movies"),
|
||||
Labels = ["UNLINKED"],
|
||||
Files = new[] { new TransmissionTorrentFiles { Name = "file1.mkv" } },
|
||||
FileStats = new[] { new TransmissionTorrentFileStats { Wanted = true } }
|
||||
})
|
||||
};
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.TorrentSetAsync(Arg.Is<TorrentSettings>(s =>
|
||||
s.Ids.Contains(123L)
|
||||
&& s.Labels.Length == 1
|
||||
&& s.Labels.Contains("UNLINKED")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasHardlinks_SkipsTorrent()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Transmission.API.RPC.Arguments;
|
||||
using Transmission.API.RPC.Entity;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
@@ -43,6 +44,16 @@ public partial class TransmissionService
|
||||
return downloads
|
||||
?.Where(x => !string.IsNullOrEmpty(x.Hash))
|
||||
.Where(x => unlinkedConfig.Categories.Any(cat => cat.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
||||
.Where(x =>
|
||||
{
|
||||
if (unlinkedConfig.UseTag)
|
||||
{
|
||||
return !x.Tags.Any(tag =>
|
||||
tag.Equals(unlinkedConfig.TargetCategory, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -128,6 +139,23 @@ public partial class TransmissionService
|
||||
}
|
||||
|
||||
string currentCategory = torrent.Category ?? string.Empty;
|
||||
|
||||
if (unlinkedConfig.UseTag)
|
||||
{
|
||||
string[] newLabels = torrent.Tags
|
||||
.Append(unlinkedConfig.TargetCategory)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeLabels(torrent.Info.Id, newLabels));
|
||||
|
||||
_logger.LogInformation("label added for {name}", torrent.Name);
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(currentCategory, unlinkedConfig.TargetCategory, isTag: true);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
string newLocation = torrent.Info.GetNewLocationByAppend(unlinkedConfig.TargetCategory);
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeDownloadLocation(torrent.Info.Id, newLocation));
|
||||
@@ -144,4 +172,13 @@ public partial class TransmissionService
|
||||
{
|
||||
await _client.TorrentSetLocationAsync([downloadId], newLocation, true);
|
||||
}
|
||||
|
||||
protected virtual async Task ChangeLabels(long downloadId, string[] labels)
|
||||
{
|
||||
await _client.TorrentSetAsync(new TorrentSettings
|
||||
{
|
||||
Ids = [downloadId],
|
||||
Labels = labels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,10 +172,10 @@
|
||||
(valueChange)="updateUnlinkedField('targetCategory', $event)"
|
||||
hint="Category to move unlinked downloads to. You have to create a seeding rule for this category if you want to remove the downloads."
|
||||
helpKey="download-cleaner:unlinkedTargetCategory" />
|
||||
@if (isSelectedClientQBittorrent()) {
|
||||
<app-toggle label="Use Tag Instead" [checked]="client.unlinkedConfig?.useTag ?? false"
|
||||
@if (isTagFilterableClient()) {
|
||||
<app-toggle [label]="isSelectedClientTransmission() ? 'Use Label Instead' : 'Use Tag Instead'" [checked]="client.unlinkedConfig?.useTag ?? false"
|
||||
(checkedChange)="updateUnlinkedField('useTag', $event)"
|
||||
hint="When enabled, uses a tag instead of category (qBittorrent only)"
|
||||
[hint]="isSelectedClientTransmission() ? 'When enabled, adds a label instead of changing the category' : 'When enabled, adds a tag instead of changing the category'"
|
||||
helpKey="download-cleaner:unlinkedUseTag" />
|
||||
}
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ Category to move unlinked downloads to. You must create a seeding rule for this
|
||||
title="Use Tag"
|
||||
>
|
||||
|
||||
When enabled, uses a tag instead of category for marking unlinked downloads (qBittorrent only).
|
||||
When enabled, marks unlinked downloads with a tag instead of changing their category, preserving the original category. Supported by **qBittorrent** (tags) and **Transmission** (labels); not available for other clients.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user