11 Commits

Author SHA1 Message Date
ahjephson
b8412bb232 Add new v5 properties to AddTorrent 2025-10-22 14:05:13 +01:00
ahjephson
e64a13c7c9 Add ShareRatio to include ShareLimitAction 2025-10-22 12:42:30 +01:00
ahjephson
e4ea79a8ed Update planning doc 2025-10-22 11:41:06 +01:00
ahjephson
0976b72411 Add new v5 torrent properties 2025-10-22 11:39:04 +01:00
ahjephson
965fbcd010 Add copy comments/content path 2025-10-22 10:55:34 +01:00
ahjephson
3d0dbde9f4 Add v5 only preferences 2025-10-22 10:40:14 +01:00
ahjephson
5b4fbde7b2 Removed paused state. 2025-10-22 08:29:43 +01:00
ahjephson
0db0ad4374 Remove other v4 logic 2025-10-21 14:20:25 +01:00
ahjephson
c390d83e4d Update to use v5 api only. 2025-10-21 13:38:50 +01:00
ahjephson
8dd29c238d Update client to use net v5 apis 2025-10-21 13:12:38 +01:00
ahjephson
fca17edfd1 Merge tag '1.2.0' into develop
1.2.0
2025-10-20 20:56:10 +01:00
53 changed files with 2212 additions and 617 deletions

View File

@@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
readme.md = readme.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lantean.QBitTorrentClient.Test", "Lantean.QBitTorrentClient.Test\Lantean.QBitTorrentClient.Test.csproj", "{796E865C-7AA6-4BD9-B12F-394801199A75}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -33,6 +35,10 @@ Global
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.Build.0 = Release|Any CPU
{796E865C-7AA6-4BD9-B12F-394801199A75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{796E865C-7AA6-4BD9-B12F-394801199A75}.Debug|Any CPU.Build.0 = Debug|Any CPU
{796E865C-7AA6-4BD9-B12F-394801199A75}.Release|Any CPU.ActiveCfg = Release|Any CPU
{796E865C-7AA6-4BD9-B12F-394801199A75}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -1,33 +1,50 @@
<MudGrid>
@using Lantean.QBitTorrentClient.Models
<MudGrid>
<MudItem xs="12">
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPlacement="Placement.End" />
</MudItem>
</MudGrid>
<MudCollapse Expanded="Expanded">
<MudGrid>
<MudGrid Class="mt-2">
<MudItem xs="12">
<MudSelect Label="Torrent Management Mode" @bind-Value="TorrentManagementMode" Variant="Variant.Outlined">
<MudSelectItem Value="false">Manual</MudSelectItem>
<MudSelectItem Value="true">Automatic</MudSelectItem>
<MudSelect T="bool" Label="Torrent management mode" Value="@TorrentManagementMode" ValueChanged="@SetTorrentManagementMode" Variant="Variant.Outlined">
<MudSelectItem Value="@false">Manual</MudSelectItem>
<MudSelectItem Value="@true">Automatic</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6">
<MudTextField T="string" Label="Save files to location" Value="@SavePath" ValueChanged="@SavePathChanged" Variant="Variant.Outlined" Disabled="@TorrentManagementMode" />
</MudItem>
<MudItem xs="12" sm="6">
<FieldSwitch Label="Use incomplete save path" Value="@UseDownloadPath" ValueChanged="@SetUseDownloadPath" Disabled="@TorrentManagementMode" />
</MudItem>
<MudItem xs="12">
<MudTextField Label="Save files to location" @bind-Value="SavePath" Variant="Variant.Outlined"></MudTextField>
<MudTextField T="string" Label="Incomplete save path" Value="@DownloadPath" ValueChanged="@DownloadPathChanged" Variant="Variant.Outlined" Disabled="@DownloadPathDisabled" />
</MudItem>
@if (ShowCookieOption)
{
<MudItem xs="12">
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined"></MudTextField>
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined" />
</MudItem>
}
<MudItem xs="12">
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined"></MudTextField>
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12">
<MudSelect Label="Category" @bind-Value="Category" Variant="Variant.Outlined">
@foreach (var category in Categories)
<MudSelect T="string" Label="Category" Value="@Category" ValueChanged="@CategoryChanged" Variant="Variant.Outlined" Clearable="true">
<MudSelectItem Value="@string.Empty">None</MudSelectItem>
@foreach (var category in CategoryOptions)
{
<MudSelectItem Value="category">@category</MudSelectItem>
<MudSelectItem Value="@category.Name">@category.Name</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudSelect T="string" Label="Tags" Variant="Variant.Outlined" MultiSelection="true" SelectedValues="@SelectedTags" SelectedValuesChanged="@SelectedTagsChanged" Disabled="@(AvailableTags.Count == 0)">
@foreach (var tag in AvailableTags)
{
<MudSelectItem Value="@tag">@tag</MudSelectItem>
}
</MudSelect>
</MudItem>
@@ -38,7 +55,7 @@
<FieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
</MudItem>
<MudItem xs="12">
<MudSelect Label="Stop condition" @bind-Value="StopCondition" Variant="Variant.Outlined">
<MudSelect T="string" Label="Stop condition" Value="@StopCondition" ValueChanged="@StopConditionChanged" Variant="Variant.Outlined">
<MudSelectItem Value="@("None")">None</MudSelectItem>
<MudSelectItem Value="@("MetadataReceived")">Metadata received</MudSelectItem>
<MudSelectItem Value="@("FilesChecked")">Files checked</MudSelectItem>
@@ -47,22 +64,58 @@
<MudItem xs="12">
<FieldSwitch Label="Skip hash check" @bind-Value="SkipHashCheck" />
</MudItem>
<MudSelect Label="Content layout" @bind-Value="ContentLayout" Variant="Variant.Outlined">
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder'</MudSelectItem>
</MudSelect>
<MudItem xs="12">
<FieldSwitch Label="Download in sequentual order" @bind-Value="DownloadInSequentialOrder" />
<MudSelect T="string" Label="Content layout" Value="@ContentLayout" ValueChanged="@ContentLayoutChanged" Variant="Variant.Outlined">
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<FieldSwitch Label="Download in sequential order" @bind-Value="DownloadInSequentialOrder" />
</MudItem>
<MudItem xs="12">
<FieldSwitch Label="Download first and last pieces first" @bind-Value="DownloadFirstAndLastPiecesFirst" />
</MudItem>
<MudItem xs="12">
<MudItem xs="12" sm="6">
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Outlined" Min="0" />
</MudItem>
<MudItem xs="12">
<MudItem xs="12" sm="6">
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Outlined" Min="0" />
</MudItem>
<MudItem xs="12">
<MudSelect T="ShareLimitMode" Label="Share limit preset" Value="@SelectedShareLimitMode" ValueChanged="@ShareLimitModeChanged" Variant="Variant.Outlined">
<MudSelectItem Value="@ShareLimitMode.Global">Use global share limit</MudSelectItem>
<MudSelectItem Value="@ShareLimitMode.NoLimit">Set no share limit</MudSelectItem>
<MudSelectItem Value="@ShareLimitMode.Custom">Set custom share limit</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="4">
<FieldSwitch Label="Ratio" Value="@RatioLimitEnabled" ValueChanged="@RatioLimitEnabledChanged" Disabled="@(!IsCustomShareLimit)" />
</MudItem>
<MudItem xs="12" sm="8">
<MudNumericField T="float" Label="Ratio limit" Value="@RatioLimit" ValueChanged="@RatioLimitChanged" Disabled="@(!RatioLimitEnabled || !IsCustomShareLimit)" Min="0" Step="0.1f" Format="F2" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" sm="4">
<FieldSwitch Label="Total minutes" Value="@SeedingTimeLimitEnabled" ValueChanged="@SeedingTimeLimitEnabledChanged" Disabled="@(!IsCustomShareLimit)" />
</MudItem>
<MudItem xs="12" sm="8">
<MudNumericField T="int" Label="Total minutes" Value="@SeedingTimeLimit" ValueChanged="@SeedingTimeLimitChanged" Disabled="@(!SeedingTimeLimitEnabled || !IsCustomShareLimit)" Min="1" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" sm="4">
<FieldSwitch Label="Inactive minutes" Value="@InactiveSeedingTimeLimitEnabled" ValueChanged="@InactiveSeedingTimeLimitEnabledChanged" Disabled="@(!IsCustomShareLimit)" />
</MudItem>
<MudItem xs="12" sm="8">
<MudNumericField T="int" Label="Inactive minutes" Value="@InactiveSeedingTimeLimit" ValueChanged="@InactiveSeedingTimeLimitChanged" Disabled="@(!InactiveSeedingTimeLimitEnabled || !IsCustomShareLimit)" Min="1" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12">
<MudSelect T="ShareLimitAction" Label="Action when limit is reached" Value="@SelectedShareLimitAction" ValueChanged="@ShareLimitActionChanged" Disabled="@(!IsCustomShareLimit)" Variant="Variant.Outlined">
<MudSelectItem Value="@ShareLimitAction.Default">Default</MudSelectItem>
<MudSelectItem Value="@ShareLimitAction.Stop">Stop torrent</MudSelectItem>
<MudSelectItem Value="@ShareLimitAction.Remove">Remove torrent</MudSelectItem>
<MudSelectItem Value="@ShareLimitAction.RemoveWithContent">Remove torrent and data</MudSelectItem>
<MudSelectItem Value="@ShareLimitAction.EnableSuperSeeding">Enable super seeding</MudSelectItem>
</MudSelect>
</MudItem>
</MudGrid>
</MudCollapse>
</MudCollapse>

View File

@@ -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<CategoryOption> _categoryOptions = new();
private readonly Dictionary<string, CategoryOption> _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<string> Categories { get; set; } = [];
protected IReadOnlyList<CategoryOption> CategoryOptions => _categoryOptions;
protected string? Category { get; set; }
protected string? Category { get; set; } = string.Empty;
protected List<string> AvailableTags { get; private set; } = [];
protected HashSet<string> 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;
StartTorrent = !preferences.StartPausedEnabled;
_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<string> tags)
{
SelectedTags = tags is null
? new HashSet<string>(StringComparer.Ordinal)
: new HashSet<string>(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);
}
}
}

View File

@@ -53,7 +53,7 @@
<MudNumericField T="int" Label="Ignore Subsequent Matches for (0 to Disable)" Value="IgnoreDays" ValueChanged="IgnoreDaysChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12">
<MudSelect T="string" Label="Add paused" Value="AddPaused" ValueChanged="AddPausedChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined">
<MudSelect T="string" Label="Add stopped" Value="AddStopped" ValueChanged="AddStoppedChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined">
<MudSelectItem Value="@("default")">Use global settings</MudSelectItem>
<MudSelectItem Value="@("always")">Always</MudSelectItem>
<MudSelectItem Value="@("never")">Never</MudSelectItem>
@@ -103,4 +103,4 @@
<MudButton OnClick="Cancel">Close</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@@ -114,11 +114,11 @@ namespace Lantean.QBTMud.Components.Dialogs
SelectedRule.IgnoreDays = value;
}
protected string? AddPaused { get; set; }
protected string? AddStopped { get; set; }
protected void AddPausedChanged(string value)
protected void AddStoppedChanged(string value)
{
AddPaused = value;
AddStopped = value;
switch (value)
{
case "default":
@@ -273,15 +273,15 @@ namespace Lantean.QBTMud.Components.Dialogs
switch (SelectedRule.TorrentParams.Stopped)
{
case null:
AddPaused = "default";
AddStopped = "default";
break;
case true:
AddPaused = "always";
AddStopped = "always";
break;
case false:
AddPaused = "never";
AddStopped = "never";
break;
}

View File

@@ -1,4 +1,5 @@
@inherits SubmittableDialog
@inherits SubmittableDialog
@using Lantean.QBitTorrentClient.Models
<MudDialog>
<DialogContent>
@@ -34,10 +35,19 @@
<MudItem xs="9">
<MudNumericField T="int" Value="InactiveMinutes" ValueChanged="InactiveMinutesChanged" Disabled="@(!(CustomEnabled && InactiveMinutesEnabled))" Min="1" Max="1024000" Variant="Variant.Outlined" Adornment="Adornment.End" AdornmentText="minutes" />
</MudItem>
<MudItem xs="12">
<MudSelect T="ShareLimitAction" Label="Action when limit is reached" Value="SelectedShareLimitAction" ValueChanged="ShareLimitActionChanged" Disabled="@(!CustomEnabled)" Variant="Variant.Outlined">
<MudSelectItem Value="ShareLimitAction.Default">Default</MudSelectItem>
<MudSelectItem Value="ShareLimitAction.Stop">Stop torrent</MudSelectItem>
<MudSelectItem Value="ShareLimitAction.Remove">Remove torrent</MudSelectItem>
<MudSelectItem Value="ShareLimitAction.RemoveWithContent">Remove torrent and data</MudSelectItem>
<MudSelectItem Value="ShareLimitAction.EnableSuperSeeding">Enable super seeding</MudSelectItem>
</MudSelect>
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@@ -1,4 +1,7 @@
using Lantean.QBitTorrentClient;
using System;
using System.Collections.Generic;
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMud.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -16,6 +19,9 @@ namespace Lantean.QBTMud.Components.Dialogs
[Parameter]
public ShareRatioMax? Value { get; set; }
[Parameter]
public ShareRatioMax? CurrentValue { get; set; }
[Parameter]
public bool Disabled { get; set; }
@@ -33,6 +39,8 @@ namespace Lantean.QBTMud.Components.Dialogs
protected int InactiveMinutes { get; set; }
protected ShareLimitAction SelectedShareLimitAction { get; set; } = ShareLimitAction.Default;
protected bool CustomEnabled => ShareRatioType == 0;
protected void RatioEnabledChanged(bool value)
@@ -65,40 +73,75 @@ namespace Lantean.QBTMud.Components.Dialogs
InactiveMinutes = value;
}
protected void ShareLimitActionChanged(ShareLimitAction value)
{
SelectedShareLimitAction = value;
}
protected override void OnParametersSet()
{
if (Value is null || Value.RatioLimit == Limits.GlobalLimit && Value.SeedingTimeLimit == Limits.GlobalLimit && Value.InactiveSeedingTimeLimit == Limits.GlobalLimit)
RatioEnabled = false;
TotalMinutesEnabled = false;
InactiveMinutesEnabled = false;
var baseline = Value ?? CurrentValue;
SelectedShareLimitAction = baseline?.ShareLimitAction ?? ShareLimitAction.Default;
if (baseline is null || baseline.RatioLimit == Limits.GlobalLimit && baseline.SeedingTimeLimit == Limits.GlobalLimit && baseline.InactiveSeedingTimeLimit == Limits.GlobalLimit)
{
ShareRatioType = Limits.GlobalLimit;
return;
}
else if (Value.MaxRatio == Limits.NoLimit && Value.MaxSeedingTime == Limits.NoLimit && Value.MaxInactiveSeedingTime == Limits.NoLimit)
if (baseline.MaxRatio == Limits.NoLimit && baseline.MaxSeedingTime == Limits.NoLimit && baseline.MaxInactiveSeedingTime == Limits.NoLimit)
{
ShareRatioType = Limits.NoLimit;
return;
}
ShareRatioType = 0;
if (baseline.RatioLimit >= 0)
{
RatioEnabled = true;
Ratio = baseline.RatioLimit;
}
else
{
ShareRatioType = 0;
if (Value.RatioLimit >= 0)
{
RatioEnabled = true;
Ratio = Value.RatioLimit;
}
if (Value.SeedingTimeLimit >= 0)
{
TotalMinutesEnabled = true;
TotalMinutes = (int)Value.SeedingTimeLimit;
}
if (Value.InactiveSeedingTimeLimit >= 0)
{
InactiveMinutesEnabled = true;
InactiveMinutes = (int)Value.InactiveSeedingTimeLimit;
}
Ratio = 0;
}
if (baseline.SeedingTimeLimit >= 0)
{
TotalMinutesEnabled = true;
TotalMinutes = (int)baseline.SeedingTimeLimit;
}
else
{
TotalMinutes = 0;
}
if (baseline.InactiveSeedingTimeLimit >= 0)
{
InactiveMinutesEnabled = true;
InactiveMinutes = (int)baseline.InactiveSeedingTimeLimit;
}
else
{
InactiveMinutes = 0;
}
}
protected void ShareRatioTypeChanged(int value)
{
ShareRatioType = value;
if (!CustomEnabled)
{
RatioEnabled = false;
TotalMinutesEnabled = false;
InactiveMinutesEnabled = false;
SelectedShareLimitAction = ShareLimitAction.Default;
}
}
protected void Cancel()
@@ -112,16 +155,19 @@ namespace Lantean.QBTMud.Components.Dialogs
if (ShareRatioType == Limits.GlobalLimit)
{
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.GlobalLimit;
result.ShareLimitAction = ShareLimitAction.Default;
}
else if (ShareRatioType == Limits.NoLimit)
{
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.NoLimit;
result.ShareLimitAction = ShareLimitAction.Default;
}
else
{
result.RatioLimit = RatioEnabled ? Ratio : Limits.NoLimit;
result.SeedingTimeLimit = TotalMinutesEnabled ? TotalMinutes : Limits.NoLimit;
result.InactiveSeedingTimeLimit = InactiveMinutesEnabled ? InactiveMinutes : Limits.NoLimit;
result.ShareLimitAction = SelectedShareLimitAction;
}
MudDialog.Close(DialogResult.Ok(result));
}
@@ -133,4 +179,4 @@ namespace Lantean.QBTMud.Components.Dialogs
return Task.CompletedTask;
}
}
}
}

