diff --git a/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor b/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor index c2518d0..2104b64 100644 --- a/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor +++ b/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor @@ -1,33 +1,50 @@ - +@using Lantean.QBitTorrentClient.Models + + - + - - Manual - Automatic + + Manual + Automatic + + + + + + - + @if (ShowCookieOption) { - + } - + - - @foreach (var category in Categories) + + None + @foreach (var category in CategoryOptions) { - @category + @category.Name + } + + + + + @foreach (var tag in AvailableTags) + { + @tag } @@ -38,7 +55,7 @@ - + None Metadata received Files checked @@ -47,22 +64,58 @@ - - Original - Create subfolder - Don't create subfolder' - - + + Original + Create subfolder + Don't create subfolder + + + + - + - + + + + Use global share limit + Set no share limit + Set custom share limit + + + + + + + + + + + + + + + + + + + + + + + Default + Stop torrent + Remove torrent + Remove torrent and data + Enable super seeding + + - \ No newline at end of file + diff --git a/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor.cs b/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor.cs index 9e51e2a..ab606b8 100644 --- a/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor.cs +++ b/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor.cs @@ -1,4 +1,10 @@ -using Lantean.QBitTorrentClient; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Lantean.QBitTorrentClient; +using Lantean.QBitTorrentClient.Models; using Lantean.QBTMud.Models; using Microsoft.AspNetCore.Components; @@ -6,6 +12,15 @@ namespace Lantean.QBTMud.Components.Dialogs { public partial class AddTorrentOptions { + private readonly List _categoryOptions = new(); + private readonly Dictionary _categoryLookup = new(StringComparer.Ordinal); + private string _manualSavePath = string.Empty; + private bool _manualUseDownloadPath; + private string _manualDownloadPath = string.Empty; + private string _defaultSavePath = string.Empty; + private string _defaultDownloadPath = string.Empty; + private bool _defaultDownloadPathEnabled; + [Inject] protected IApiClient ApiClient { get; set; } = default!; @@ -16,15 +31,25 @@ namespace Lantean.QBTMud.Components.Dialogs protected bool TorrentManagementMode { get; set; } - protected string SavePath { get; set; } = default!; + protected string SavePath { get; set; } = string.Empty; + + protected string DownloadPath { get; set; } = string.Empty; + + protected bool UseDownloadPath { get; set; } + + protected bool DownloadPathDisabled => TorrentManagementMode || !UseDownloadPath; protected string? Cookie { get; set; } protected string? RenameTorrent { get; set; } - protected IEnumerable Categories { get; set; } = []; + protected IReadOnlyList CategoryOptions => _categoryOptions; - protected string? Category { get; set; } + protected string? Category { get; set; } = string.Empty; + + protected List AvailableTags { get; private set; } = []; + + protected HashSet SelectedTags { get; private set; } = new(StringComparer.Ordinal); protected bool StartTorrent { get; set; } = true; @@ -32,41 +57,264 @@ namespace Lantean.QBTMud.Components.Dialogs protected string StopCondition { get; set; } = "None"; - protected bool SkipHashCheck { get; set; } = false; + protected bool SkipHashCheck { get; set; } protected string ContentLayout { get; set; } = "Original"; - protected bool DownloadInSequentialOrder { get; set; } = false; + protected bool DownloadInSequentialOrder { get; set; } - protected bool DownloadFirstAndLastPiecesFirst { get; set; } = false; + protected bool DownloadFirstAndLastPiecesFirst { get; set; } protected long DownloadLimit { get; set; } protected long UploadLimit { get; set; } + protected ShareLimitMode SelectedShareLimitMode { get; set; } = ShareLimitMode.Global; + + protected bool RatioLimitEnabled { get; set; } + + protected float RatioLimit { get; set; } = 1.0f; + + protected bool SeedingTimeLimitEnabled { get; set; } + + protected int SeedingTimeLimit { get; set; } = 1440; + + protected bool InactiveSeedingTimeLimitEnabled { get; set; } + + protected int InactiveSeedingTimeLimit { get; set; } = 1440; + + protected ShareLimitAction SelectedShareLimitAction { get; set; } = ShareLimitAction.Default; + + protected bool IsCustomShareLimit => SelectedShareLimitMode == ShareLimitMode.Custom; + protected override async Task OnInitializedAsync() { var categories = await ApiClient.GetAllCategories(); - Categories = categories.Select(c => c.Key).ToList(); + foreach (var (name, value) in categories.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)) + { + var option = new CategoryOption(name, value.SavePath, value.DownloadPath); + _categoryOptions.Add(option); + _categoryLookup[name] = option; + } + + var tags = await ApiClient.GetAllTags(); + AvailableTags = tags.OrderBy(t => t, StringComparer.OrdinalIgnoreCase).ToList(); var preferences = await ApiClient.GetApplicationPreferences(); TorrentManagementMode = preferences.AutoTmmEnabled; - SavePath = preferences.SavePath; + + _defaultSavePath = preferences.SavePath ?? string.Empty; + _manualSavePath = _defaultSavePath; + SavePath = _defaultSavePath; + + _defaultDownloadPath = preferences.TempPath ?? string.Empty; + _defaultDownloadPathEnabled = preferences.TempPathEnabled; + _manualDownloadPath = _defaultDownloadPath; + _manualUseDownloadPath = preferences.TempPathEnabled; + UseDownloadPath = _manualUseDownloadPath; + DownloadPath = UseDownloadPath ? _manualDownloadPath : string.Empty; + StartTorrent = !preferences.AddStoppedEnabled; AddToTopOfQueue = preferences.AddToTopOfQueue; StopCondition = preferences.TorrentStopCondition; ContentLayout = preferences.TorrentContentLayout; + + RatioLimitEnabled = preferences.MaxRatioEnabled; + RatioLimit = preferences.MaxRatio; + SeedingTimeLimitEnabled = preferences.MaxSeedingTimeEnabled; + if (preferences.MaxSeedingTimeEnabled) + { + SeedingTimeLimit = preferences.MaxSeedingTime; + } + InactiveSeedingTimeLimitEnabled = preferences.MaxInactiveSeedingTimeEnabled; + if (preferences.MaxInactiveSeedingTimeEnabled) + { + InactiveSeedingTimeLimit = preferences.MaxInactiveSeedingTime; + } + SelectedShareLimitAction = MapShareLimitAction(preferences.MaxRatioAct); + + if (TorrentManagementMode) + { + ApplyAutomaticPaths(); + } + } + + protected Task SetTorrentManagementMode(bool value) + { + if (TorrentManagementMode == value) + { + return Task.CompletedTask; + } + + TorrentManagementMode = value; + if (TorrentManagementMode) + { + ApplyAutomaticPaths(); + } + else + { + RestoreManualPaths(); + } + + return Task.CompletedTask; + } + + protected Task SavePathChanged(string value) + { + SavePath = value; + if (!TorrentManagementMode) + { + _manualSavePath = value; + } + + return Task.CompletedTask; + } + + protected Task SetUseDownloadPath(bool value) + { + if (TorrentManagementMode) + { + return Task.CompletedTask; + } + + _manualUseDownloadPath = value; + UseDownloadPath = value; + + if (value) + { + if (string.IsNullOrWhiteSpace(_manualDownloadPath)) + { + _manualDownloadPath = string.IsNullOrWhiteSpace(_defaultDownloadPath) ? string.Empty : _defaultDownloadPath; + } + + DownloadPath = _manualDownloadPath; + } + else + { + _manualDownloadPath = DownloadPath; + DownloadPath = string.Empty; + } + + return Task.CompletedTask; + } + + protected Task DownloadPathChanged(string value) + { + DownloadPath = value; + if (!TorrentManagementMode && UseDownloadPath) + { + _manualDownloadPath = value; + } + + return Task.CompletedTask; + } + + protected Task CategoryChanged(string? value) + { + Category = string.IsNullOrWhiteSpace(value) ? null : value; + if (TorrentManagementMode) + { + ApplyAutomaticPaths(); + } + + return Task.CompletedTask; + } + + protected Task SelectedTagsChanged(IEnumerable tags) + { + SelectedTags = tags is null + ? new HashSet(StringComparer.Ordinal) + : new HashSet(tags, StringComparer.Ordinal); + + return Task.CompletedTask; + } + + protected Task StopConditionChanged(string value) + { + StopCondition = value; + + return Task.CompletedTask; + } + + protected Task ContentLayoutChanged(string value) + { + ContentLayout = value; + + return Task.CompletedTask; + } + + protected Task ShareLimitModeChanged(ShareLimitMode mode) + { + SelectedShareLimitMode = mode; + if (mode != ShareLimitMode.Custom) + { + RatioLimitEnabled = false; + SeedingTimeLimitEnabled = false; + InactiveSeedingTimeLimitEnabled = false; + SelectedShareLimitAction = ShareLimitAction.Default; + } + + return Task.CompletedTask; + } + + protected Task RatioLimitEnabledChanged(bool value) + { + RatioLimitEnabled = value; + + return Task.CompletedTask; + } + + protected Task RatioLimitChanged(float value) + { + RatioLimit = value; + + return Task.CompletedTask; + } + + protected Task SeedingTimeLimitEnabledChanged(bool value) + { + SeedingTimeLimitEnabled = value; + + return Task.CompletedTask; + } + + protected Task SeedingTimeLimitChanged(int value) + { + SeedingTimeLimit = value; + + return Task.CompletedTask; + } + + protected Task InactiveSeedingTimeLimitEnabledChanged(bool value) + { + InactiveSeedingTimeLimitEnabled = value; + + return Task.CompletedTask; + } + + protected Task InactiveSeedingTimeLimitChanged(int value) + { + InactiveSeedingTimeLimit = value; + + return Task.CompletedTask; + } + + protected Task ShareLimitActionChanged(ShareLimitAction value) + { + SelectedShareLimitAction = value; + + return Task.CompletedTask; } public TorrentOptions GetTorrentOptions() { - return new TorrentOptions( + var options = new TorrentOptions( TorrentManagementMode, - SavePath, + _manualSavePath, Cookie, RenameTorrent, - Category, + string.IsNullOrWhiteSpace(Category) ? null : Category, StartTorrent, AddToTopOfQueue, StopCondition, @@ -76,6 +324,152 @@ namespace Lantean.QBTMud.Components.Dialogs DownloadFirstAndLastPiecesFirst, DownloadLimit, UploadLimit); + + options.UseDownloadPath = TorrentManagementMode ? null : UseDownloadPath; + options.DownloadPath = (!TorrentManagementMode && UseDownloadPath) ? DownloadPath : null; + options.Tags = SelectedTags.Count > 0 ? SelectedTags.ToArray() : null; + + switch (SelectedShareLimitMode) + { + case ShareLimitMode.Global: + options.RatioLimit = Limits.GlobalLimit; + options.SeedingTimeLimit = Limits.GlobalLimit; + options.InactiveSeedingTimeLimit = Limits.GlobalLimit; + options.ShareLimitAction = ShareLimitAction.Default.ToString(); + break; + case ShareLimitMode.NoLimit: + options.RatioLimit = Limits.NoLimit; + options.SeedingTimeLimit = Limits.NoLimit; + options.InactiveSeedingTimeLimit = Limits.NoLimit; + options.ShareLimitAction = ShareLimitAction.Default.ToString(); + break; + case ShareLimitMode.Custom: + options.RatioLimit = RatioLimitEnabled ? RatioLimit : Limits.NoLimit; + options.SeedingTimeLimit = SeedingTimeLimitEnabled ? SeedingTimeLimit : Limits.NoLimit; + options.InactiveSeedingTimeLimit = InactiveSeedingTimeLimitEnabled ? InactiveSeedingTimeLimit : Limits.NoLimit; + options.ShareLimitAction = SelectedShareLimitAction.ToString(); + break; + } + + return options; } + + private void ApplyAutomaticPaths() + { + SavePath = ResolveAutomaticSavePath(); + var (enabled, path) = ResolveAutomaticDownloadPath(); + UseDownloadPath = enabled; + DownloadPath = enabled ? path ?? string.Empty : string.Empty; + } + + private void RestoreManualPaths() + { + SavePath = _manualSavePath; + UseDownloadPath = _manualUseDownloadPath; + DownloadPath = _manualUseDownloadPath ? _manualDownloadPath : string.Empty; + } + + private string ResolveAutomaticSavePath() + { + var category = GetSelectedCategory(); + if (category is null) + { + return _defaultSavePath; + } + + if (!string.IsNullOrWhiteSpace(category.SavePath)) + { + return category.SavePath!; + } + + if (!string.IsNullOrWhiteSpace(_defaultSavePath) && !string.IsNullOrWhiteSpace(category.Name)) + { + return Path.Combine(_defaultSavePath, category.Name); + } + + return _defaultSavePath; + } + + private (bool Enabled, string? Path) ResolveAutomaticDownloadPath() + { + var category = GetSelectedCategory(); + if (category is null) + { + if (!_defaultDownloadPathEnabled) + { + return (false, string.Empty); + } + + return (true, _defaultDownloadPath); + } + + if (category.DownloadPath is null) + { + if (!_defaultDownloadPathEnabled) + { + return (false, string.Empty); + } + + return (true, ComposeDefaultDownloadPath(category.Name)); + } + + if (!category.DownloadPath.Enabled) + { + return (false, string.Empty); + } + + if (!string.IsNullOrWhiteSpace(category.DownloadPath.Path)) + { + return (true, category.DownloadPath.Path); + } + + return (true, ComposeDefaultDownloadPath(category.Name)); + } + + private string ComposeDefaultDownloadPath(string categoryName) + { + if (string.IsNullOrWhiteSpace(_defaultDownloadPath)) + { + return string.Empty; + } + + if (string.IsNullOrWhiteSpace(categoryName)) + { + return _defaultDownloadPath; + } + + return Path.Combine(_defaultDownloadPath, categoryName); + } + + private CategoryOption? GetSelectedCategory() + { + if (string.IsNullOrWhiteSpace(Category)) + { + return null; + } + + return _categoryLookup.TryGetValue(Category, out var option) ? option : null; + } + + private static ShareLimitAction MapShareLimitAction(int preferenceValue) + { + return preferenceValue switch + { + 0 => ShareLimitAction.Stop, + 1 => ShareLimitAction.Remove, + 2 => ShareLimitAction.RemoveWithContent, + 3 => ShareLimitAction.EnableSuperSeeding, + _ => ShareLimitAction.Default + }; + } + + protected enum ShareLimitMode + { + Global, + NoLimit, + Custom + } + + protected sealed record CategoryOption(string Name, string? SavePath, DownloadPathOption? DownloadPath); } } diff --git a/Lantean.QBTMud/Helpers/DialogHelper.cs b/Lantean.QBTMud/Helpers/DialogHelper.cs index 6826b8b..4cd33d0 100644 --- a/Lantean.QBTMud/Helpers/DialogHelper.cs +++ b/Lantean.QBTMud/Helpers/DialogHelper.cs @@ -1,4 +1,3 @@ -using System.Linq; using Lantean.QBitTorrentClient; using ShareLimitAction = Lantean.QBitTorrentClient.Models.ShareLimitAction; using Lantean.QBTMud.Components.Dialogs; @@ -77,12 +76,18 @@ namespace Lantean.QBTMud.Helpers addTorrentParams.ContentLayout = Enum.Parse(options.ContentLayout); } addTorrentParams.DownloadLimit = options.DownloadLimit; - addTorrentParams.DownloadPath = options.DownloadPath; + if (!string.IsNullOrWhiteSpace(options.DownloadPath)) + { + addTorrentParams.DownloadPath = options.DownloadPath; + } addTorrentParams.FirstLastPiecePriority = options.DownloadFirstAndLastPiecesFirst; addTorrentParams.InactiveSeedingTimeLimit = options.InactiveSeedingTimeLimit; addTorrentParams.RatioLimit = options.RatioLimit; addTorrentParams.RenameTorrent = options.RenameTorrent; - addTorrentParams.SavePath = options.SavePath; + if (!options.TorrentManagementMode) + { + addTorrentParams.SavePath = options.SavePath; + } addTorrentParams.SeedingTimeLimit = options.SeedingTimeLimit; addTorrentParams.SequentialDownload = options.DownloadInSequentialOrder; if (!string.IsNullOrEmpty(options.ShareLimitAction)) @@ -97,7 +102,10 @@ namespace Lantean.QBTMud.Helpers addTorrentParams.Stopped = !options.StartTorrent; addTorrentParams.Tags = options.Tags; addTorrentParams.UploadLimit = options.UploadLimit; - addTorrentParams.UseDownloadPath = options.UseDownloadPath; + if (options.UseDownloadPath.HasValue) + { + addTorrentParams.UseDownloadPath = options.UseDownloadPath; + } return addTorrentParams; } @@ -259,7 +267,7 @@ namespace Lantean.QBTMud.Helpers ShareLimitAction = t.ShareLimitAction, }).ToList(); - var referenceValue = shareRatioValues.First(); + var referenceValue = shareRatioValues[0]; var torrentsHaveSameShareRatio = shareRatioValues.Distinct().Count() == 1; var parameters = new DialogParameters diff --git a/Lantean.QBitTorrentClient/Converters/DownloadPathOptionJsonConverter.cs b/Lantean.QBitTorrentClient/Converters/DownloadPathOptionJsonConverter.cs new file mode 100644 index 0000000..5587719 --- /dev/null +++ b/Lantean.QBitTorrentClient/Converters/DownloadPathOptionJsonConverter.cs @@ -0,0 +1,44 @@ +using Lantean.QBitTorrentClient.Models; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Converters +{ + public sealed class DownloadPathOptionJsonConverter : JsonConverter + { + public override DownloadPathOption? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Null => null, + JsonTokenType.False => new DownloadPathOption(false, null), + JsonTokenType.True => new DownloadPathOption(true, null), + JsonTokenType.String => new DownloadPathOption(true, reader.GetString()), + _ => throw new JsonException($"Unexpected token {reader.TokenType} when parsing download_path.") + }; + } + + public override void Write(Utf8JsonWriter writer, DownloadPathOption? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + if (!value.Enabled) + { + writer.WriteBooleanValue(false); + return; + } + + if (string.IsNullOrWhiteSpace(value.Path)) + { + writer.WriteBooleanValue(true); + return; + } + + writer.WriteStringValue(value.Path); + } + } +} diff --git a/Lantean.QBitTorrentClient/Models/Category.cs b/Lantean.QBitTorrentClient/Models/Category.cs index 943cc1c..69d1e31 100644 --- a/Lantean.QBitTorrentClient/Models/Category.cs +++ b/Lantean.QBitTorrentClient/Models/Category.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using Lantean.QBitTorrentClient.Converters; +using System.Text.Json.Serialization; namespace Lantean.QBitTorrentClient.Models { @@ -7,10 +8,12 @@ namespace Lantean.QBitTorrentClient.Models [JsonConstructor] public Category( string name, - string? savePath) + string? savePath, + DownloadPathOption? downloadPath) { Name = name; SavePath = savePath; + DownloadPath = downloadPath; } [JsonPropertyName("name")] @@ -18,5 +21,9 @@ namespace Lantean.QBitTorrentClient.Models [JsonPropertyName("savePath")] public string? SavePath { get; } + + [JsonPropertyName("download_path")] + [JsonConverter(typeof(DownloadPathOptionJsonConverter))] + public DownloadPathOption? DownloadPath { get; } } -} \ No newline at end of file +} diff --git a/Lantean.QBitTorrentClient/Models/DownloadPathOption.cs b/Lantean.QBitTorrentClient/Models/DownloadPathOption.cs new file mode 100644 index 0000000..cad0f8e --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/DownloadPathOption.cs @@ -0,0 +1,15 @@ +namespace Lantean.QBitTorrentClient.Models +{ + public record DownloadPathOption + { + public DownloadPathOption(bool enabled, string? path) + { + Enabled = enabled; + Path = path; + } + + public bool Enabled { get; } + + public string? Path { get; } + } +} \ No newline at end of file