mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-01-16 09:48:13 -05:00
279 lines
10 KiB
C#
279 lines
10 KiB
C#
using Cleanuparr.Domain.Enums;
|
|
using Cleanuparr.Infrastructure.Extensions;
|
|
using Cleanuparr.Infrastructure.Features.Context;
|
|
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
|
using Microsoft.Extensions.Logging;
|
|
using QBittorrent.Client;
|
|
|
|
namespace Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
|
|
|
public partial class QBitService
|
|
{
|
|
/// <inheritdoc/>
|
|
public override async Task<List<object>?> GetSeedingDownloads()
|
|
{
|
|
var torrentList = await _client.GetTorrentListAsync(new TorrentListQuery { Filter = TorrentListFilter.Seeding });
|
|
return torrentList?.Where(x => !string.IsNullOrEmpty(x.Hash))
|
|
.Cast<object>()
|
|
.ToList();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override List<object>? FilterDownloadsToBeCleanedAsync(List<object>? downloads, List<CleanCategory> categories) =>
|
|
downloads
|
|
?.Cast<TorrentInfo>()
|
|
.Where(x => !string.IsNullOrEmpty(x.Hash))
|
|
.Where(x => categories.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
|
.Cast<object>()
|
|
.ToList();
|
|
|
|
/// <inheritdoc/>
|
|
public override List<object>? FilterDownloadsToChangeCategoryAsync(List<object>? downloads, List<string> categories)
|
|
{
|
|
var downloadCleanerConfig = ContextProvider.Get<DownloadCleanerConfig>(nameof(DownloadCleanerConfig));
|
|
|
|
return downloads
|
|
?.Cast<TorrentInfo>()
|
|
.Where(x => !string.IsNullOrEmpty(x.Hash))
|
|
.Where(x => categories.Any(cat => cat.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
|
.Where(x =>
|
|
{
|
|
if (downloadCleanerConfig.UnlinkedUseTag)
|
|
{
|
|
return !x.Tags.Any(tag =>
|
|
tag.Equals(downloadCleanerConfig.UnlinkedTargetCategory, StringComparison.InvariantCultureIgnoreCase));
|
|
}
|
|
|
|
return true;
|
|
})
|
|
.Cast<object>()
|
|
.ToList();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async Task CleanDownloadsAsync(List<object>? downloads, List<CleanCategory> categoriesToClean,
|
|
HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads)
|
|
{
|
|
if (downloads?.Count is null or 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (TorrentInfo download in downloads)
|
|
{
|
|
if (string.IsNullOrEmpty(download.Hash))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (excludedHashes.Any(x => x.Equals(download.Hash, StringComparison.InvariantCultureIgnoreCase)))
|
|
{
|
|
_logger.LogDebug("skip | download is used by an arr | {name}", download.Name);
|
|
continue;
|
|
}
|
|
|
|
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(download.Hash);
|
|
|
|
if (ignoredDownloads.Count > 0 &&
|
|
(download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads))))
|
|
{
|
|
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
|
continue;
|
|
}
|
|
|
|
CleanCategory? category = categoriesToClean
|
|
.FirstOrDefault(x => download.Category.Equals(x.Name, StringComparison.InvariantCultureIgnoreCase));
|
|
|
|
if (category is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var downloadCleanerConfig = ContextProvider.Get<DownloadCleanerConfig>(nameof(DownloadCleanerConfig));
|
|
|
|
if (!downloadCleanerConfig.DeletePrivate)
|
|
{
|
|
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(download.Hash);
|
|
|
|
if (torrentProperties is null)
|
|
{
|
|
_logger.LogDebug("failed to find torrent properties in the download client | {name}", download.Name);
|
|
return;
|
|
}
|
|
|
|
bool isPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) &&
|
|
bool.TryParse(dictValue?.ToString(), out bool boolValue)
|
|
&& boolValue;
|
|
|
|
if (isPrivate)
|
|
{
|
|
_logger.LogDebug("skip | download is private | {name}", download.Name);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
ContextProvider.Set("downloadName", download.Name);
|
|
ContextProvider.Set("hash", download.Hash);
|
|
|
|
SeedingCheckResult result = ShouldCleanDownload(download.Ratio, download.SeedingTime ?? TimeSpan.Zero, category);
|
|
|
|
if (!result.ShouldClean)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
await _dryRunInterceptor.InterceptAsync(DeleteDownload, download.Hash);
|
|
|
|
_logger.LogInformation(
|
|
"download cleaned | {reason} reached | {name}",
|
|
result.Reason is CleanReason.MaxRatioReached
|
|
? "MAX_RATIO & MIN_SEED_TIME"
|
|
: "MAX_SEED_TIME",
|
|
download.Name
|
|
);
|
|
|
|
await _eventPublisher.PublishDownloadCleaned(download.Ratio, download.SeedingTime ?? TimeSpan.Zero, category.Name, result.Reason);
|
|
}
|
|
}
|
|
|
|
public override async Task CreateCategoryAsync(string name)
|
|
{
|
|
IReadOnlyDictionary<string, Category>? existingCategories = await _client.GetCategoriesAsync();
|
|
|
|
if (existingCategories.Any(x => x.Value.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_logger.LogDebug("Creating category {name}", name);
|
|
|
|
await _dryRunInterceptor.InterceptAsync(CreateCategory, name);
|
|
}
|
|
|
|
public override async Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads)
|
|
{
|
|
if (downloads?.Count is null or 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var downloadCleanerConfig = ContextProvider.Get<DownloadCleanerConfig>(nameof(DownloadCleanerConfig));
|
|
|
|
if (!string.IsNullOrEmpty(downloadCleanerConfig.UnlinkedIgnoredRootDir))
|
|
{
|
|
_hardLinkFileService.PopulateFileCounts(downloadCleanerConfig.UnlinkedIgnoredRootDir);
|
|
}
|
|
|
|
foreach (TorrentInfo download in downloads)
|
|
{
|
|
if (string.IsNullOrEmpty(download.Hash))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (excludedHashes.Any(x => x.Equals(download.Hash, StringComparison.InvariantCultureIgnoreCase)))
|
|
{
|
|
_logger.LogDebug("skip | download is used by an arr | {name}", download.Name);
|
|
continue;
|
|
}
|
|
|
|
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(download.Hash);
|
|
|
|
if (ignoredDownloads.Count > 0 &&
|
|
(download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads))))
|
|
{
|
|
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
|
continue;
|
|
}
|
|
|
|
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(download.Hash);
|
|
|
|
if (files is null)
|
|
{
|
|
_logger.LogDebug("failed to find files for {name}", download.Name);
|
|
continue;
|
|
}
|
|
|
|
ContextProvider.Set("downloadName", download.Name);
|
|
ContextProvider.Set("hash", download.Hash);
|
|
bool hasHardlinks = false;
|
|
|
|
foreach (TorrentContent file in files)
|
|
{
|
|
if (!file.Index.HasValue)
|
|
{
|
|
_logger.LogDebug("skip | file index is null for {name}", download.Name);
|
|
hasHardlinks = true;
|
|
break;
|
|
}
|
|
|
|
string filePath = string.Join(Path.DirectorySeparatorChar, Path.Combine(download.SavePath, file.Name).Split(['\\', '/']));
|
|
|
|
if (file.Priority is TorrentContentPriority.Skip)
|
|
{
|
|
_logger.LogDebug("skip | file is not downloaded | {file}", filePath);
|
|
continue;
|
|
}
|
|
|
|
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, !string.IsNullOrEmpty(downloadCleanerConfig.UnlinkedIgnoredRootDir));
|
|
|
|
if (hardlinkCount < 0)
|
|
{
|
|
_logger.LogDebug("skip | could not get file properties | {file}", filePath);
|
|
hasHardlinks = true;
|
|
break;
|
|
}
|
|
|
|
if (hardlinkCount > 0)
|
|
{
|
|
hasHardlinks = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasHardlinks)
|
|
{
|
|
_logger.LogDebug("skip | download has hardlinks | {name}", download.Name);
|
|
continue;
|
|
}
|
|
|
|
await _dryRunInterceptor.InterceptAsync(ChangeCategory, download.Hash, downloadCleanerConfig.UnlinkedTargetCategory);
|
|
|
|
if (downloadCleanerConfig.UnlinkedUseTag)
|
|
{
|
|
_logger.LogInformation("tag added for {name}", download.Name);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation("category changed for {name}", download.Name);
|
|
download.Category = downloadCleanerConfig.UnlinkedTargetCategory;
|
|
}
|
|
|
|
await _eventPublisher.PublishCategoryChanged(download.Category, downloadCleanerConfig.UnlinkedTargetCategory, downloadCleanerConfig.UnlinkedUseTag);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async Task DeleteDownload(string hash)
|
|
{
|
|
await _client.DeleteAsync([hash], deleteDownloadedData: true);
|
|
}
|
|
|
|
protected async Task CreateCategory(string name)
|
|
{
|
|
await _client.AddCategoryAsync(name);
|
|
}
|
|
|
|
protected virtual async Task ChangeCategory(string hash, string newCategory)
|
|
{
|
|
var downloadCleanerConfig = ContextProvider.Get<DownloadCleanerConfig>(nameof(DownloadCleanerConfig));
|
|
|
|
if (downloadCleanerConfig.UnlinkedUseTag)
|
|
{
|
|
await _client.AddTorrentTagAsync([hash], newCategory);
|
|
return;
|
|
}
|
|
|
|
await _client.SetTorrentCategoryAsync([hash], newCategory);
|
|
}
|
|
} |