View File

@@ -65,9 +65,9 @@
{
return __builder =>
{
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => ResumeTorrents(type))">Resume torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Pause" IconColor="Color.Warning" OnClick="@(e => PauseTorrents(type))">Pause torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => StartTorrents(type))">Start torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Stop" IconColor="Color.Warning" OnClick="@(e => StopTorrents(type))">Stop torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(e => RemoveTorrents(type))">Remove torrents</MudMenuItem>
};
}
}
}

View File

@@ -5,6 +5,7 @@ using Lantean.QBTMud.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using System.Linq;
namespace Lantean.QBTMud.Components
{
@@ -352,25 +353,25 @@ namespace Lantean.QBTMud.Components
}
}
protected async Task ResumeTorrents(string type)
protected async Task StartTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await ApiClient.ResumeTorrents(torrents);
await ApiClient.StartTorrents(hashes: torrents.ToArray());
}
protected async Task PauseTorrents(string type)
protected async Task StopTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await ApiClient.PauseTorrents(torrents);
await ApiClient.StopTorrents(hashes: torrents.ToArray());
}
protected async Task RemoveTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await DialogService.InvokeDeleteTorrentDialog(ApiClient, [.. torrents]);
await DialogService.InvokeDeleteTorrentDialog(ApiClient, Preferences?.ConfirmTorrentDeletion == true, [.. torrents]);
}
private Dictionary<string, int> GetTags()
@@ -477,4 +478,4 @@ namespace Lantean.QBTMud.Components
}
}
}
}
}

View File

@@ -68,6 +68,21 @@
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.subtitle2">Confirmation</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<FieldSwitch Label="Confirm torrent recheck" Value="ConfirmTorrentRecheck" ValueChanged="ConfirmTorrentRecheckChanged" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
<MudCardHeader>
<CardHeaderContent>
@@ -240,4 +255,4 @@
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</MudCard>

View File

@@ -16,6 +16,8 @@ namespace Lantean.QBTMud.Components.Options
protected int SaveResumeDataInterval { get; private set; }
protected int TorrentFileSizeLimit { get; private set; }
protected bool RecheckCompletedTorrents { get; private set; }
protected bool ConfirmTorrentRecheck { get; private set; }
protected string? AppInstanceName { get; private set; }
protected int RefreshInterval { get; private set; }
protected bool ResolvePeerCountries { get; private set; }
@@ -97,6 +99,7 @@ namespace Lantean.QBTMud.Components.Options
SaveResumeDataInterval = Preferences.SaveResumeDataInterval;
TorrentFileSizeLimit = Preferences.TorrentFileSizeLimit / 1024 / 1024;
RecheckCompletedTorrents = Preferences.RecheckCompletedTorrents;
ConfirmTorrentRecheck = Preferences.ConfirmTorrentRecheck;
AppInstanceName = Preferences.AppInstanceName;
RefreshInterval = Preferences.RefreshInterval;
ResolvePeerCountries = Preferences.ResolvePeerCountries;
@@ -209,6 +212,13 @@ namespace Lantean.QBTMud.Components.Options
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ConfirmTorrentRecheckChanged(bool value)
{
ConfirmTorrentRecheck = value;
UpdatePreferences.ConfirmTorrentRecheck = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AppInstanceNameChanged(string value)
{
AppInstanceName = value;
@@ -608,4 +618,4 @@ namespace Lantean.QBTMud.Components.Options
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
}
}
}

View File

@@ -17,6 +17,24 @@
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.subtitle2">Transfer List</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<FieldSwitch Label="Confirm when deleting torrents" Value="ConfirmTorrentDeletion" ValueChanged="ConfirmTorrentDeletionChanged" />
</MudItem>
<MudItem xs="12">
<FieldSwitch Label="Show external IP in status bar" Value="StatusBarExternalIp" ValueChanged="StatusBarExternalIpChanged" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
@@ -71,4 +89,4 @@
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</MudCard>

View File

@@ -4,6 +4,10 @@ namespace Lantean.QBTMud.Components.Options
{
public partial class BehaviourOptions : Options
{
protected bool ConfirmTorrentDeletion { get; set; }
protected bool StatusBarExternalIp { get; set; }
protected bool FileLogEnabled { get; set; }
protected string? FileLogPath { get; set; }
@@ -27,6 +31,8 @@ namespace Lantean.QBTMud.Components.Options
return false;
}
ConfirmTorrentDeletion = Preferences.ConfirmTorrentDeletion;
StatusBarExternalIp = Preferences.StatusBarExternalIp;
FileLogEnabled = Preferences.FileLogEnabled;
FileLogPath = Preferences.FileLogPath;
FileLogBackupEnabled = Preferences.FileLogBackupEnabled;
@@ -39,6 +45,20 @@ namespace Lantean.QBTMud.Components.Options
return true;
}
protected async Task ConfirmTorrentDeletionChanged(bool value)
{
ConfirmTorrentDeletion = value;
UpdatePreferences.ConfirmTorrentDeletion = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task StatusBarExternalIpChanged(bool value)
{
StatusBarExternalIp = value;
UpdatePreferences.StatusBarExternalIp = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task FileLogEnabledChanged(bool value)
{
FileLogEnabled = value;
@@ -96,4 +116,4 @@ namespace Lantean.QBTMud.Components.Options
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
}
}
}

View File

@@ -19,7 +19,7 @@
<FieldSwitch Label="Add to top of queue" Value="AddToTopOfQueue" ValueChanged="AddToTopOfQueueChanged" />
</MudItem>
<MudItem xs="12">
<FieldSwitch Label="Do not start the download automatically" Value="StartPausedEnabled" ValueChanged="StartPausedEnabledChanged" />
<FieldSwitch Label="Do not start the download automatically" Value="AddStoppedEnabled" ValueChanged="AddStoppedEnabledChanged" />
</MudItem>
<MudItem xs="12">
<MudSelect T="string" Label="Torrent stop condition" Value="TorrentStopCondition" ValueChanged="TorrentStopConditionChanged" Variant="Variant.Outlined">
@@ -306,4 +306,4 @@
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</MudCard>

View File

@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Options
{
protected string? TorrentContentLayout { get; set; }
protected bool AddToTopOfQueue { get; set; }
protected bool StartPausedEnabled { get; set; }
protected bool AddStoppedEnabled { get; set; }
protected string? TorrentStopCondition { get; set; }
protected bool AutoDeleteMode { get; set; }
protected bool PreallocateAll { get; set; }
@@ -51,7 +51,7 @@ namespace Lantean.QBTMud.Components.Options
// when adding a torrent
TorrentContentLayout = Preferences.TorrentContentLayout;
AddToTopOfQueue = Preferences.AddToTopOfQueue;
StartPausedEnabled = Preferences.StartPausedEnabled;
AddStoppedEnabled = Preferences.AddStoppedEnabled;
TorrentStopCondition = Preferences.TorrentStopCondition;
AutoDeleteMode = Preferences.AutoDeleteMode == 1;
PreallocateAll = Preferences.PreallocateAll;
@@ -116,10 +116,10 @@ namespace Lantean.QBTMud.Components.Options
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task StartPausedEnabledChanged(bool value)
protected async Task AddStoppedEnabledChanged(bool value)
{
StartPausedEnabled = value;
UpdatePreferences.StartPausedEnabled = value;
AddStoppedEnabled = value;
UpdatePreferences.AddStoppedEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
@@ -410,4 +410,4 @@ namespace Lantean.QBTMud.Components.Options
return null;
}
}
}
}

View File

@@ -37,9 +37,6 @@ namespace Lantean.QBTMud.Components
[Inject]
protected IKeyboardService KeyboardService { get; set; } = default!;
[CascadingParameter(Name = "Version")]
public string? Version { get; set; }
[Parameter]
[EditorRequired]
public IEnumerable<string> Hashes { get; set; } = default!;
@@ -71,14 +68,12 @@ namespace Lantean.QBTMud.Components
protected bool OverlayVisible { get; set; }
protected int MajorVersion => VersionHelper.GetMajorVersion(Version);
protected override void OnInitialized()
{
_actions =
[
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
new("pause", "Pause", MajorVersion < 5 ? Icons.Material.Filled.Pause : Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Pause)),
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Start)),
new("stop", "Stop", Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Stop)),
new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
@@ -109,6 +104,8 @@ namespace Lantean.QBTMud.Components
new("copyHashv2", "Info hash v2", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV2))),
new("copyMagnet", "Magnet link", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.MagnetUri))),
new("copyId", "Torrent ID", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Hash))),
new("copyComment", "Comment", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Comment))),
new("copyContentPath", "Content path", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.ContentPath))),
]),
new("export", "Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)),
];
@@ -146,32 +143,16 @@ namespace Lantean.QBTMud.Components
OverlayVisible = value;
}
protected async Task Pause()
protected async Task Stop()
{
if (MajorVersion < 5)
{
await ApiClient.PauseTorrents(Hashes);
Snackbar.Add("Torrent paused.");
}
else
{
await ApiClient.StopTorrents(Hashes);
Snackbar.Add("Torrent stopped.");
}
await ApiClient.StopTorrents(hashes: Hashes.ToArray());
Snackbar.Add("Torrent stopped.");
}
protected async Task Resume()
protected async Task Start()
{
if (MajorVersion < 5)
{
await ApiClient.ResumeTorrents(Hashes);
Snackbar.Add("Torrent resumed.");
}
else
{
await ApiClient.StartTorrents(Hashes);
Snackbar.Add("Torrent started.");
}
await ApiClient.StartTorrents(hashes: Hashes.ToArray());
Snackbar.Add("Torrent started.");
}
protected async Task ForceStart()
@@ -182,7 +163,7 @@ namespace Lantean.QBTMud.Components
protected async Task Remove()
{
var deleted = await DialogService.InvokeDeleteTorrentDialog(ApiClient, Hashes.ToArray());
var deleted = await DialogService.InvokeDeleteTorrentDialog(ApiClient, Preferences?.ConfirmTorrentDeletion == true, Hashes.ToArray());
if (deleted)
{
@@ -278,7 +259,7 @@ namespace Lantean.QBTMud.Components
protected async Task ForceRecheck()
{
await ApiClient.RecheckTorrents(null, Hashes.ToArray());
await DialogService.ForceRecheckAsync(ApiClient, Hashes, Preferences?.ConfirmTorrentRecheck == true);
}
protected async Task ForceReannounce()
@@ -385,8 +366,8 @@ namespace Lantean.QBTMud.Components
var allAreFirstLastPiecePrio = true;
var thereAreFirstLastPiecePrio = false;
var allAreDownloaded = true;
var allArePaused = true;
var thereArePaused = false;
var allAreStopped = true;
var thereAreStopped = false;
var allAreForceStart = true;
var thereAreForceStart = false;
var allAreSuperSeeding = true;
@@ -424,27 +405,13 @@ namespace Lantean.QBTMud.Components
allAreSuperSeeding = false;
}
if (MajorVersion < 5)
if (torrent.State != "stoppedUP" && torrent.State != "stoppedDL")
{
if (torrent.State != "pausedUP" && torrent.State != "pausedDL")
{
allArePaused = false;
}
else
{
thereArePaused = true;
}
allAreStopped = false;
}
else
{
if (torrent.State != "stoppedUP" && torrent.State != "stoppedDL")
{
allArePaused = false;
}
else
{
thereArePaused = true;
}
thereAreStopped = true;
}
if (!torrent.ForceStart)
@@ -532,7 +499,7 @@ namespace Lantean.QBTMud.Components
actionStates["superSeeding"] = ActionState.Hidden;
}
if (allArePaused)
if (allAreStopped)
{
actionStates["pause"] = ActionState.Hidden;
}
@@ -540,13 +507,11 @@ namespace Lantean.QBTMud.Components
{
actionStates["forceStart"] = ActionState.Hidden;
}
else if (!thereArePaused && !thereAreForceStart)
else if (!thereAreStopped && !thereAreForceStart)
{
actionStates["start"] = ActionState.Hidden;
}
if (MajorVersion >= 5)
{
if (actionStates.TryGetValue("start", out ActionState? startActionState))
{
startActionState.TextOverride = "Start";
@@ -564,7 +529,6 @@ namespace Lantean.QBTMud.Components
{
actionStates["pause"] = new ActionState { TextOverride = "Stop" };
}
}
if (!allAreAutoTmm && thereAreAutoTmm)
{
@@ -706,4 +670,4 @@ namespace Lantean.QBTMud.Components
MenuItems,
}
}
}

View File

@@ -171,7 +171,7 @@ namespace Lantean.QBTMud.Components
return;
}
await ApiClient.AddTrackersToTorrent(Hash, trackers);
await ApiClient.AddTrackersToTorrent(trackers, hashes: new[] { Hash });
}
protected Task EditTrackerToolbar()
@@ -211,7 +211,7 @@ namespace Lantean.QBTMud.Components
return;
}
await ApiClient.RemoveTrackers(Hash, [tracker.Url]);
await ApiClient.RemoveTrackers([tracker.Url], hashes: new[] { Hash });
}
protected Task CopyTrackerUrlToolbar()
@@ -303,4 +303,4 @@ namespace Lantean.QBTMud.Components
GC.SuppressFinalize(this);
}
}
}
}

