From 6bc59c83899cd258c41b86e7dc61ea4e1534711e Mon Sep 17 00:00:00 2001 From: Flaminel Date: Mon, 24 Mar 2025 15:50:56 +0200 Subject: [PATCH] added implementation for Deluge and Transmission --- .../DownloadClient/Deluge/DelugeService.cs | 111 +++++++++++++++- .../Transmission/TransmissionService.cs | 124 +++++++++++++++++- 2 files changed, 231 insertions(+), 4 deletions(-) diff --git a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs index 7eba628..51db8ff 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs @@ -280,9 +280,110 @@ public class DelugeService : DownloadService, IDelugeService throw new NotImplementedException(); } - public override Task ChangeCategoryForNoHardLinksAsync(List? downloads, HashSet excludedHashes) + public override async Task ChangeCategoryForNoHardLinksAsync(List? downloads, HashSet excludedHashes) { - throw new NotImplementedException(); + if (downloads?.Count is null or 0) + { + return; + } + + if (_downloadCleanerConfig.NoHardLinksIgnoreRootDir) + { + downloads + .Cast() + .Select(x => + { + string? firstDir = GetRootWithFirstDirectory(x.DownloadPath); + + if (string.IsNullOrEmpty(firstDir)) + { + return string.Empty; + } + + if (firstDir == Path.GetPathRoot(x.DownloadPath)) + { + return string.Empty; + } + + return firstDir; + }) + .Where(x => !string.IsNullOrEmpty(x)) + .Distinct() + .ToList() + .ForEach(x => + { + _logger.LogTrace("populating file counts from {dir}", x); + + if (!Directory.Exists(x)) + { + throw new ValidationException($"directory \"{x}\" does not exist"); + } + + _hardLinkFileService.PopulateFileCounts(x); + }); + } + + foreach (TorrentStatus download in downloads.Cast()) + { + if (string.IsNullOrEmpty(download.Hash)) + { + _logger.LogDebug("skip | download hash is null for {name}", download.Name); + continue; + } + + if (excludedHashes.Any(x => x.Equals(download.Hash, StringComparison.InvariantCultureIgnoreCase))) + { + _logger.LogDebug("skip | download is used by an arr | {name}", download.Name); + continue; + } + + ContextProvider.Set("downloadName", download.Name); + ContextProvider.Set("hash", download.Hash); + + DelugeContents? contents = null; + try + { + contents = await _client.GetTorrentFiles(download.Hash); + } + catch (Exception exception) + { + _logger.LogDebug(exception, "failed to find torrent files for {name}", download.Name); + continue; + } + + bool hasHardlinks = false; + + ProcessFiles(contents?.Contents, (name, file) => + { + string filePath = Path.Combine(download.DownloadPath, file.Path); + + long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.NoHardLinksIgnoreRootDir); + + if (hardlinkCount < 0) + { + _logger.LogDebug("skip | could not get file properties | {name}", download.Name); + hasHardlinks = true; + return; + } + + if (hardlinkCount > 0) + { + hasHardlinks = true; + } + }); + + if (hasHardlinks) + { + _logger.LogDebug("skip | download has hardlinks | {name}", download.Name); + continue; + } + + await _dryRunInterceptor.InterceptAsync(ChangeLabel, download.Hash, _downloadCleanerConfig.NoHardLinksCategory); + + _logger.LogInformation("category changed for {name}", download.Name); + + await _notifier.NotifyCategoryChanged(download.Label, _downloadCleanerConfig.NoHardLinksCategory); + } } /// @@ -300,6 +401,12 @@ public class DelugeService : DownloadService, IDelugeService await _client.ChangeFilesPriority(hash, sortedPriorities); } + [DryRunSafeguard] + protected virtual async Task ChangeLabel(string hash, string newLabel) + { + await _client.SetTorrentLabel(hash, newLabel); + } + private async Task IsItemStuckAndShouldRemove(TorrentStatus status) { if (_queueCleanerConfig.StalledMaxStrikes is 0) diff --git a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs index 010439b..e8deeee 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs @@ -5,6 +5,7 @@ using Common.Configuration.ContentBlocker; using Common.Configuration.DownloadCleaner; using Common.Configuration.DownloadClient; using Common.Configuration.QueueCleaner; +using Common.Exceptions; using Common.Helpers; using Domain.Enums; using Infrastructure.Interceptors; @@ -304,9 +305,128 @@ public class TransmissionService : DownloadService, ITransmissionService throw new NotImplementedException(); } - public override Task ChangeCategoryForNoHardLinksAsync(List? downloads, HashSet excludedHashes) + public override async Task ChangeCategoryForNoHardLinksAsync(List? downloads, HashSet excludedHashes) { - throw new NotImplementedException(); + if (downloads?.Count is null or 0) + { + return; + } + + if (_downloadCleanerConfig.NoHardLinksIgnoreRootDir) + { + downloads + .Cast() + .Select(x => + { + if (x.DownloadDir == null) + { + return string.Empty; + } + + string? firstDir = GetRootWithFirstDirectory(x.DownloadDir); + + if (string.IsNullOrEmpty(firstDir)) + { + return string.Empty; + } + + if (firstDir == Path.GetPathRoot(x.DownloadDir)) + { + return string.Empty; + } + + return firstDir; + }) + .Where(x => !string.IsNullOrEmpty(x)) + .Distinct() + .ToList() + .ForEach(x => + { + _logger.LogTrace("populating file counts from {dir}", x); + + if (!Directory.Exists(x)) + { + throw new ValidationException($"directory \"{x}\" does not exist"); + } + + _hardLinkFileService.PopulateFileCounts(x); + }); + } + + foreach (TorrentInfo download in downloads.Cast()) + { + if (string.IsNullOrEmpty(download.HashString) || download.DownloadDir == null) + { + _logger.LogDebug("skip | download hash or download directory is null for {name}", download.Name); + continue; + } + + if (excludedHashes.Any(x => x.Equals(download.HashString, StringComparison.InvariantCultureIgnoreCase))) + { + _logger.LogDebug("skip | download is used by an arr | {name}", download.Name); + continue; + } + + ContextProvider.Set("downloadName", download.Name); + ContextProvider.Set("hash", download.HashString); + + bool hasHardlinks = false; + + if (download.Files != null) + { + foreach (TransmissionTorrentFiles file in download.Files) + { + string filePath = Path.Combine(download.DownloadDir, file.Name); + + long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.NoHardLinksIgnoreRootDir); + + if (hardlinkCount < 0) + { + _logger.LogDebug("skip | could not get file properties | {name}", download.Name); + hasHardlinks = true; + break; + } + + if (hardlinkCount > 0) + { + hasHardlinks = true; + break; + } + } + } + + if (hasHardlinks) + { + _logger.LogDebug("skip | download has hardlinks | {name}", download.Name); + continue; + } + + // Get the current category (directory name) + string currentCategory = Path.GetFileName(Path.TrimEndingDirectorySeparator(download.DownloadDir)); + + // Create the new location path + string newLocation = Path.Combine( + Path.GetDirectoryName(Path.TrimEndingDirectorySeparator(download.DownloadDir)) ?? string.Empty, + _downloadCleanerConfig.NoHardLinksCategory + ); + + await _dryRunInterceptor.InterceptAsync(MoveDownload, download.Id, newLocation); + + _logger.LogInformation("category changed for {name}", download.Name); + + await _notifier.NotifyCategoryChanged(currentCategory, _downloadCleanerConfig.NoHardLinksCategory); + } + } + + [DryRunSafeguard] + protected virtual async Task MoveDownload(long downloadId, string newLocation) + { + await _client.TorrentSetAsync(new TorrentSettings + { + Ids = [downloadId], + Location = newLocation, + // Move = true + }); } public override async Task DeleteDownload(string hash)