Files
Cleanuparr/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/QBitServiceDC.cs
T

193 lines
7.0 KiB
C#

using Cleanuparr.Domain.Entities;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Cleanuparr.Shared.Helpers;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using QBittorrent.Client;
namespace Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
public partial class QBitService
{
/// <inheritdoc/>
public override async Task<List<ITorrentItemWrapper>> GetSeedingDownloads()
{
var torrentList = await _client.GetTorrentListAsync(new TorrentListQuery { Filter = TorrentListFilter.Completed });
if (torrentList is null)
{
return [];
}
var result = new List<ITorrentItemWrapper>();
foreach (var torrent in torrentList.Where(x => !string.IsNullOrEmpty(x.Hash)))
{
IReadOnlyList<TorrentTracker>? trackers = await GetTrackersAsync(torrent.Hash);
TorrentProperties? properties = await _client.GetTorrentPropertiesAsync(torrent.Hash);
if (trackers is null || properties is null)
{
_logger.LogDebug("skip | torrent no longer exists in the download client | {Name}", torrent.Name);
continue;
}
bool isPrivate = properties.AdditionalData.TryGetValue("is_private", out JToken? dictValue) &&
bool.TryParse(dictValue?.ToString(), out bool boolValue) && boolValue;
result.Add(new QBitItemWrapper(torrent, trackers, isPrivate));
}
return result;
}
/// <inheritdoc/>
public override async Task<List<IDownloadItem>> GetAllDownloadsLite()
{
IReadOnlyList<TorrentInfo>? torrentList = await _client.GetTorrentListAsync(new TorrentListQuery());
if (torrentList is null)
{
throw new InvalidOperationException("qBittorrent returned no torrent list");
}
List<IDownloadItem> torrents = torrentList
.Where(x => !string.IsNullOrEmpty(x.Hash))
.Select(IDownloadItem (t) => new QBitItemWrapper(t, [], false))
.ToList();
ThrowIfTorrentListCollapsed(torrentList.Count, torrents.Count);
return torrents;
}
/// <inheritdoc/>
public override Task<IReadOnlyList<string>> GetClaimedPathsAsync(IReadOnlyList<IDownloadItem> downloads) =>
BuildClaimedPathsAsync(downloads, async torrent =>
{
if (string.IsNullOrEmpty(torrent.DownloadId))
{
return [];
}
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(torrent.DownloadId);
return files?.Select(f => f.Name).Where(name => !string.IsNullOrEmpty(name)).ToList() ?? [];
});
/// <inheritdoc/>
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules) =>
downloads
?.Where(x => !string.IsNullOrEmpty(x.DownloadId))
.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
.ToList();
/// <inheritdoc/>
public override List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig)
{
return downloads
?.Where(x => !string.IsNullOrEmpty(x.DownloadId))
.Where(x => unlinkedConfig.Categories.Any(cat => cat.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
.Where(x =>
{
if (unlinkedConfig.UseTag && x is QBitItemWrapper qBitItemWrapper)
{
return !qBitItemWrapper.Tags.Any(tag =>
tag.Equals(unlinkedConfig.TargetCategory, StringComparison.InvariantCultureIgnoreCase));
}
return true;
})
.ToList();
}
/// <inheritdoc/>
public override async Task DeleteDownload(IDownloadItem item, bool deleteSourceFiles)
{
await _client.DeleteAsync([item.DownloadId], deleteSourceFiles);
}
/// <inheritdoc/>
public override async Task StopDownload(IDownloadItem item)
{
await _client.PauseAsync([item.DownloadId]);
}
/// <inheritdoc cref="IErrorRecoveryCapable.ResumeDownload"/>
public async Task ResumeDownload(IDownloadItem item)
{
await _client.ResumeAsync([item.DownloadId]);
}
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));
}
/// <inheritdoc/>
protected override async Task<IEnumerable<(string FilePath, HardLinkScanAction Action)>?> GetHardLinkScanItemsAsync(ITorrentItemWrapper torrent)
{
QBitItemWrapper qBitTorrent = (QBitItemWrapper)torrent;
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(qBitTorrent.DownloadId);
if (files is null)
{
_logger.LogDebug("failed to find files for {Name}", qBitTorrent.Name);
return null;
}
IEnumerable<(string FilePath, HardLinkScanAction Action)> BuildScanItems()
{
foreach (TorrentContent file in files)
{
if (!file.Index.HasValue)
{
_logger.LogDebug("skip | file index is null for {Name}", qBitTorrent.Name);
yield return (string.Empty, HardLinkScanAction.TreatAsLinked);
yield break;
}
string filePath = PathHelper.NormalizeAndRemap(
Path.Combine(qBitTorrent.Info.SavePath, file.Name),
_downloadClientConfig.DownloadDirectorySource,
_downloadClientConfig.DownloadDirectoryTarget);
yield return (filePath, file.Priority is TorrentContentPriority.Skip
? HardLinkScanAction.SkipUnwanted
: HardLinkScanAction.CheckHardLinks);
}
}
return BuildScanItems();
}
/// <inheritdoc/>
protected override bool SupportsTags => true;
/// <inheritdoc/>
protected override async Task ChangeCategoryInClientAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag) =>
await ChangeCategory(torrent.DownloadId, targetCategory, useTag);
protected async Task CreateCategory(string name)
{
await _client.AddCategoryAsync(name);
}
protected virtual async Task ChangeCategory(string hash, string newCategory, bool useTag)
{
if (useTag)
{
await _client.AddTorrentTagAsync([hash], newCategory);
return;
}
await _client.SetTorrentCategoryAsync([hash], newCategory);
}
}