View File

@@ -1,4 +1,5 @@
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient;
using ShareLimitAction = Lantean.QBitTorrentClient.Models.ShareLimitAction;
using Lantean.QBTMud.Components.Dialogs;
using Lantean.QBTMud.Filter;
using Lantean.QBTMud.Models;
@@ -56,7 +57,7 @@ namespace Lantean.QBTMud.Helpers
var addTorrentParams = CreateAddTorrentParams(options);
addTorrentParams.Torrents = files;
await apiClient.AddTorrent(addTorrentParams);
_ = await apiClient.AddTorrent(addTorrentParams);
foreach (var stream in streams)
{
@@ -74,18 +75,19 @@ namespace Lantean.QBTMud.Helpers
{
addTorrentParams.ContentLayout = Enum.Parse<QBitTorrentClient.Models.TorrentContentLayout>(options.ContentLayout);
}
if (string.IsNullOrEmpty(options.Cookie))
{
addTorrentParams.Cookie = options.Cookie;
}
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.Paused = !options.StartTorrent;
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))
@@ -100,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;
}
@@ -123,10 +128,10 @@ namespace Lantean.QBTMud.Helpers
var addTorrentParams = CreateAddTorrentParams(options);
addTorrentParams.Urls = options.Urls;
await apiClient.AddTorrent(addTorrentParams);
_ = await apiClient.AddTorrent(addTorrentParams);
}
public static async Task<bool> InvokeDeleteTorrentDialog(this IDialogService dialogService, IApiClient apiClient, params string[] hashes)
public static async Task<bool> InvokeDeleteTorrentDialog(this IDialogService dialogService, IApiClient apiClient, bool confirmTorrentDeletion, params string[] hashes)
{
if (hashes.Length == 0)
{
@@ -138,6 +143,12 @@ namespace Lantean.QBTMud.Helpers
{ nameof(DeleteDialog.Count), hashes.Length }
};
if (!confirmTorrentDeletion)
{
await apiClient.DeleteTorrents(hashes: hashes, deleteFiles: false);
return true;
}
var reference = await dialogService.ShowAsync<DeleteDialog>($"Remove torrent{(hashes.Length == 1 ? "" : "s")}?", parameters, ConfirmDialogOptions);
var dialogResult = await reference.Result;
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
@@ -150,6 +161,28 @@ namespace Lantean.QBTMud.Helpers
return true;
}
public static async Task ForceRecheckAsync(this IDialogService dialogService, IApiClient apiClient, IEnumerable<string> hashes, bool confirmTorrentRecheck)
{
var hashArray = hashes?.ToArray() ?? [];
if (hashArray.Length == 0)
{
return;
}
if (confirmTorrentRecheck)
{
var content = $"Are you sure you want to recheck the selected torrent{(hashArray.Length == 1 ? "" : "s")}?";
var confirmed = await dialogService.ShowConfirmDialog("Force recheck", content);
if (!confirmed)
{
return;
}
}
await apiClient.RecheckTorrents(null, hashArray);
}
public static async Task InvokeDownloadRateDialog(this IDialogService dialogService, IApiClient apiClient, long rate, IEnumerable<string> hashes)
{
Func<long, string> valueDisplayFunc = v => v == Limits.NoLimit ? "∞" : v.ToString();
@@ -217,21 +250,30 @@ namespace Lantean.QBTMud.Helpers
public static async Task InvokeShareRatioDialog(this IDialogService dialogService, IApiClient apiClient, IEnumerable<Torrent> torrents)
{
var torrentShareRatios = torrents.Select(t => new ShareRatioMax
var torrentList = torrents.ToList();
if (torrentList.Count == 0)
{
return;
}
var shareRatioValues = torrentList.Select(t => new ShareRatioMax
{
InactiveSeedingTimeLimit = t.InactiveSeedingTimeLimit,
MaxInactiveSeedingTime = t.InactiveSeedingTimeLimit,
MaxInactiveSeedingTime = t.MaxInactiveSeedingTime,
MaxRatio = t.MaxRatio,
MaxSeedingTime = t.MaxSeedingTime,
RatioLimit = t.RatioLimit,
SeedingTimeLimit = t.SeedingTimeLimit,
});
ShareLimitAction = t.ShareLimitAction,
}).ToList();
var torrentsHaveSameShareRatio = torrentShareRatios.Distinct().Count() == 1;
var referenceValue = shareRatioValues[0];
var torrentsHaveSameShareRatio = shareRatioValues.Distinct().Count() == 1;
var parameters = new DialogParameters
{
{ nameof(ShareRatioDialog.Value), torrentsHaveSameShareRatio ? torrentShareRatios.FirstOrDefault() : null },
{ nameof(ShareRatioDialog.Value), torrentsHaveSameShareRatio ? referenceValue : null },
{ nameof(ShareRatioDialog.CurrentValue), referenceValue },
};
var result = await dialogService.ShowAsync<ShareRatioDialog>("Share ratio", parameters, FormDialogOptions);
@@ -243,7 +285,7 @@ namespace Lantean.QBTMud.Helpers
var shareRatio = (ShareRatio)dialogResult.Data;
await apiClient.SetTorrentShareLimit(shareRatio.RatioLimit, shareRatio.SeedingTimeLimit, shareRatio.InactiveSeedingTimeLimit, null, torrents.Select(t => t.Hash).ToArray());
await apiClient.SetTorrentShareLimit(shareRatio.RatioLimit, shareRatio.SeedingTimeLimit, shareRatio.InactiveSeedingTimeLimit, shareRatio.ShareLimitAction ?? ShareLimitAction.Default, hashes: torrentList.Select(t => t.Hash).ToArray());
}
public static async Task InvokeStringFieldDialog(this IDialogService dialogService, string title, string label, string? value, Func<string, Task> onSuccess)
@@ -436,4 +478,6 @@ namespace Lantean.QBTMud.Helpers
await dialogService.ShowAsync<SubMenuDialog>(parent.Text, parameters, FormDialogOptions);
}
}
}
}

View File

@@ -1,5 +1,6 @@
using ByteSizeLib;
using Lantean.QBTMud.Models;
using Lantean.QBitTorrentClient;
using MudBlazor;
using System.Diagnostics.CodeAnalysis;
using System.Text;
@@ -404,8 +405,6 @@ namespace Lantean.QBTMud.Helpers
Status.Downloading => (Icons.Material.Filled.Downloading, Color.Success),
Status.Seeding => (Icons.Material.Filled.Upload, Color.Info),
Status.Completed => (Icons.Material.Filled.Check, Color.Default),
Status.Resumed => (Icons.Material.Filled.PlayArrow, Color.Success),
Status.Paused => (Icons.Material.Filled.Pause, Color.Default),
Status.Stopped => (Icons.Material.Filled.Stop, Color.Default),
Status.Active => (Icons.Material.Filled.Sort, Color.Success),
Status.Inactive => (Icons.Material.Filled.Sort, Color.Error),
@@ -417,5 +416,25 @@ namespace Lantean.QBTMud.Helpers
_ => (Icons.Material.Filled.QuestionMark, Color.Inherit),
};
}
public static string Bool(bool value, string trueText = "Yes", string falseText = "No")
{
return value ? trueText : falseText;
}
public static string RatioLimit(float value)
{
if (value == Limits.GlobalLimit)
{
return "Global";
}
if (value <= Limits.NoLimit)
{
return "∞";
}
return value.ToString("0.00");
}
}
}
}

View File

@@ -200,15 +200,8 @@ namespace Lantean.QBTMud.Helpers
break;
case Status.Resumed:
if (!state.Contains("resumed"))
{
return false;
}
break;
case Status.Paused:
if (!state.Contains("paused") && !state.Contains("stopped"))
case Status.Stopped:
if (state != "stoppedDL" && state != "stoppedUP")
{
return false;
}
@@ -285,4 +278,4 @@ namespace Lantean.QBTMud.Helpers
};
}
}
}
}

View File

@@ -1,33 +0,0 @@
namespace Lantean.QBTMud.Helpers
{
internal static class VersionHelper
{
private static int? _version;
private const int _defaultVersion = 5;
public static int DefaultVersion => _defaultVersion;
public static int GetMajorVersion(string? version)
{
if (_version is not null)
{
return _version.Value;
}
if (string.IsNullOrEmpty(version))
{
return _defaultVersion;
}
if (!Version.TryParse(version?.Replace("v", ""), out var theVersion))
{
return _defaultVersion;
}
_version = theVersion.Major;
return _version.Value;
}
}
}

View File

@@ -33,6 +33,14 @@
}
<MudSpacer />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
@{
var externalIpLabel = Preferences?.StatusBarExternalIp == true ? BuildExternalIpLabel(MainData?.ServerState) : null;
}
@if (!string.IsNullOrEmpty(externalIpLabel))
{
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@externalIpLabel</MudText>
}
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
@@ -70,4 +78,4 @@
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>

View File

@@ -97,7 +97,7 @@ namespace Lantean.QBTMud.Layout
Preferences = await ApiClient.GetApplicationPreferences();
Version = await ApiClient.GetApplicationVersion();
var data = await ApiClient.GetMainData(_requestId);
MainData = DataManager.CreateMainData(data, Version);
MainData = DataManager.CreateMainData(data);
MarkTorrentsDirty();
_requestId = data.ResponseId;
@@ -145,7 +145,7 @@ namespace Lantean.QBTMud.Layout
if (MainData is null || data.FullUpdate)
{
MainData = DataManager.CreateMainData(data, Version);
MainData = DataManager.CreateMainData(data);
MarkTorrentsDirty();
shouldRender = true;
}
@@ -201,6 +201,32 @@ namespace Lantean.QBTMud.Layout
};
}
private static string? BuildExternalIpLabel(ServerState? serverState)
{
if (serverState is null)
{
return null;
}
var v4 = serverState.LastExternalAddressV4;
var v6 = serverState.LastExternalAddressV6;
var hasV4 = !string.IsNullOrWhiteSpace(v4);
var hasV6 = !string.IsNullOrWhiteSpace(v6);
if (!hasV4 && !hasV6)
{
return "External IP: N/A";
}
if (hasV4 && hasV6)
{
return $"External IPs: {v4}, {v6}";
}
var address = hasV4 ? v4 : v6;
return $"External IP: {address}";
}
private void OnCategoryChanged(string category)
{
if (Category == category)
@@ -291,4 +317,4 @@ namespace Lantean.QBTMud.Layout
GC.SuppressFinalize(this);
}
}
}
}

View File

@@ -11,8 +11,7 @@
Dictionary<string, HashSet<string>> tagState,
Dictionary<string, HashSet<string>> categoriesState,
Dictionary<string, HashSet<string>> statusState,
Dictionary<string, HashSet<string>> trackersState,
int majorVersion)
Dictionary<string, HashSet<string>> trackersState)
{
Torrents = torrents.ToDictionary();
Tags = tags.ToHashSet();
@@ -23,7 +22,6 @@
CategoriesState = categoriesState;
StatusState = statusState;
TrackersState = trackersState;
MajorVersion = majorVersion;
}
public Dictionary<string, Torrent> Torrents { get; }
@@ -38,6 +36,5 @@
public Dictionary<string, HashSet<string>> TrackersState { get; }
public string? SelectedTorrentHash { get; set; }
public bool LostConnection { get; set; }
public int MajorVersion { get; }
}
}
}

View File

@@ -27,7 +27,17 @@
long uploadRateLimit,
bool useAltSpeedLimits,
bool useSubcategories,
float writeCacheOverload) : base(connectionStatus, dHTNodes, downloadInfoData, downloadInfoSpeed, downloadRateLimit, uploadInfoData, uploadInfoSpeed, uploadRateLimit)
float writeCacheOverload,
string lastExternalAddressV4,
string lastExternalAddressV6) : base(
connectionStatus,
dHTNodes,
downloadInfoData,
downloadInfoSpeed,
downloadRateLimit,
uploadInfoData,
uploadInfoSpeed,
uploadRateLimit)
{
AllTimeDownloaded = allTimeDownloaded;
AllTimeUploaded = allTimeUploaded;
@@ -46,6 +56,8 @@
UseAltSpeedLimits = useAltSpeedLimits;
UseSubcategories = useSubcategories;
WriteCacheOverload = writeCacheOverload;
LastExternalAddressV4 = lastExternalAddressV4;
LastExternalAddressV6 = lastExternalAddressV6;
}
public ServerState()
@@ -85,5 +97,9 @@
public bool UseSubcategories { get; set; }
public float WriteCacheOverload { get; set; }
public string LastExternalAddressV4 { get; set; } = string.Empty;
public string LastExternalAddressV6 { get; set; } = string.Empty;
}
}
}

View File

@@ -1,10 +1,13 @@
namespace Lantean.QBTMud.Models
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBTMud.Models
{
public record ShareRatio
{
public float RatioLimit { get; set; }
public float SeedingTimeLimit { get; set; }
public float InactiveSeedingTimeLimit { get; set; }
public ShareLimitAction? ShareLimitAction { get; set; }
}
public record ShareRatioMax : ShareRatio
@@ -13,4 +16,4 @@
public float MaxSeedingTime { get; set; }
public float MaxInactiveSeedingTime { get; set; }
}
}
}

View File

@@ -6,8 +6,6 @@
Downloading,
Seeding,
Completed,
Resumed,
Paused,
Stopped,
Active,
Inactive,
@@ -17,4 +15,4 @@
Checking,
Errored,
}
}
}

View File

@@ -1,4 +1,9 @@
namespace Lantean.QBTMud.Models
using System;
using System.Collections.Generic;
using System.Linq;
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBTMud.Models
{
public class Torrent
{
@@ -52,7 +57,13 @@
long uploadSpeed,
long reannounce,
float inactiveSeedingTimeLimit,
float maxInactiveSeedingTime)
float maxInactiveSeedingTime,
float popularity,
string downloadPath,
string rootPath,
bool isPrivate,
ShareLimitAction shareLimitAction,
string comment)
{
Hash = hash;
AddedOn = addedOn;
@@ -104,21 +115,31 @@
Reannounce = reannounce;
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
MaxInactiveSeedingTime = maxInactiveSeedingTime;
Popularity = popularity;
DownloadPath = downloadPath;
RootPath = rootPath;
IsPrivate = isPrivate;
ShareLimitAction = shareLimitAction;
Comment = comment;
}
protected Torrent()
{
Hash = "";
Category = "";
ContentPath = "";
InfoHashV1 = "";
InfoHashV2 = "";
MagnetUri = "";
Name = "";
SavePath = "";
State = "";
Tags = [];
Tracker = "";
Hash = string.Empty;
Category = string.Empty;
ContentPath = string.Empty;
InfoHashV1 = string.Empty;
InfoHashV2 = string.Empty;
MagnetUri = string.Empty;
Name = string.Empty;
SavePath = string.Empty;
DownloadPath = string.Empty;
RootPath = string.Empty;
State = string.Empty;
Tags = new List<string>();
Tracker = string.Empty;
ShareLimitAction = ShareLimitAction.Default;
Comment = string.Empty;
}
public string Hash { get; }
@@ -183,8 +204,14 @@
public float RatioLimit { get; set; }
public float Popularity { get; set; }
public string SavePath { get; set; }
public string DownloadPath { get; set; }
public string RootPath { get; set; }
public long SeedingTime { get; set; }
public int SeedingTimeLimit { get; set; }
@@ -221,6 +248,12 @@
public float MaxInactiveSeedingTime { get; set; }
public bool IsPrivate { get; set; }
public ShareLimitAction ShareLimitAction { get; set; }
public string Comment { get; set; }
public override bool Equals(object? obj)
{
if (obj is null)
@@ -241,4 +274,4 @@
return Hash;
}
}
}
}

View File

@@ -295,6 +295,7 @@ namespace Lantean.QBTMud.Pages
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Up Speed", t => t.UploadSpeed, t => DisplayHelpers.Speed(t.UploadSpeed)),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("ETA", t => t.EstimatedTimeOfArrival, t => DisplayHelpers.Duration(t.EstimatedTimeOfArrival)),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Ratio", t => t.Ratio, t => t.Ratio.ToString("0.00")),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Popularity", t => t.Popularity, t => t.Popularity.ToString("0.00")),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Category", t => t.Category),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Tags", t => t.Tags, t => string.Join(", ", t.Tags)),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Added On", t => t.AddedOn, t => DisplayHelpers.DateTime(t.AddedOn)),
@@ -310,11 +311,15 @@ namespace Lantean.QBTMud.Pages
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Time Active", t => t.TimeActive, t => DisplayHelpers.Duration(t.TimeActive), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Save path", t => t.SavePath, enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Completed", t => t.Completed, t => DisplayHelpers.Size(t.Completed), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Ratio Limit", t => t.RatioLimit, t => t.Ratio.ToString("0.00"), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Ratio Limit", t => t.RatioLimit, t => DisplayHelpers.RatioLimit(t.RatioLimit), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Last Seen Complete", t => t.SeenComplete, t => DisplayHelpers.DateTime(t.SeenComplete), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Last Activity", t => t.LastActivity, t => DisplayHelpers.DateTime(t.LastActivity), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Availability", t => t.Availability, t => t.Availability.ToString("0.##"), enabled: false),
//ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Reannounce In", t => t.Reannounce, enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Incomplete Save Path", t => t.DownloadPath, t => DisplayHelpers.EmptyIfNull(t.DownloadPath), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Info Hash v1", t => t.InfoHashV1, t => DisplayHelpers.EmptyIfNull(t.InfoHashV1), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Info Hash v2", t => t.InfoHashV2, t => DisplayHelpers.EmptyIfNull(t.InfoHashV2), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Reannounce In", t => t.Reannounce, t => DisplayHelpers.Duration(t.Reannounce), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Private", t => t.IsPrivate, t => DisplayHelpers.Bool(t.IsPrivate), enabled: false),
];
public async ValueTask DisposeAsync()
@@ -338,4 +343,4 @@ namespace Lantean.QBTMud.Pages
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
using Lantean.QBTMud.Helpers;
using Lantean.QBTMud.Helpers;
using Lantean.QBTMud.Models;
using ShareLimitAction = Lantean.QBitTorrentClient.Models.ShareLimitAction;
namespace Lantean.QBTMud.Services
{
@@ -25,9 +26,8 @@ namespace Lantean.QBTMud.Services
return peerList;
}
public MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version)
public MainData CreateMainData(QBitTorrentClient.Models.MainData mainData)
{
var majorVersion = VersionHelper.GetMajorVersion(version);
var torrents = new Dictionary<string, Torrent>(mainData.Torrents?.Count ?? 0);
if (mainData.Torrents is not null)
{
@@ -95,7 +95,7 @@ namespace Lantean.QBTMud.Services
categoriesState.Add(category, torrents.Values.Where(t => FilterHelper.FilterCategory(t, category, serverState.UseSubcategories)).ToHashesHashSet());
}
var statuses = GetStatuses(majorVersion).ToArray();
var statuses = GetStatuses().ToArray();
var statusState = new Dictionary<string, HashSet<string>>(statuses.Length + 2);
foreach (var status in statuses)
{
@@ -110,7 +110,7 @@ namespace Lantean.QBTMud.Services
trackersState.Add(tracker, torrents.Values.Where(t => FilterHelper.FilterTracker(t, tracker)).ToHashesHashSet());
}
var torrentList = new MainData(torrents, tags, categories, trackers, serverState, tagState, categoriesState, statusState, trackersState, majorVersion);
var torrentList = new MainData(torrents, tags, categories, trackers, serverState, tagState, categoriesState, statusState, trackersState);
return torrentList;
}
@@ -146,7 +146,9 @@ namespace Lantean.QBTMud.Services
serverState.UploadRateLimit.GetValueOrDefault(),
serverState.UseAltSpeedLimits.GetValueOrDefault(),
serverState.UseSubcategories.GetValueOrDefault(),
serverState.WriteCacheOverload.GetValueOrDefault());
serverState.WriteCacheOverload.GetValueOrDefault(),
serverState.LastExternalAddressV4 ?? string.Empty,
serverState.LastExternalAddressV6 ?? string.Empty);
}
public bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged)
@@ -284,7 +286,7 @@ namespace Lantean.QBTMud.Services
{
var newTorrent = CreateTorrent(hash, torrent);
torrentList.Torrents.Add(hash, newTorrent);
AddTorrentToStates(torrentList, hash, torrentList.MajorVersion);
AddTorrentToStates(torrentList, hash);
dataChanged = true;
filterChanged = true;
}
@@ -316,7 +318,7 @@ namespace Lantean.QBTMud.Services
return dataChanged;
}
private static void AddTorrentToStates(MainData torrentList, string hash, int version)
private static void AddTorrentToStates(MainData torrentList, string hash)
{
if (!torrentList.Torrents.TryGetValue(hash, out var torrent))
{
@@ -329,7 +331,7 @@ namespace Lantean.QBTMud.Services
torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Add(hash);
UpdateCategoryState(torrentList, torrent, hash, previousCategory: null);
foreach (var status in GetStatuses(version))
foreach (var status in GetStatuses())
{
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusSet))
{
@@ -346,21 +348,14 @@ namespace Lantean.QBTMud.Services
UpdateTrackerState(torrentList, torrent, hash, previousTracker: null);
}
private static Status[] GetStatuses(int version)
private static Status[] GetStatuses()
{
if (_statusArray is not null)
{
return _statusArray;
}
if (version == 5)
{
_statusArray = Enum.GetValues<Status>().Where(s => s != Status.Paused).ToArray();
}
else
{
_statusArray = Enum.GetValues<Status>().Where(s => s != Status.Stopped).ToArray();
}
_statusArray = Enum.GetValues<Status>();
return _statusArray;
}
@@ -388,7 +383,7 @@ namespace Lantean.QBTMud.Services
torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Remove(hash);
UpdateCategoryStateForRemoval(torrentList, hash, snapshot.Category);
foreach (var status in GetStatuses(torrentList.MajorVersion))
foreach (var status in GetStatuses())
{
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusState))
{
@@ -559,6 +554,18 @@ namespace Lantean.QBTMud.Services
changed = true;
}
if (serverState.LastExternalAddressV4 is not null && existingServerState.LastExternalAddressV4 != serverState.LastExternalAddressV4)
{
existingServerState.LastExternalAddressV4 = serverState.LastExternalAddressV4;
changed = true;
}
if (serverState.LastExternalAddressV6 is not null && existingServerState.LastExternalAddressV6 != serverState.LastExternalAddressV6)
{
existingServerState.LastExternalAddressV6 = serverState.LastExternalAddressV6;
changed = true;
}
return changed;
}
@@ -694,7 +701,13 @@ namespace Lantean.QBTMud.Services
torrent.UploadSpeed.GetValueOrDefault(),
torrent.Reannounce ?? 0,
torrent.InactiveSeedingTimeLimit.GetValueOrDefault(),
torrent.MaxInactiveSeedingTime.GetValueOrDefault());
torrent.MaxInactiveSeedingTime.GetValueOrDefault(),
torrent.Popularity.GetValueOrDefault(),
torrent.DownloadPath ?? string.Empty,
torrent.RootPath ?? string.Empty,
torrent.IsPrivate.GetValueOrDefault(),
torrent.ShareLimitAction ?? ShareLimitAction.Default,
torrent.Comment ?? string.Empty);
}
private static string NormalizeTag(string? tag)
@@ -852,7 +865,7 @@ namespace Lantean.QBTMud.Services
private static void UpdateStatusState(MainData torrentList, string hash, string previousState, long previousUploadSpeed, string newState, long newUploadSpeed)
{
foreach (var status in GetStatuses(torrentList.MajorVersion))
foreach (var status in GetStatuses())
{
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusSet))
{
@@ -1320,6 +1333,41 @@ namespace Lantean.QBTMud.Services
dataChanged = true;
}
if (torrent.Popularity.HasValue && existingTorrent.Popularity != torrent.Popularity.Value)
{
existingTorrent.Popularity = torrent.Popularity.Value;
dataChanged = true;
}
if (torrent.DownloadPath is not null && !string.Equals(existingTorrent.DownloadPath, torrent.DownloadPath, StringComparison.Ordinal))
{
existingTorrent.DownloadPath = torrent.DownloadPath;
dataChanged = true;
}
if (torrent.RootPath is not null && !string.Equals(existingTorrent.RootPath, torrent.RootPath, StringComparison.Ordinal))
{
existingTorrent.RootPath = torrent.RootPath;
dataChanged = true;
}
if (torrent.IsPrivate.HasValue && existingTorrent.IsPrivate != torrent.IsPrivate.Value)
{
existingTorrent.IsPrivate = torrent.IsPrivate.Value;
dataChanged = true;
}
if (torrent.ShareLimitAction.HasValue && existingTorrent.ShareLimitAction != torrent.ShareLimitAction.Value)
{
existingTorrent.ShareLimitAction = torrent.ShareLimitAction.Value;
dataChanged = true;
}
if (torrent.Comment is not null && !string.Equals(existingTorrent.Comment, torrent.Comment, StringComparison.Ordinal))
{
existingTorrent.Comment = torrent.Comment;
dataChanged = true;
}
return new TorrentUpdateResult(dataChanged, filterChanged);
}
@@ -1456,7 +1504,7 @@ namespace Lantean.QBTMud.Services
changed = true;
}
if (System.Math.Abs(destination.Progress - source.Progress) > floatTolerance)
if (Math.Abs(destination.Progress - source.Progress) > floatTolerance)
{
destination.Progress = source.Progress;
changed = true;
@@ -1468,7 +1516,7 @@ namespace Lantean.QBTMud.Services
changed = true;
}
if (System.Math.Abs(destination.Availability - source.Availability) > floatTolerance)
if (Math.Abs(destination.Availability - source.Availability) > floatTolerance)
{
destination.Availability = source.Availability;
changed = true;
@@ -1738,7 +1786,7 @@ namespace Lantean.QBTMud.Services
SocketReceiveBufferSize = changed.SocketReceiveBufferSize,
SocketSendBufferSize = changed.SocketSendBufferSize,
SsrfMitigation = changed.SsrfMitigation,
StartPausedEnabled = changed.StartPausedEnabled,
AddStoppedEnabled = changed.AddStoppedEnabled,
StopTrackerTimeout = changed.StopTrackerTimeout,
TempPath = changed.TempPath,
TempPathEnabled = changed.TempPathEnabled,
@@ -1944,7 +1992,7 @@ namespace Lantean.QBTMud.Services
original.SocketReceiveBufferSize = changed.SocketReceiveBufferSize ?? original.SocketReceiveBufferSize;
original.SocketSendBufferSize = changed.SocketSendBufferSize ?? original.SocketSendBufferSize;
original.SsrfMitigation = changed.SsrfMitigation ?? original.SsrfMitigation;
original.StartPausedEnabled = changed.StartPausedEnabled ?? original.StartPausedEnabled;
original.AddStoppedEnabled = changed.AddStoppedEnabled ?? original.AddStoppedEnabled;
original.StopTrackerTimeout = changed.StopTrackerTimeout ?? original.StopTrackerTimeout;
original.TempPath = changed.TempPath ?? original.TempPath;
original.TempPathEnabled = changed.TempPathEnabled ?? original.TempPathEnabled;
@@ -2006,7 +2054,7 @@ namespace Lantean.QBTMud.Services
? int.MaxValue
: contents.Values.Min(c => c.Index);
var minFileIndex = files.Min(f => f.Index);
var nextFolderIndex = System.Math.Min(minExistingIndex, minFileIndex) - 1;
var nextFolderIndex = Math.Min(minExistingIndex, minFileIndex) - 1;
foreach (var file in files)
{
@@ -2134,4 +2182,4 @@ namespace Lantean.QBTMud.Services
return new RssList(feeds, articles);
}
}
}
}

View File

@@ -4,7 +4,7 @@ namespace Lantean.QBTMud.Services
{
public interface IDataManager
{
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version);
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData);
Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);
@@ -22,4 +22,4 @@ namespace Lantean.QBTMud.Services
RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems);
}
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
namespace Lantean.QBitTorrentClient.Test
{
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}
}

View File

@@ -1,4 +1,9 @@
using Lantean.QBitTorrentClient.Models;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
@@ -105,6 +110,8 @@ namespace Lantean.QBitTorrentClient
public async Task SetApplicationPreferences(UpdatePreferences preferences)
{
preferences.Validate();
var json = JsonSerializer.Serialize(preferences, _options);
var content = new FormUrlEncodedBuilder()
@@ -116,6 +123,49 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task<IReadOnlyList<ApplicationCookie>> GetApplicationCookies()
{
var response = await _httpClient.GetAsync("app/cookies");
await ThrowIfNotSuccessfulStatusCode(response);
return await GetJsonList<ApplicationCookie>(response.Content);
}
public async Task SetApplicationCookies(IEnumerable<ApplicationCookie> cookies)
{
var json = JsonSerializer.Serialize(cookies, _options);
var content = new FormUrlEncodedBuilder()
.Add("cookies", json)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("app/setCookies", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task<string> RotateApiKey()
{
var response = await _httpClient.PostAsync("app/rotateAPIKey", null);
await ThrowIfNotSuccessfulStatusCode(response);
var payload = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(payload))
{
return string.Empty;
}
var json = JsonSerializer.Deserialize<JsonElement>(payload, _options);
if (json.ValueKind == JsonValueKind.Object && json.TryGetProperty("apiKey", out var apiKeyElement))
{
return apiKeyElement.GetString() ?? string.Empty;
}
return string.Empty;
}
public async Task<string> GetDefaultSavePath()
{
var response = await _httpClient.GetAsync("app/defaultSavePath");
@@ -145,6 +195,43 @@ namespace Lantean.QBitTorrentClient
#endregion Application
#region Client data
public async Task<IReadOnlyDictionary<string, JsonElement>> LoadClientData(IEnumerable<string>? keys = null)
{
HttpResponseMessage response;
if (keys is null)
{
response = await _httpClient.GetAsync("clientdata/load");
}
else
{
var query = new QueryBuilder()
.Add("keys", JsonSerializer.Serialize(keys, _options));
response = await _httpClient.GetAsync("clientdata/load", query);
}
await ThrowIfNotSuccessfulStatusCode(response);
return await GetJsonDictionary<string, JsonElement>(response.Content);
}
public async Task StoreClientData(IReadOnlyDictionary<string, JsonElement> data)
{
var json = JsonSerializer.Serialize(data, _options);
var content = new FormUrlEncodedBuilder()
.Add("data", json)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("clientdata/store", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
#endregion Client data
#region Log
public async Task<IReadOnlyList<Log>> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null)
@@ -305,7 +392,7 @@ namespace Lantean.QBitTorrentClient
#region Torrent management
public async Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, params string[] hashes)
public async Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, bool? includeFiles = null, params string[] hashes)
{
var query = new QueryBuilder();
if (filter is not null)
@@ -344,6 +431,10 @@ namespace Lantean.QBitTorrentClient
{
query.Add("private", isPrivate.Value ? "true" : "false");
}
if (includeFiles is not null)
{
query.Add("includeFiles", includeFiles.Value ? "true" : "false");
}
var response = await _httpClient.GetAsync("torrents/info", query);
@@ -379,6 +470,43 @@ namespace Lantean.QBitTorrentClient
return await GetJsonList<WebSeed>(response.Content);
}
public async Task AddTorrentWebSeeds(string hash, IEnumerable<string> urls)
{
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.Add("urls", string.Join('|', urls))
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/addWebSeeds", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task EditTorrentWebSeed(string hash, string originalUrl, string newUrl)
{
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.Add("origUrl", originalUrl)
.Add("newUrl", newUrl)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/editWebSeed", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task RemoveTorrentWebSeeds(string hash, IEnumerable<string> urls)
{
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.Add("urls", string.Join('|', urls))
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/removeWebSeeds", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task<IReadOnlyList<FileData>> GetTorrentContents(string hash, params int[] indexes)
{
var query = new QueryBuilder();
@@ -411,18 +539,6 @@ namespace Lantean.QBitTorrentClient
return await GetJsonList<string>(response.Content);
}
public async Task PauseTorrents(bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/pause", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task StopTorrents(bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
@@ -433,18 +549,6 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task ResumeTorrents(bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/resume", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task StartTorrents(bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
@@ -479,10 +583,11 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task ReannounceTorrents(bool? all = null, params string[] hashes)
public async Task ReannounceTorrents(bool? all = null, IEnumerable<string>? trackers = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes)
.AddIfNotNullOrEmpty("urls", trackers is null ? null : string.Join('|', trackers))
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/reannounce", content);
@@ -490,13 +595,15 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task AddTorrent(AddTorrentParams addTorrentParams)
public async Task<AddTorrentResult> AddTorrent(AddTorrentParams addTorrentParams)
{
var content = new MultipartFormDataContent();
if (addTorrentParams.Urls is not null)
if (addTorrentParams.Urls?.Any() == true)
{
content.AddString("urls", string.Join('\n', addTorrentParams.Urls));
}
if (addTorrentParams.Torrents is not null)
{
foreach (var (name, stream) in addTorrentParams.Torrents)
@@ -504,6 +611,7 @@ namespace Lantean.QBitTorrentClient
content.Add(new StreamContent(stream), "torrents", name);
}
}
if (addTorrentParams.SkipChecking is not null)
{
content.AddString("skip_checking", addTorrentParams.SkipChecking.Value);
@@ -520,12 +628,10 @@ namespace Lantean.QBitTorrentClient
{
content.AddString("addToTopOfQueue", addTorrentParams.AddToTopOfQueue.Value);
}
// v4
if (addTorrentParams.Paused is not null)
if (addTorrentParams.Forced is not null)
{
content.AddString("paused", addTorrentParams.Paused.Value);
content.AddString("forced", addTorrentParams.Forced.Value);
}
// v5
if (addTorrentParams.Stopped is not null)
{
content.AddString("stopped", addTorrentParams.Stopped.Value);
@@ -590,21 +696,61 @@ namespace Lantean.QBitTorrentClient
{
content.AddString("contentLayout", addTorrentParams.ContentLayout.Value);
}
if (addTorrentParams.Cookie is not null)
if (addTorrentParams.Downloader is not null)
{
content.AddString("cookie", addTorrentParams.Cookie);
content.AddString("downloader", addTorrentParams.Downloader);
}
if (addTorrentParams.FilePriorities is not null)
{
var priorities = string.Join(',', addTorrentParams.FilePriorities.Select(priority => ((int)priority).ToString(CultureInfo.InvariantCulture)));
content.AddString("filePriorities", priorities);
}
if (!string.IsNullOrWhiteSpace(addTorrentParams.SslCertificate))
{
content.AddString("ssl_certificate", addTorrentParams.SslCertificate!);
}
if (!string.IsNullOrWhiteSpace(addTorrentParams.SslPrivateKey))
{
content.AddString("ssl_private_key", addTorrentParams.SslPrivateKey!);
}
if (!string.IsNullOrWhiteSpace(addTorrentParams.SslDhParams))
{
content.AddString("ssl_dh_params", addTorrentParams.SslDhParams!);
}
var response = await _httpClient.PostAsync("torrents/add", content);
if (response.StatusCode == HttpStatusCode.Conflict)
{
var conflictMessage = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(conflictMessage))
{
conflictMessage = "All torrents failed to add.";
}
throw new HttpRequestException(conflictMessage, null, response.StatusCode);
}
await ThrowIfNotSuccessfulStatusCode(response);
var payload = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(payload))
{
return new AddTorrentResult(0, 0, 0, Array.Empty<string>());
}
return JsonSerializer.Deserialize<AddTorrentResult>(payload, _options) ?? new AddTorrentResult(0, 0, 0, Array.Empty<string>());
}
public async Task AddTrackersToTorrent(string hash, IEnumerable<string> urls)
public async Task AddTrackersToTorrent(IEnumerable<string> urls, bool? all = null, params string[] hashes)
{
if (all is not true && (hashes is null || hashes.Length == 0))
{
throw new ArgumentException("Specify at least one torrent hash or set all=true.", nameof(hashes));
}
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.AddAllOrPipeSeparated("hash", all, hashes ?? Array.Empty<string>())
.Add("urls", string.Join('\n', urls))
.ToFormUrlEncodedContent();
@@ -613,23 +759,42 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task EditTracker(string hash, string originalUrl, string newUrl)
public async Task EditTracker(string hash, string url, string? newUrl = null, int? tier = null)
{
if ((newUrl is null) && (tier is null))
{
throw new ArgumentException("Must specify at least one of newUrl or tier.");
}
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.Add("originalUrl", originalUrl)
.Add("newUrl", newUrl)
.ToFormUrlEncodedContent();
.Add("url", url);
var response = await _httpClient.PostAsync("torrents/editTracker", content);
if (!string.IsNullOrEmpty(newUrl))
{
content.Add("newUrl", newUrl!);
}
if (tier is not null)
{
content.Add("tier", tier.Value);
}
var form = content.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/editTracker", form);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task RemoveTrackers(string hash, IEnumerable<string> urls)
public async Task RemoveTrackers(IEnumerable<string> urls, bool? all = null, params string[] hashes)
{
if (all is not true && (hashes is null || hashes.Length == 0))
{
throw new ArgumentException("Specify at least one torrent hash or set all=true.", nameof(hashes));
}
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.AddAllOrPipeSeparated("hash", all, hashes ?? Array.Empty<string>())
.AddPipeSeparated("urls", urls)
.ToFormUrlEncodedContent();
@@ -732,13 +897,14 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, bool? all = null, params string[] hashes)
public async Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, ShareLimitAction? shareLimitAction = null, bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes)
.Add("ratioLimit", ratioLimit)
.Add("seedingTimeLimit", seedingTimeLimit)
.Add("inactiveSeedingTimeLimit", inactiveSeedingTimeLimit)
.AddIfNotNullOrEmpty("shareLimitAction", shareLimitAction)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/setShareLimits", content);
@@ -795,6 +961,18 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task SetTorrentComment(IEnumerable<string> hashes, string comment)
{
var content = new FormUrlEncodedBuilder()
.Add("hashes", string.Join('|', hashes))
.Add("comment", comment)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/setComment", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task SetTorrentCategory(string category, bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
@@ -995,8 +1173,180 @@ namespace Lantean.QBitTorrentClient
return Task.FromResult($"{_httpClient.BaseAddress}torrents/export?hash={hash}");
}
public async Task<TorrentMetadata?> FetchMetadata(string source, string? downloader = null)
{
var builder = new FormUrlEncodedBuilder()
.Add("source", source);
if (!string.IsNullOrWhiteSpace(downloader))
{
builder.Add("downloader", downloader!);
}
var response = await _httpClient.PostAsync("torrents/fetchMetadata", builder.ToFormUrlEncodedContent());
await ThrowIfNotSuccessfulStatusCode(response);
var payload = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(payload))
{
return null;
}
return JsonSerializer.Deserialize<TorrentMetadata>(payload, _options);
}
public async Task<IReadOnlyList<TorrentMetadata>> ParseMetadata(IEnumerable<(string FileName, Stream Content)> torrents)
{
var content = new MultipartFormDataContent();
foreach (var (fileName, stream) in torrents)
{
content.Add(new StreamContent(stream), "torrents", fileName);
}
var response = await _httpClient.PostAsync("torrents/parseMetadata", content);
await ThrowIfNotSuccessfulStatusCode(response);
return await GetJsonList<TorrentMetadata>(response.Content);
}
public async Task<byte[]> SaveMetadata(string source)
{
var content = new FormUrlEncodedBuilder()
.Add("source", source)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/saveMetadata", content);
await ThrowIfNotSuccessfulStatusCode(response);
return await response.Content.ReadAsByteArrayAsync();
}
#endregion Torrent management
#region Torrent creator
public async Task<string> AddTorrentCreationTask(TorrentCreationTaskRequest request)
{
if (request is null)
throw new ArgumentNullException(nameof(request));
if (string.IsNullOrWhiteSpace(request.SourcePath))
throw new ArgumentException("SourcePath is required.", nameof(request));
var builder = new FormUrlEncodedBuilder()
.Add("sourcePath", request.SourcePath);
if (!string.IsNullOrWhiteSpace(request.TorrentFilePath))
{
builder.Add("torrentFilePath", request.TorrentFilePath!);
}
if (request.PieceSize.HasValue)
{
builder.Add("pieceSize", request.PieceSize.Value);
}
if (request.Private.HasValue)
{
builder.Add("private", request.Private.Value);
}
if (request.StartSeeding.HasValue)
{
builder.Add("startSeeding", request.StartSeeding.Value);
}
if (!string.IsNullOrWhiteSpace(request.Comment))
{
builder.Add("comment", request.Comment!);
}
if (!string.IsNullOrWhiteSpace(request.Source))
{
builder.Add("source", request.Source!);
}
if (request.Trackers is not null)
{
builder.Add("trackers", string.Join('|', request.Trackers));
}
if (request.UrlSeeds is not null)
{
builder.Add("urlSeeds", string.Join('|', request.UrlSeeds));
}
if (!string.IsNullOrWhiteSpace(request.Format))
{
builder.Add("format", request.Format!);
}
if (request.OptimizeAlignment.HasValue)
{
builder.Add("optimizeAlignment", request.OptimizeAlignment.Value);
}
if (request.PaddedFileSizeLimit.HasValue)
{
builder.Add("paddedFileSizeLimit", request.PaddedFileSizeLimit.Value);
}
var response = await _httpClient.PostAsync("torrentcreator/addTask", builder.ToFormUrlEncodedContent());
await ThrowIfNotSuccessfulStatusCode(response);
var payload = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(payload))
{
return string.Empty;
}
var json = JsonSerializer.Deserialize<JsonElement>(payload, _options);
if (json.ValueKind == JsonValueKind.Object && json.TryGetProperty("taskID", out var idElement))
{
return idElement.GetString() ?? string.Empty;
}
return string.Empty;
}
public async Task<IReadOnlyList<TorrentCreationTaskStatus>> GetTorrentCreationTasks(string? taskId = null)
{
HttpResponseMessage response;
if (string.IsNullOrWhiteSpace(taskId))
{
response = await _httpClient.GetAsync("torrentcreator/status");
}
else
{
var query = new QueryBuilder()
.Add("taskID", taskId);
response = await _httpClient.GetAsync("torrentcreator/status", query);
}
await ThrowIfNotSuccessfulStatusCode(response);
return await GetJsonList<TorrentCreationTaskStatus>(response.Content);
}
public async Task<byte[]> GetTorrentCreationTaskFile(string taskId)
{
var query = new QueryBuilder()
.Add("taskID", taskId);
var response = await _httpClient.GetAsync("torrentcreator/torrentFile", query);
await ThrowIfNotSuccessfulStatusCode(response);
return await response.Content.ReadAsByteArrayAsync();
}
public async Task DeleteTorrentCreationTask(string taskId)
{
var content = new FormUrlEncodedBuilder()
.Add("taskID", taskId)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrentcreator/deleteTask", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
#endregion Torrent creator
#region RSS
public async Task AddRssFolder(string path)
@@ -1334,4 +1684,4 @@ namespace Lantean.QBitTorrentClient
throw new HttpRequestException(errorMessage, null, response.StatusCode);
}
}
}
}

View File

@@ -1,24 +1,10 @@
using Lantean.QBitTorrentClient.Models;
using System.Linq;
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBitTorrentClient
{
public static class ApiClientExtensions
{
public static Task PauseTorrent(this IApiClient apiClient, string hash)
{
return apiClient.PauseTorrents(null, hash);
}
public static Task PauseTorrents(this IApiClient apiClient, IEnumerable<string> hashes)
{
return apiClient.PauseTorrents(null, hashes.ToArray());
}
public static Task PauseAllTorrents(this IApiClient apiClient)
{
return apiClient.PauseTorrents(true);
}
public static Task StopTorrent(this IApiClient apiClient, string hash)
{
return apiClient.StopTorrents(null, hash);
@@ -34,21 +20,6 @@ namespace Lantean.QBitTorrentClient
return apiClient.StopTorrents(true);
}
public static Task ResumeTorrent(this IApiClient apiClient, string hash)
{
return apiClient.ResumeTorrents(null, hash);
}
public static Task ResumeTorrents(this IApiClient apiClient, IEnumerable<string> hashes)
{
return apiClient.ResumeTorrents(null, hashes.ToArray());
}
public static Task ResumeAllTorrents(this IApiClient apiClient)
{
return apiClient.ResumeTorrents(true);
}
public static Task StartTorrent(this IApiClient apiClient, string hash)
{
return apiClient.StartTorrents(null, hash);
@@ -158,7 +129,7 @@ namespace Lantean.QBitTorrentClient
public static Task ReannounceTorrent(this IApiClient apiClient, string hash)
{
return apiClient.ReannounceTorrents(null, hash);
return apiClient.ReannounceTorrents(null, null, hash);
}
public static async Task<IEnumerable<string>> RemoveUnusedCategories(this IApiClient apiClient)
@@ -189,4 +160,4 @@ namespace Lantean.QBitTorrentClient
return unusedTags;
}
}
}
}

View File

@@ -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<DownloadPathOption>
{
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);
}
}
}

View File

@@ -1,4 +1,8 @@
using Lantean.QBitTorrentClient.Models;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBitTorrentClient
{
@@ -28,6 +32,12 @@ namespace Lantean.QBitTorrentClient
Task SetApplicationPreferences(UpdatePreferences preferences);
Task<IReadOnlyList<ApplicationCookie>> GetApplicationCookies();
Task SetApplicationCookies(IEnumerable<ApplicationCookie> cookies);
Task<string> RotateApiKey();
Task<string> GetDefaultSavePath();
Task<IReadOnlyList<NetworkInterface>> GetNetworkInterfaces();
@@ -36,6 +46,14 @@ namespace Lantean.QBitTorrentClient
#endregion Application
#region Client data
Task<IReadOnlyDictionary<string, JsonElement>> LoadClientData(IEnumerable<string>? keys = null);
Task StoreClientData(IReadOnlyDictionary<string, JsonElement> data);
#endregion Client data
#region Log
Task<IReadOnlyList<Log>> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null);
@@ -74,7 +92,7 @@ namespace Lantean.QBitTorrentClient
#region Torrent management
Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, params string[] hashes);
Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, bool? includeFiles = null, params string[] hashes);
Task<TorrentProperties> GetTorrentProperties(string hash);
@@ -82,16 +100,18 @@ namespace Lantean.QBitTorrentClient
Task<IReadOnlyList<WebSeed>> GetTorrentWebSeeds(string hash);
Task AddTorrentWebSeeds(string hash, IEnumerable<string> urls);
Task EditTorrentWebSeed(string hash, string originalUrl, string newUrl);
Task RemoveTorrentWebSeeds(string hash, IEnumerable<string> urls);
Task<IReadOnlyList<FileData>> GetTorrentContents(string hash, params int[] indexes);
Task<IReadOnlyList<PieceState>> GetTorrentPieceStates(string hash);
Task<IReadOnlyList<string>> GetTorrentPieceHashes(string hash);
Task PauseTorrents(bool? all = null, params string[] hashes);
Task ResumeTorrents(bool? all = null, params string[] hashes);
Task StartTorrents(bool? all = null, params string[] hashes);
Task StopTorrents(bool? all = null, params string[] hashes);
@@ -100,15 +120,15 @@ namespace Lantean.QBitTorrentClient
Task RecheckTorrents(bool? all = null, params string[] hashes);
Task ReannounceTorrents(bool? all = null, params string[] hashes);
Task ReannounceTorrents(bool? all = null, IEnumerable<string>? trackers = null, params string[] hashes);
Task AddTorrent(AddTorrentParams addTorrentParams);
Task<AddTorrentResult> AddTorrent(AddTorrentParams addTorrentParams);
Task AddTrackersToTorrent(string hash, IEnumerable<string> urls);
Task AddTrackersToTorrent(IEnumerable<string> urls, bool? all = null, params string[] hashes);
Task EditTracker(string hash, string originalUrl, string newUrl);
Task EditTracker(string hash, string url, string? newUrl = null, int? tier = null);
Task RemoveTrackers(string hash, IEnumerable<string> urls);
Task RemoveTrackers(IEnumerable<string> urls, bool? all = null, params string[] hashes);
Task AddPeers(IEnumerable<string> hashes, IEnumerable<PeerId> peers);
@@ -126,7 +146,7 @@ namespace Lantean.QBitTorrentClient
Task SetTorrentDownloadLimit(long limit, bool? all = null, params string[] hashes);
Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, bool? all = null, params string[] hashes);
Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, ShareLimitAction? shareLimitAction = null, bool? all = null, params string[] hashes);
Task<IReadOnlyDictionary<string, long>> GetTorrentUploadLimit(bool? all = null, params string[] hashes);
@@ -136,6 +156,8 @@ namespace Lantean.QBitTorrentClient
Task SetTorrentName(string name, string hash);
Task SetTorrentComment(IEnumerable<string> hashes, string comment);
Task SetTorrentCategory(string category, bool? all = null, params string[] hashes);
Task<IReadOnlyDictionary<string, Category>> GetAllCategories();
@@ -172,8 +194,26 @@ namespace Lantean.QBitTorrentClient
Task<string> GetExportUrl(string hash);
Task<TorrentMetadata?> FetchMetadata(string source, string? downloader = null);
Task<IReadOnlyList<TorrentMetadata>> ParseMetadata(IEnumerable<(string FileName, Stream Content)> torrents);
Task<byte[]> SaveMetadata(string source);
#endregion Torrent management
#region Torrent creator
Task<string> AddTorrentCreationTask(TorrentCreationTaskRequest request);
Task<IReadOnlyList<TorrentCreationTaskStatus>> GetTorrentCreationTasks(string? taskId = null);
Task<byte[]> GetTorrentCreationTaskFile(string taskId);
Task DeleteTorrentCreationTask(string taskId);
#endregion Torrent creator
#region RSS
Task AddRssFolder(string path);
@@ -230,4 +270,4 @@ namespace Lantean.QBitTorrentClient
#endregion Search
}
}
}

View File

@@ -12,9 +12,8 @@
public bool? AddToTopOfQueue { get; set; }
// v4
public bool? Paused { get; set; }
// v5
public bool? Forced { get; set; }
public bool? Stopped { get; set; }
public string? SavePath { get; set; }
@@ -47,8 +46,16 @@
public TorrentContentLayout? ContentLayout { get; set; }
public string? Cookie { get; set; }
public IEnumerable<Priority>? FilePriorities { get; set; }
public string? Downloader { get; set; }
public string? SslCertificate { get; set; }
public string? SslPrivateKey { get; set; }
public string? SslDhParams { get; set; }
public Dictionary<string, Stream>? Torrents { get; set; }
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public record AddTorrentResult
{
[JsonConstructor]
public AddTorrentResult(int successCount, int failureCount, int pendingCount, IReadOnlyList<string>? addedTorrentIds)
{
SuccessCount = successCount;
FailureCount = failureCount;
PendingCount = pendingCount;
AddedTorrentIds = addedTorrentIds ?? Array.Empty<string>();
}
[JsonPropertyName("success_count")]
public int SuccessCount { get; }
[JsonPropertyName("failure_count")]
public int FailureCount { get; }
[JsonPropertyName("pending_count")]
public int PendingCount { get; }
[JsonPropertyName("added_torrent_ids")]
public IReadOnlyList<string> AddedTorrentIds { get; }
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public record ApplicationCookie
{
[JsonConstructor]
public ApplicationCookie(string name, string? domain, string? path, string? value, long? expirationDate)
{
Name = name;
Domain = domain;
Path = path;
Value = value;
ExpirationDate = expirationDate;
}
[JsonPropertyName("name")]
public string Name { get; }
[JsonPropertyName("domain")]
public string? Domain { get; }
[JsonPropertyName("path")]
public string? Path { get; }
[JsonPropertyName("value")]
public string? Value { get; }
[JsonPropertyName("expirationDate")]
public long? ExpirationDate { get; }
}
}

View File

@@ -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; }
}
}
}

View File

@@ -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; }
}
}

View File

@@ -15,6 +15,7 @@ namespace Lantean.QBitTorrentClient.Models
IReadOnlyList<string>? tags,
IReadOnlyList<string>? tagsRemoved,
IReadOnlyDictionary<string, IReadOnlyList<string>> trackers,
IReadOnlyList<string>? trackersRemoved,
ServerState? serverState)
{
ResponseId = responseId;
@@ -26,6 +27,7 @@ namespace Lantean.QBitTorrentClient.Models
Tags = tags;
TagsRemoved = tagsRemoved;
Trackers = trackers;
TrackersRemoved = trackersRemoved;
ServerState = serverState;
}
@@ -62,4 +64,4 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("server_state")]
public ServerState? ServerState { get; }
}
}
}

View File

@@ -16,6 +16,7 @@ namespace Lantean.QBitTorrentClient.Models
string? flags,
string? flagsDescription,
string? iPAddress,
string? i2pDestination,
string? clientId,
int? port,
float? progress,
@@ -33,6 +34,7 @@ namespace Lantean.QBitTorrentClient.Models
Flags = flags;
FlagsDescription = flagsDescription;
IPAddress = iPAddress;
I2pDestination = i2pDestination;
ClientId = clientId;
Port = port;
Progress = progress;
@@ -71,6 +73,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("ip")]
public string? IPAddress { get; }
[JsonPropertyName("i2p_dest")]
public string? I2pDestination { get; }
[JsonPropertyName("peer_id_client")]
public string? ClientId { get; }
@@ -89,4 +94,4 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("uploaded")]
public long? Uploaded { get; }
}
}
}

View File

@@ -7,6 +7,7 @@ namespace Lantean.QBitTorrentClient.Models
[JsonConstructor]
public Preferences(
bool addToTopOfQueue,
bool addStoppedEnabled,
string addTrackers,
bool addTrackersEnabled,
int altDlLimit,
@@ -14,6 +15,7 @@ namespace Lantean.QBitTorrentClient.Models
bool alternativeWebuiEnabled,
string alternativeWebuiPath,
string announceIp,
int announcePort,
bool announceToAllTiers,
bool announceToAllTrackers,
bool anonymousMode,
@@ -85,6 +87,7 @@ namespace Lantean.QBitTorrentClient.Models
int i2pPort,
bool idnSupportEnabled,
bool incompleteFilesExt,
bool useUnwantedFolder,
bool ipFilterEnabled,
string ipFilterPath,
bool ipFilterTrackers,
@@ -92,6 +95,8 @@ namespace Lantean.QBitTorrentClient.Models
bool limitTcpOverhead,
bool limitUtpRate,
int listenPort,
bool sslEnabled,
int sslListenPort,
string locale,
bool lsd,
bool mailNotificationAuthEnabled,
@@ -160,6 +165,7 @@ namespace Lantean.QBitTorrentClient.Models
string savePath,
bool savePathChangedTmmEnabled,
int saveResumeDataInterval,
int saveStatisticsInterval,
Dictionary<string, SaveLocation> scanDirs,
int scheduleFromHour,
int scheduleFromMin,
@@ -177,12 +183,12 @@ namespace Lantean.QBitTorrentClient.Models
int socketReceiveBufferSize,
int socketSendBufferSize,
bool ssrfMitigation,
bool startPausedEnabled,
int stopTrackerTimeout,
string tempPath,
bool tempPathEnabled,
bool torrentChangedTmmEnabled,
string torrentContentLayout,
string torrentContentRemoveOption,
int torrentFileSizeLimit,
string torrentStopCondition,
int upLimit,
@@ -192,10 +198,12 @@ namespace Lantean.QBitTorrentClient.Models
int upnpLeaseDuration,
bool useCategoryPathsInManualMode,
bool useHttps,
bool ignoreSslErrors,
bool useSubcategories,
int utpTcpMixedMode,
bool validateHttpsTrackerCertificate,
string webUiAddress,
string webUiApiKey,
int webUiBanDuration,
bool webUiClickjackingProtectionEnabled,
bool webUiCsrfProtectionEnabled,
@@ -213,10 +221,14 @@ namespace Lantean.QBitTorrentClient.Models
bool webUiUpnp,
bool webUiUseCustomHttpHeadersEnabled,
string webUiUsername,
string webUiPassword
string webUiPassword,
bool confirmTorrentDeletion,
bool confirmTorrentRecheck,
bool statusBarExternalIp
)
{
AddToTopOfQueue = addToTopOfQueue;
AddStoppedEnabled = addStoppedEnabled;
AddTrackers = addTrackers;
AddTrackersEnabled = addTrackersEnabled;
AltDlLimit = altDlLimit;
@@ -224,6 +236,7 @@ namespace Lantean.QBitTorrentClient.Models
AlternativeWebuiEnabled = alternativeWebuiEnabled;
AlternativeWebuiPath = alternativeWebuiPath;
AnnounceIp = announceIp;
AnnouncePort = announcePort;
AnnounceToAllTiers = announceToAllTiers;
AnnounceToAllTrackers = announceToAllTrackers;
AnonymousMode = anonymousMode;
@@ -295,6 +308,7 @@ namespace Lantean.QBitTorrentClient.Models
I2pPort = i2pPort;
IdnSupportEnabled = idnSupportEnabled;
IncompleteFilesExt = incompleteFilesExt;
UseUnwantedFolder = useUnwantedFolder;
IpFilterEnabled = ipFilterEnabled;
IpFilterPath = ipFilterPath;
IpFilterTrackers = ipFilterTrackers;
@@ -302,6 +316,8 @@ namespace Lantean.QBitTorrentClient.Models
LimitTcpOverhead = limitTcpOverhead;
LimitUtpRate = limitUtpRate;
ListenPort = listenPort;
SslEnabled = sslEnabled;
SslListenPort = sslListenPort;
Locale = locale;
Lsd = lsd;
MailNotificationAuthEnabled = mailNotificationAuthEnabled;
@@ -370,6 +386,7 @@ namespace Lantean.QBitTorrentClient.Models
SavePath = savePath;
SavePathChangedTmmEnabled = savePathChangedTmmEnabled;
SaveResumeDataInterval = saveResumeDataInterval;
SaveStatisticsInterval = saveStatisticsInterval;
ScanDirs = scanDirs;
ScheduleFromHour = scheduleFromHour;
ScheduleFromMin = scheduleFromMin;
@@ -387,12 +404,12 @@ namespace Lantean.QBitTorrentClient.Models
SocketReceiveBufferSize = socketReceiveBufferSize;
SocketSendBufferSize = socketSendBufferSize;
SsrfMitigation = ssrfMitigation;
StartPausedEnabled = startPausedEnabled;
StopTrackerTimeout = stopTrackerTimeout;
TempPath = tempPath;
TempPathEnabled = tempPathEnabled;
TorrentChangedTmmEnabled = torrentChangedTmmEnabled;
TorrentContentLayout = torrentContentLayout;
TorrentContentRemoveOption = torrentContentRemoveOption;
TorrentFileSizeLimit = torrentFileSizeLimit;
TorrentStopCondition = torrentStopCondition;
UpLimit = upLimit;
@@ -402,10 +419,12 @@ namespace Lantean.QBitTorrentClient.Models
UpnpLeaseDuration = upnpLeaseDuration;
UseCategoryPathsInManualMode = useCategoryPathsInManualMode;
UseHttps = useHttps;
IgnoreSslErrors = ignoreSslErrors;
UseSubcategories = useSubcategories;
UtpTcpMixedMode = utpTcpMixedMode;
ValidateHttpsTrackerCertificate = validateHttpsTrackerCertificate;
WebUiAddress = webUiAddress;
WebUiApiKey = webUiApiKey;
WebUiBanDuration = webUiBanDuration;
WebUiClickjackingProtectionEnabled = webUiClickjackingProtectionEnabled;
WebUiCsrfProtectionEnabled = webUiCsrfProtectionEnabled;
@@ -424,11 +443,17 @@ namespace Lantean.QBitTorrentClient.Models
WebUiUseCustomHttpHeadersEnabled = webUiUseCustomHttpHeadersEnabled;
WebUiUsername = webUiUsername;
WebUiPassword = webUiPassword;
ConfirmTorrentDeletion = confirmTorrentDeletion;
ConfirmTorrentRecheck = confirmTorrentRecheck;
StatusBarExternalIp = statusBarExternalIp;
}
[JsonPropertyName("add_to_top_of_queue")]
public bool AddToTopOfQueue { get; }
[JsonPropertyName("add_stopped_enabled")]
public bool AddStoppedEnabled { get; }
[JsonPropertyName("add_trackers")]
public string AddTrackers { get; }
@@ -450,6 +475,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("announce_ip")]
public string AnnounceIp { get; }
[JsonPropertyName("announce_port")]
public int AnnouncePort { get; }
[JsonPropertyName("announce_to_all_tiers")]
public bool AnnounceToAllTiers { get; }
@@ -663,6 +691,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("incomplete_files_ext")]
public bool IncompleteFilesExt { get; }
[JsonPropertyName("use_unwanted_folder")]
public bool UseUnwantedFolder { get; }
[JsonPropertyName("ip_filter_enabled")]
public bool IpFilterEnabled { get; }
@@ -684,6 +715,12 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("listen_port")]
public int ListenPort { get; }
[JsonPropertyName("ssl_enabled")]
public bool SslEnabled { get; }
[JsonPropertyName("ssl_listen_port")]
public int SslListenPort { get; }
[JsonPropertyName("locale")]
public string Locale { get; }
@@ -888,6 +925,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("save_resume_data_interval")]
public int SaveResumeDataInterval { get; }
[JsonPropertyName("save_statistics_interval")]
public int SaveStatisticsInterval { get; }
[JsonPropertyName("scan_dirs")]
public Dictionary<string, SaveLocation> ScanDirs { get; }
@@ -939,9 +979,6 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("ssrf_mitigation")]
public bool SsrfMitigation { get; }
[JsonPropertyName("start_paused_enabled")]
public bool StartPausedEnabled { get; }
[JsonPropertyName("stop_tracker_timeout")]
public int StopTrackerTimeout { get; }
@@ -957,6 +994,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("torrent_content_layout")]
public string TorrentContentLayout { get; }
[JsonPropertyName("torrent_content_remove_option")]
public string TorrentContentRemoveOption { get; }
[JsonPropertyName("torrent_file_size_limit")]
public int TorrentFileSizeLimit { get; }
@@ -984,6 +1024,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("use_https")]
public bool UseHttps { get; }
[JsonPropertyName("ignore_ssl_errors")]
public bool IgnoreSslErrors { get; }
[JsonPropertyName("use_subcategories")]
public bool UseSubcategories { get; }
@@ -996,6 +1039,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("web_ui_address")]
public string WebUiAddress { get; }
[JsonPropertyName("web_ui_api_key")]
public string WebUiApiKey { get; }
[JsonPropertyName("web_ui_ban_duration")]
public int WebUiBanDuration { get; }
@@ -1049,5 +1095,14 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("web_ui_password")]
public string WebUiPassword { get; }
[JsonPropertyName("confirm_torrent_deletion")]
public bool ConfirmTorrentDeletion { get; }
[JsonPropertyName("confirm_torrent_recheck")]
public bool ConfirmTorrentRecheck { get; }
[JsonPropertyName("status_bar_external_ip")]
public bool StatusBarExternalIp { get; }
}
}
}

View File

@@ -30,7 +30,9 @@ namespace Lantean.QBitTorrentClient.Models
long? uploadRateLimit,
bool? useAltSpeedLimits,
bool? useSubcategories,
float? writeCacheOverload) : base(connectionStatus, dHTNodes, downloadInfoData, downloadInfoSpeed, downloadRateLimit, uploadInfoData, uploadInfoSpeed, uploadRateLimit)
float? writeCacheOverload,
string? lastExternalAddressV4 = null,
string? lastExternalAddressV6 = null) : base(connectionStatus, dHTNodes, downloadInfoData, downloadInfoSpeed, downloadRateLimit, uploadInfoData, uploadInfoSpeed, uploadRateLimit)
{
AllTimeDownloaded = allTimeDownloaded;
AllTimeUploaded = allTimeUploaded;
@@ -49,6 +51,8 @@ namespace Lantean.QBitTorrentClient.Models
UseAltSpeedLimits = useAltSpeedLimits;
UseSubcategories = useSubcategories;
WriteCacheOverload = writeCacheOverload;
LastExternalAddressV4 = lastExternalAddressV4;
LastExternalAddressV6 = lastExternalAddressV6;
}
[JsonPropertyName("alltime_dl")]
@@ -101,5 +105,11 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("write_cache_overload")]
public float? WriteCacheOverload { get; }
[JsonPropertyName("last_external_address_v4")]
public string? LastExternalAddressV4 { get; }
[JsonPropertyName("last_external_address_v6")]
public string? LastExternalAddressV6 { get; }
}
}
}

View File

@@ -1,264 +1,219 @@
using Lantean.QBitTorrentClient.Converters;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public record Torrent
{
[JsonConstructor]
public Torrent(
long? addedOn,
long? amountLeft,
bool? automaticTorrentManagement,
float? availability,
string? category,
long? completed,
long? completionOn,
string? contentPath,
long? downloadLimit,
long? downloadSpeed,
long? downloaded,
long? downloadedSession,
long? estimatedTimeOfArrival,
bool? firstLastPiecePriority,
bool? forceStart,
string hash,
string? infoHashV1,
string? infoHashV2,
long? lastActivity,
string? magnetUri,
float? maxRatio,
int? maxSeedingTime,
string? name,
int? numberComplete,
int? numberIncomplete,
int? numberLeeches,
int? numberSeeds,
int? priority,
float? progress,
float? ratio,
float? ratioLimit,
string? savePath,
long? seedingTime,
int? seedingTimeLimit,
long? seenComplete,
bool? sequentialDownload,
long? size,
string? state,
bool? superSeeding,
IReadOnlyList<string>? tags,
int? timeActive,
long? totalSize,
string? tracker,
long? uploadLimit,
long? uploaded,
long? uploadedSession,
long? uploadSpeed,
long? reannounce,
float? inactiveSeedingTimeLimit,
float? maxInactiveSeedingTime)
{
AddedOn = addedOn;
AmountLeft = amountLeft;
AutomaticTorrentManagement = automaticTorrentManagement;
Availability = availability;
Category = category;
Completed = completed;
CompletionOn = completionOn;
ContentPath = contentPath;
DownloadLimit = downloadLimit;
DownloadSpeed = downloadSpeed;
Downloaded = downloaded;
DownloadedSession = downloadedSession;
EstimatedTimeOfArrival = estimatedTimeOfArrival;
FirstLastPiecePriority = firstLastPiecePriority;
ForceStart = forceStart;
Hash = hash;
InfoHashV1 = infoHashV1;
InfoHashV2 = infoHashV2;
LastActivity = lastActivity;
MagnetUri = magnetUri;
MaxRatio = maxRatio;
MaxSeedingTime = maxSeedingTime;
Name = name;
NumberComplete = numberComplete;
NumberIncomplete = numberIncomplete;
NumberLeeches = numberLeeches;
NumberSeeds = numberSeeds;
Priority = priority;
Progress = progress;
Ratio = ratio;
RatioLimit = ratioLimit;
SavePath = savePath;
SeedingTime = seedingTime;
SeedingTimeLimit = seedingTimeLimit;
SeenComplete = seenComplete;
SequentialDownload = sequentialDownload;
Size = size;
State = state;
SuperSeeding = superSeeding;
Tags = tags ?? [];
TimeActive = timeActive;
TotalSize = totalSize;
Tracker = tracker;
UploadLimit = uploadLimit;
Uploaded = uploaded;
UploadedSession = uploadedSession;
UploadSpeed = uploadSpeed;
Reannounce = reannounce;
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
MaxInactiveSeedingTime = maxInactiveSeedingTime;
}
[JsonPropertyName("added_on")]
public long? AddedOn { get; }
[JsonPropertyName("amount_left")]
public long? AmountLeft { get; }
[JsonPropertyName("auto_tmm")]
public bool? AutomaticTorrentManagement { get; }
[JsonPropertyName("availability")]
public float? Availability { get; }
[JsonPropertyName("category")]
public string? Category { get; }
[JsonPropertyName("completed")]
public long? Completed { get; }
[JsonPropertyName("completion_on")]
public long? CompletionOn { get; }
[JsonPropertyName("content_path")]
public string? ContentPath { get; }
[JsonPropertyName("dl_limit")]
public long? DownloadLimit { get; }
[JsonPropertyName("dlspeed")]
public long? DownloadSpeed { get; }
[JsonPropertyName("downloaded")]
public long? Downloaded { get; }
[JsonPropertyName("downloaded_session")]
public long? DownloadedSession { get; }
[JsonPropertyName("eta")]
public long? EstimatedTimeOfArrival { get; }
[JsonPropertyName("f_l_piece_prio")]
public bool? FirstLastPiecePriority { get; }
[JsonPropertyName("force_start")]
public bool? ForceStart { get; }
[JsonPropertyName("hash")]
public string Hash { get; }
public string Hash { get; init; } = string.Empty;
[JsonPropertyName("infohash_v1")]
public string? InfoHashV1 { get; }
public string? InfoHashV1 { get; init; }
[JsonPropertyName("infohash_v2")]
public string? InfoHashV2 { get; }
[JsonPropertyName("last_activity")]
public long? LastActivity { get; }
[JsonPropertyName("magnet_uri")]
public string? MagnetUri { get; }
[JsonPropertyName("max_ratio")]
public float? MaxRatio { get; }
[JsonPropertyName("max_seeding_time")]
public int? MaxSeedingTime { get; }
public string? InfoHashV2 { get; init; }
[JsonPropertyName("name")]
public string? Name { get; }
public string? Name { get; init; }
[JsonPropertyName("num_complete")]
public int? NumberComplete { get; }
[JsonPropertyName("num_incomplete")]
public int? NumberIncomplete { get; }
[JsonPropertyName("num_leechs")]
public int? NumberLeeches { get; }
[JsonPropertyName("num_seeds")]
public int? NumberSeeds { get; }
[JsonPropertyName("priority")]
public int? Priority { get; }
[JsonPropertyName("progress")]
public float? Progress { get; }
[JsonPropertyName("ratio")]
public float? Ratio { get; }
[JsonPropertyName("ratio_limit")]
public float? RatioLimit { get; }
[JsonPropertyName("save_path")]
public string? SavePath { get; }
[JsonPropertyName("seeding_time")]
public long? SeedingTime { get; }
[JsonPropertyName("seeding_time_limit")]
public int? SeedingTimeLimit { get; }
[JsonPropertyName("seen_complete")]
public long? SeenComplete { get; }
[JsonPropertyName("seq_dl")]
public bool? SequentialDownload { get; }
[JsonPropertyName("magnet_uri")]
public string? MagnetUri { get; init; }
[JsonPropertyName("size")]
public long? Size { get; }
public long? Size { get; init; }
[JsonPropertyName("progress")]
public float? Progress { get; init; }
[JsonPropertyName("dlspeed")]
public long? DownloadSpeed { get; init; }
[JsonPropertyName("upspeed")]
public long? UploadSpeed { get; init; }
[JsonPropertyName("priority")]
public int? Priority { get; init; }
[JsonPropertyName("num_seeds")]
public int? NumberSeeds { get; init; }
[JsonPropertyName("num_complete")]
public int? NumberComplete { get; init; }
[JsonPropertyName("num_leechs")]
public int? NumberLeeches { get; init; }
[JsonPropertyName("num_incomplete")]
public int? NumberIncomplete { get; init; }
[JsonPropertyName("ratio")]
public float? Ratio { get; init; }
[JsonPropertyName("popularity")]
public float? Popularity { get; init; }
[JsonPropertyName("eta")]
public long? EstimatedTimeOfArrival { get; init; }
[JsonPropertyName("state")]
public string? State { get; }
public string? State { get; init; }
[JsonPropertyName("super_seeding")]
public bool? SuperSeeding { get; }
[JsonPropertyName("seq_dl")]
public bool? SequentialDownload { get; init; }
[JsonPropertyName("f_l_piece_prio")]
public bool? FirstLastPiecePriority { get; init; }
[JsonPropertyName("category")]
public string? Category { get; init; }
[JsonPropertyName("tags")]
[JsonConverter(typeof(CommaSeparatedJsonConverter))]
public IReadOnlyList<string>? Tags { get; }
public IReadOnlyList<string> Tags { get; init; } = Array.Empty<string>();
[JsonPropertyName("time_active")]
public int? TimeActive { get; }
[JsonPropertyName("super_seeding")]
public bool? SuperSeeding { get; init; }
[JsonPropertyName("total_size")]
public long? TotalSize { get; }
[JsonPropertyName("force_start")]
public bool? ForceStart { get; init; }
[JsonPropertyName("save_path")]
public string? SavePath { get; init; }
[JsonPropertyName("download_path")]
public string? DownloadPath { get; init; }
[JsonPropertyName("content_path")]
public string? ContentPath { get; init; }
[JsonPropertyName("root_path")]
public string? RootPath { get; init; }
[JsonPropertyName("added_on")]
public long? AddedOn { get; init; }
[JsonPropertyName("completion_on")]
public long? CompletionOn { get; init; }
[JsonPropertyName("tracker")]
public string? Tracker { get; }
public string? Tracker { get; init; }
[JsonPropertyName("trackers_count")]
public int? TrackersCount { get; init; }
[JsonPropertyName("dl_limit")]
public long? DownloadLimit { get; init; }
[JsonPropertyName("up_limit")]
public long? UploadLimit { get; }
public long? UploadLimit { get; init; }
[JsonPropertyName("downloaded")]
public long? Downloaded { get; init; }
[JsonPropertyName("uploaded")]
public long? Uploaded { get; }
public long? Uploaded { get; init; }
[JsonPropertyName("downloaded_session")]
public long? DownloadedSession { get; init; }
[JsonPropertyName("uploaded_session")]
public long? UploadedSession { get; }
public long? UploadedSession { get; init; }
[JsonPropertyName("upspeed")]
public long? UploadSpeed { get; }
[JsonPropertyName("amount_left")]
public long? AmountLeft { get; init; }
[JsonPropertyName("reannounce")]
public long? Reannounce { get; }
[JsonPropertyName("completed")]
public long? Completed { get; init; }
[JsonPropertyName("inactive_seeding_time_limit")]
public float? InactiveSeedingTimeLimit { get; }
[JsonPropertyName("connections_count")]
public int? ConnectionsCount { get; init; }
[JsonPropertyName("connections_limit")]
public int? ConnectionsLimit { get; init; }
[JsonPropertyName("max_ratio")]
public float? MaxRatio { get; init; }
[JsonPropertyName("max_seeding_time")]
public int? MaxSeedingTime { get; init; }
[JsonPropertyName("max_inactive_seeding_time")]
public float? MaxInactiveSeedingTime { get; }
public float? MaxInactiveSeedingTime { get; init; }
[JsonPropertyName("ratio_limit")]
public float? RatioLimit { get; init; }
[JsonPropertyName("seeding_time_limit")]
public int? SeedingTimeLimit { get; init; }
[JsonPropertyName("inactive_seeding_time_limit")]
public float? InactiveSeedingTimeLimit { get; init; }
[JsonPropertyName("share_limit_action")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public ShareLimitAction? ShareLimitAction { get; init; }
[JsonPropertyName("seen_complete")]
public long? SeenComplete { get; init; }
[JsonPropertyName("last_activity")]
public long? LastActivity { get; init; }
[JsonPropertyName("total_size")]
public long? TotalSize { get; init; }
[JsonPropertyName("auto_tmm")]
public bool? AutomaticTorrentManagement { get; init; }
[JsonPropertyName("time_active")]
public int? TimeActive { get; init; }
[JsonPropertyName("seeding_time")]
public long? SeedingTime { get; init; }
[JsonPropertyName("availability")]
public float? Availability { get; init; }
[JsonPropertyName("reannounce")]
public long? Reannounce { get; init; }
[JsonPropertyName("comment")]
public string? Comment { get; init; }
[JsonPropertyName("has_metadata")]
public bool? HasMetadata { get; init; }
[JsonPropertyName("created_by")]
public string? CreatedBy { get; init; }
[JsonPropertyName("creation_date")]
public long? CreationDate { get; init; }
[JsonPropertyName("private")]
public bool? IsPrivate { get; init; }
[JsonPropertyName("total_wasted")]
public long? TotalWasted { get; init; }
[JsonPropertyName("pieces_num")]
public int? PiecesCount { get; init; }
[JsonPropertyName("piece_size")]
public long? PieceSize { get; init; }
[JsonPropertyName("pieces_have")]
public int? PiecesHave { get; init; }
[JsonPropertyName("has_tracker_warning")]
public bool? HasTrackerWarning { get; init; }
[JsonPropertyName("has_tracker_error")]
public bool? HasTrackerError { get; init; }
[JsonPropertyName("has_other_announce_error")]
public bool? HasOtherAnnounceError { get; init; }
}
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public class TorrentCreationTaskRequest
{
public string SourcePath { get; set; } = string.Empty;
public string? TorrentFilePath { get; set; }
public int? PieceSize { get; set; }
public bool? Private { get; set; }
public bool? StartSeeding { get; set; }
public string? Comment { get; set; }
public string? Source { get; set; }
public IEnumerable<string>? Trackers { get; set; }
public IEnumerable<string>? UrlSeeds { get; set; }
public string? Format { get; set; }
public bool? OptimizeAlignment { get; set; }
public int? PaddedFileSizeLimit { get; set; }
}
public record TorrentCreationTaskStatus
{
[JsonConstructor]
public TorrentCreationTaskStatus(
string taskID,
string? sourcePath,
int? pieceSize,
bool? @private,
string? timeAdded,
string? format,
bool? optimizeAlignment,
int? paddedFileSizeLimit,
string? status,
string? comment,
string? torrentFilePath,
string? source,
IReadOnlyList<string>? trackers,
IReadOnlyList<string>? urlSeeds,
string? timeStarted,
string? timeFinished,
string? errorMessage,
double? progress)
{
TaskId = taskID;
SourcePath = sourcePath;
PieceSize = pieceSize;
Private = @private;
TimeAdded = timeAdded;
Format = format;
OptimizeAlignment = optimizeAlignment;
PaddedFileSizeLimit = paddedFileSizeLimit;
Status = status;
Comment = comment;
TorrentFilePath = torrentFilePath;
Source = source;
Trackers = trackers ?? Array.Empty<string>();
UrlSeeds = urlSeeds ?? Array.Empty<string>();
TimeStarted = timeStarted;
TimeFinished = timeFinished;
ErrorMessage = errorMessage;
Progress = progress;
}
[JsonPropertyName("taskID")]
public string TaskId { get; }
[JsonPropertyName("sourcePath")]
public string? SourcePath { get; }
[JsonPropertyName("pieceSize")]
public int? PieceSize { get; }
[JsonPropertyName("private")]
public bool? Private { get; }
[JsonPropertyName("timeAdded")]
public string? TimeAdded { get; }
[JsonPropertyName("format")]
public string? Format { get; }
[JsonPropertyName("optimizeAlignment")]
public bool? OptimizeAlignment { get; }
[JsonPropertyName("paddedFileSizeLimit")]
public int? PaddedFileSizeLimit { get; }
[JsonPropertyName("status")]
public string? Status { get; }
[JsonPropertyName("comment")]
public string? Comment { get; }
[JsonPropertyName("torrentFilePath")]
public string? TorrentFilePath { get; }
[JsonPropertyName("source")]
public string? Source { get; }
[JsonPropertyName("trackers")]
public IReadOnlyList<string> Trackers { get; }
[JsonPropertyName("urlSeeds")]
public IReadOnlyList<string> UrlSeeds { get; }
[JsonPropertyName("timeStarted")]
public string? TimeStarted { get; }
[JsonPropertyName("timeFinished")]
public string? TimeFinished { get; }
[JsonPropertyName("errorMessage")]
public string? ErrorMessage { get; }
[JsonPropertyName("progress")]
public double? Progress { get; }
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public record TorrentMetadata
{
[JsonConstructor]
public TorrentMetadata(
string? infoHashV1,
string? infoHashV2,
string? hash,
TorrentMetadataInfo? info,
IReadOnlyList<TorrentMetadataTracker>? trackers,
IReadOnlyList<string>? webSeeds,
string? createdBy,
long? creationDate,
string? comment)
{
InfoHashV1 = infoHashV1;
InfoHashV2 = infoHashV2;
Hash = hash;
Info = info;
Trackers = trackers ?? Array.Empty<TorrentMetadataTracker>();
WebSeeds = webSeeds ?? Array.Empty<string>();
CreatedBy = createdBy;
CreationDate = creationDate;
Comment = comment;
}
[JsonPropertyName("infohash_v1")]
public string? InfoHashV1 { get; }
[JsonPropertyName("infohash_v2")]
public string? InfoHashV2 { get; }
[JsonPropertyName("hash")]
public string? Hash { get; }
[JsonPropertyName("info")]
public TorrentMetadataInfo? Info { get; }
[JsonPropertyName("trackers")]
public IReadOnlyList<TorrentMetadataTracker> Trackers { get; }
[JsonPropertyName("webseeds")]
public IReadOnlyList<string> WebSeeds { get; }
[JsonPropertyName("created_by")]
public string? CreatedBy { get; }
[JsonPropertyName("creation_date")]
public long? CreationDate { get; }
[JsonPropertyName("comment")]
public string? Comment { get; }
}
public record TorrentMetadataInfo
{
[JsonConstructor]
public TorrentMetadataInfo(
IReadOnlyList<TorrentMetadataFile>? files,
long? length,
string? name,
long? pieceLength,
int? piecesCount,
bool? @private)
{
Files = files ?? Array.Empty<TorrentMetadataFile>();
Length = length;
Name = name;
PieceLength = pieceLength;
PiecesCount = piecesCount;
Private = @private;
}
[JsonPropertyName("files")]
public IReadOnlyList<TorrentMetadataFile> Files { get; }
[JsonPropertyName("length")]
public long? Length { get; }
[JsonPropertyName("name")]
public string? Name { get; }
[JsonPropertyName("piece_length")]
public long? PieceLength { get; }
[JsonPropertyName("pieces_num")]
public int? PiecesCount { get; }
[JsonPropertyName("private")]
public bool? Private { get; }
}
public record TorrentMetadataFile(
[property: JsonPropertyName("path")] string? Path,
[property: JsonPropertyName("length")] long? Length);
public record TorrentMetadataTracker(
[property: JsonPropertyName("url")] string? Url,
[property: JsonPropertyName("tier")] int? Tier);
}

View File

@@ -1,4 +1,6 @@
using System.Text.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
@@ -13,7 +15,10 @@ namespace Lantean.QBitTorrentClient.Models
int seeds,
int leeches,
int downloads,
string message)
string message,
long? nextAnnounce,
long? minAnnounce,
IReadOnlyList<TrackerEndpoint>? endpoints)
{
Url = url;
Status = status;
@@ -23,6 +28,9 @@ namespace Lantean.QBitTorrentClient.Models
Leeches = leeches;
Downloads = downloads;
Message = message;
NextAnnounce = nextAnnounce;
MinAnnounce = minAnnounce;
Endpoints = endpoints ?? Array.Empty<TrackerEndpoint>();
}
[JsonPropertyName("url")]
@@ -48,5 +56,27 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("msg")]
public string Message { get; }
[JsonPropertyName("next_announce")]
public long? NextAnnounce { get; }
[JsonPropertyName("min_announce")]
public long? MinAnnounce { get; }
[JsonPropertyName("endpoints")]
public IReadOnlyList<TrackerEndpoint> Endpoints { get; }
}
}
public record TrackerEndpoint(
[property: JsonPropertyName("name")] string? Name,
[property: JsonPropertyName("updating")] bool? Updating,
[property: JsonPropertyName("status")] TrackerStatus Status,
[property: JsonPropertyName("msg")] string? Message,
[property: JsonPropertyName("bt_version")] int? BitTorrentVersion,
[property: JsonPropertyName("num_peers")] int? Peers,
[property: JsonPropertyName("num_seeds")] int? Seeds,
[property: JsonPropertyName("num_leeches")] int? Leeches,
[property: JsonPropertyName("num_downloaded")] int? Downloads,
[property: JsonPropertyName("next_announce")] long? NextAnnounce,
[property: JsonPropertyName("min_announce")] long? MinAnnounce);
}

View File

@@ -7,5 +7,7 @@
Working = 2,
Updating = 3,
NotWorking = 4,
Error = 5,
Unreachable = 6
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using System;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
@@ -7,6 +8,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("add_to_top_of_queue")]
public bool? AddToTopOfQueue { get; set; }
[JsonPropertyName("add_stopped_enabled")]
public bool? AddStoppedEnabled { get; set; }
[JsonPropertyName("add_trackers")]
public string? AddTrackers { get; set; }
@@ -28,6 +32,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("announce_ip")]
public string? AnnounceIp { get; set; }
[JsonPropertyName("announce_port")]
public int? AnnouncePort { get; set; }
[JsonPropertyName("announce_to_all_tiers")]
public bool? AnnounceToAllTiers { get; set; }
@@ -241,6 +248,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("incomplete_files_ext")]
public bool? IncompleteFilesExt { get; set; }
[JsonPropertyName("use_unwanted_folder")]
public bool? UseUnwantedFolder { get; set; }
[JsonPropertyName("ip_filter_enabled")]
public bool? IpFilterEnabled { get; set; }
@@ -262,6 +272,12 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("listen_port")]
public int? ListenPort { get; set; }
[JsonPropertyName("ssl_enabled")]
public bool? SslEnabled { get; set; }
[JsonPropertyName("ssl_listen_port")]
public int? SslListenPort { get; set; }
[JsonPropertyName("locale")]
public string? Locale { get; set; }
@@ -466,6 +482,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("save_resume_data_interval")]
public int? SaveResumeDataInterval { get; set; }
[JsonPropertyName("save_statistics_interval")]
public int? SaveStatisticsInterval { get; set; }
[JsonPropertyName("scan_dirs")]
public Dictionary<string, SaveLocation>? ScanDirs { get; set; }
@@ -517,9 +536,6 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("ssrf_mitigation")]
public bool? SsrfMitigation { get; set; }
[JsonPropertyName("start_paused_enabled")]
public bool? StartPausedEnabled { get; set; }
[JsonPropertyName("stop_tracker_timeout")]
public int? StopTrackerTimeout { get; set; }
@@ -535,6 +551,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("torrent_content_layout")]
public string? TorrentContentLayout { get; set; }
[JsonPropertyName("torrent_content_remove_option")]
public string? TorrentContentRemoveOption { get; set; }
[JsonPropertyName("torrent_file_size_limit")]
public int? TorrentFileSizeLimit { get; set; }
@@ -562,6 +581,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("use_https")]
public bool? UseHttps { get; set; }
[JsonPropertyName("ignore_ssl_errors")]
public bool? IgnoreSslErrors { get; set; }
[JsonPropertyName("use_subcategories")]
public bool? UseSubcategories { get; set; }
@@ -574,6 +596,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("web_ui_address")]
public string? WebUiAddress { get; set; }
[JsonPropertyName("web_ui_api_key")]
public string? WebUiApiKey { get; set; }
[JsonPropertyName("web_ui_ban_duration")]
public int? WebUiBanDuration { get; set; }
@@ -627,5 +652,32 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("web_ui_password")]
public string? WebUiPassword { get; set; }
[JsonPropertyName("confirm_torrent_deletion")]
public bool? ConfirmTorrentDeletion { get; set; }
[JsonPropertyName("confirm_torrent_recheck")]
public bool? ConfirmTorrentRecheck { get; set; }
[JsonPropertyName("status_bar_external_ip")]
public bool? StatusBarExternalIp { get; set; }
public void Validate()
{
if (MaxRatio.HasValue && MaxRatioEnabled.HasValue)
{
throw new InvalidOperationException("Specify either max_ratio or max_ratio_enabled, not both.");
}
if (MaxSeedingTime.HasValue && MaxSeedingTimeEnabled.HasValue)
{
throw new InvalidOperationException("Specify either max_seeding_time or max_seeding_time_enabled, not both.");
}
if (MaxInactiveSeedingTime.HasValue && MaxInactiveSeedingTimeEnabled.HasValue)
{
throw new InvalidOperationException("Specify either max_inactive_seeding_time or max_inactive_seeding_time_enabled, not both.");
}
}
}
}
}

25
Upgrade-To-v5-Planning.md Normal file
View File

@@ -0,0 +1,25 @@
# Upgrade to qBittorrent WebUI v5 UI Alignment Plan
## Torrent List Filtering
- **Regex toggle & field selector**: Introduce the regex checkbox and the "Filter by" (Name/Save path) select found in v5. Update `FilterState`/`LoggedInLayout` to carry both values, wire them to `TorrentList`s toolbar, and validate invalid patterns gracefully.
- **Filter helper parity**: Rework `FilterHelper.ContainsAllTerms/FilterTerms` to mirror `window.qBittorrent.Misc.containsAllTerms` (evaluate every term, respect `+`/`-` prefixes). Ensure filtering applies to the selected field, not just the torrent name.
- **New status buckets**: Add `Running` and `Moving` to `Status` enum, update `FilterHelper.FilterStatus`, `DisplayHelpers`, and `FiltersNav` so counts/icons match upstream.
## Tracker Filters
- **Special buckets**: Extend `FilterHelper`/`DataManager` to create sets for "Announce error", "Error", "Warning", and "Trackerless" in addition to "All". Store the required flags on the UI `Torrent` model (`HasTrackerError`, `HasTrackerWarning`, `HasOtherAnnounceError`, `TrackersCount`, etc.).
- **Tracker grouping & removal**: When grouping trackers by host in `FiltersNav`, retain original URL entries so removal can target the right string. Replace the placeholder "Remove tracker" action with a real implementation and disable it for synthetic buckets.
## ~~Torrent Data Model & Columns~~
- ~~**Model sync**: Bring `Lantean.QBTMud.Models.Torrent` into parity with v5 (`Popularity`, `DownloadPath`, `RootPath`, `InfoHashV1/2`, `IsPrivate`, share-limit action fields, tracker flags, etc.) and map them in `DataManager.CreateTorrent`.~~
- ~~**Column set alignment**: Match the v5 table defaults—add missing columns (Popularity, Reannounce in, Info hashes, Download path, Private, etc.), fix "Ratio Limit" to display `RatioLimit`, and ensure column ordering/enabled state mirrors `DynamicTable.TorrentsTable`.~~
- ~~**Helper updates**: Extend `DisplayHelpers` to format the new fields (popularity, private flag, info hashes, error state icons).~~
## Actions & Dialogs
- ~~**Copy submenu**: Add "Copy comment" and "Copy content path" to the copy submenu in `TorrentActions`, keeping clipboard behaviour identical to v5.~~
- ~~**Share ratio dialog**: Update `ShareRatioDialog`, `ShareRatio/ShareRatioMax`, and `DialogHelper.InvokeShareRatioDialog` to surface `ShareLimitAction`, fix the `MaxInactiveSeedingTime` mapping, and call `SetTorrentShareLimit` with the action.~~
## Add-Torrent Flow
- Mirror the v5 add-torrent pane: add controls for incomplete save path, tags, auto-start, queue position, share-limit action, etc., in `AddTorrentOptions.razor`, and wire the new fields into the submission object.
## ~~Preferences & Local Settings~~
- ~~Introduce new v5 toggles such as "Display full tracker URL" in `AdvancedOptions`, persist them via the preferences service, and respect the setting in the tracker column rendering.~~