From 7c4a185b589b561e281c4826626962f96abec9ac Mon Sep 17 00:00:00 2001 From: ahjephson Date: Wed, 28 Aug 2024 16:15:59 +0100 Subject: [PATCH] Add initial RSS implementation --- .editorconfig | 4 + Lantean.QBTBlazor.sln | 5 + .../Components/ApplicationActions.razor.cs | 42 ++++-- .../Dialogs/AddTorrentLinkDialog.razor.cs | 11 ++ .../Components/Dialogs/ShareRatioDialog.razor | 2 +- .../Components/EnhancedErrorBoundary.cs | 2 +- Lantean.QBTMudBlade/Components/Menu.razor | 9 +- Lantean.QBTMudBlade/Components/Menu.razor.cs | 14 ++ .../Components/TorrentActions.razor.cs | 12 +- .../Components/TrackersTab.razor.cs | 1 + .../Components/UI/DynamicTable.razor.cs | 1 + .../Components/WebSeedsTab.razor.cs | 1 + Lantean.QBTMudBlade/GlobalSuppressions.cs | 1 - Lantean.QBTMudBlade/Helpers/DialogHelper.cs | 9 +- .../Lantean.QBTMudBlade.csproj | 8 +- .../Layout/LoggedInLayout.razor.cs | 6 + Lantean.QBTMudBlade/Layout/MainLayout.razor | 9 +- .../Layout/MainLayout.razor.cs | 9 +- Lantean.QBTMudBlade/Layout/OtherLayout.razor | 2 +- .../Layout/OtherLayout.razor.cs | 6 +- .../ColumnDefinition.cs} | 31 +--- Lantean.QBTMudBlade/Models/FilterState.cs | 2 +- Lantean.QBTMudBlade/Models/LogForm.cs | 2 + Lantean.QBTMudBlade/Models/RowContext.cs | 24 +++ Lantean.QBTMudBlade/Pages/Login.razor.cs | 2 +- Lantean.QBTMudBlade/Pages/Options.razor.cs | 15 +- Lantean.QBTMudBlade/Pages/Rss.razor | 62 +++++++- Lantean.QBTMudBlade/Pages/Rss.razor.cs | 88 +++++++++++ Lantean.QBTMudBlade/Pages/Search.razor.cs | 2 +- .../Pages/TorrentList.razor.cs | 4 +- Lantean.QBTMudBlade/Program.cs | 2 + Lantean.QBTMudBlade/Services/DataManager.cs | 8 +- Lantean.QBTMudBlade/Services/HttpLogger.cs | 25 ++-- Lantean.QBTMudBlade/wwwroot/css/app.css | 6 +- Lantean.QBitTorrentClient/ApiClient.cs | 138 +++++++++++++++++- .../FormUrlEncodedBuilder.cs | 12 +- Lantean.QBitTorrentClient/IApiClient.cs | 24 ++- .../Models/AutoDownloadingRule.cs | 77 ++++++++++ .../Models/RssArticle.cs | 57 ++++++++ Lantean.QBitTorrentClient/Models/RssItem.cs | 47 ++++++ Lantean.QBitTorrentClient/QueryBuilder.cs | 12 +- 41 files changed, 678 insertions(+), 116 deletions(-) create mode 100644 .editorconfig rename Lantean.QBTMudBlade/{TableHelper.cs => Models/ColumnDefinition.cs} (72%) create mode 100644 Lantean.QBTMudBlade/Models/RowContext.cs create mode 100644 Lantean.QBitTorrentClient/Models/AutoDownloadingRule.cs create mode 100644 Lantean.QBitTorrentClient/Models/RssArticle.cs create mode 100644 Lantean.QBitTorrentClient/Models/RssItem.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..75e7a15 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# IDE0290: Use primary constructor +csharp_style_prefer_primary_constructors = false diff --git a/Lantean.QBTBlazor.sln b/Lantean.QBTBlazor.sln index b218bcb..28dc590 100644 --- a/Lantean.QBTBlazor.sln +++ b/Lantean.QBTBlazor.sln @@ -9,6 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBitTorrentClient", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMudBlade", "Lantean.QBTMudBlade\Lantean.QBTMudBlade.csproj", "{83BC76CC-D51B-42AF-A6EE-FA400C300098}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BF1A631-87D7-4039-A701-88C5E0234B63}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/Lantean.QBTMudBlade/Components/ApplicationActions.razor.cs b/Lantean.QBTMudBlade/Components/ApplicationActions.razor.cs index 3f2ab2b..8771a86 100644 --- a/Lantean.QBTMudBlade/Components/ApplicationActions.razor.cs +++ b/Lantean.QBTMudBlade/Components/ApplicationActions.razor.cs @@ -4,9 +4,6 @@ using Microsoft.AspNetCore.Components; using MudBlazor; using Lantean.QBTMudBlade.Helpers; using Lantean.QBTMudBlade.Models; -using System; -using Lantean.QBTMudBlade.Pages; -using static MudBlazor.CategoryTypes; namespace Lantean.QBTMudBlade.Components { @@ -26,20 +23,39 @@ namespace Lantean.QBTMudBlade.Components [Parameter] public bool IsMenu { get; set; } - protected IEnumerable Actions => _actions ?? []; + [Parameter] + [EditorRequired] + public Preferences? Preferences { get; set; } + + protected IEnumerable Actions => GetActions(); + + private IEnumerable GetActions() + { + if (_actions is not null) + { + foreach (var action in _actions) + { + if (action.Name != "rss" || Preferences is not null && Preferences.RssProcessingEnabled) + { + yield return action; + } + } + } + } protected override void OnInitialized() { _actions = [ - new("Statistics", "Statistics", Icons.Material.Filled.PieChart, Color.Default, "/statistics"), - new("Search", "Search", Icons.Material.Filled.Search, Color.Default, "/search"), - new("RSS", "RSS", Icons.Material.Filled.RssFeed, Color.Default, "/rss"), - new("Execution Log", "Execution Log", Icons.Material.Filled.List, Color.Default, "/log"), - new("Blocked IPs", "Blocked IPs", Icons.Material.Filled.DisabledByDefault, Color.Default, "/blocks"), - new("Tag Management", "Tag Management", Icons.Material.Filled.Label, Color.Default, "/tags", separatorBefore: true), - new("Category Management", "Category Management", Icons.Material.Filled.List, Color.Default, "/categories"), - new("Settings", "Settings", Icons.Material.Filled.Settings, Color.Default, "/settings", separatorBefore: true), + new("statistics", "Statistics", Icons.Material.Filled.PieChart, Color.Default, "/statistics"), + new("search", "Search", Icons.Material.Filled.Search, Color.Default, "/search"), + new("rss", "RSS", Icons.Material.Filled.RssFeed, Color.Default, "/rss"), + new("log", "Execution Log", Icons.Material.Filled.List, Color.Default, "/log"), + new("blocks", "Blocked IPs", Icons.Material.Filled.DisabledByDefault, Color.Default, "/blocks"), + new("tags", "Tag Management", Icons.Material.Filled.Label, Color.Default, "/tags", separatorBefore: true), + new("categories", "Category Management", Icons.Material.Filled.List, Color.Default, "/categories"), + new("settings", "Settings", Icons.Material.Filled.Settings, Color.Default, "/settings", separatorBefore: true), + new("about", "About", Icons.Material.Filled.Info, Color.Default, "/about"), ]; } @@ -66,7 +82,7 @@ namespace Lantean.QBTMudBlade.Components { await ApiClient.Logout(); - NavigationManager.NavigateTo("/login", true); + NavigationManager.NavigateTo("/", true); }); } diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor.cs index f33f6af..ec5dc4e 100644 --- a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor.cs +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor.cs @@ -19,12 +19,23 @@ namespace Lantean.QBTMudBlade.Components.Dialogs [CascadingParameter] public MudDialogInstance MudDialog { get; set; } = default!; + [Parameter] + public string? Url { get; set; } + protected MudTextField? UrlsTextField { get; set; } protected string? Urls { get; set; } protected AddTorrentOptions TorrentOptions { get; set; } = default!; + protected override void OnInitialized() + { + if (Url is not null) + { + Urls = Url; + } + } + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) diff --git a/Lantean.QBTMudBlade/Components/Dialogs/ShareRatioDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/ShareRatioDialog.razor index e87f43d..bd1a6ad 100644 --- a/Lantean.QBTMudBlade/Components/Dialogs/ShareRatioDialog.razor +++ b/Lantean.QBTMudBlade/Components/Dialogs/ShareRatioDialog.razor @@ -20,7 +20,7 @@ - + diff --git a/Lantean.QBTMudBlade/Components/EnhancedErrorBoundary.cs b/Lantean.QBTMudBlade/Components/EnhancedErrorBoundary.cs index 2a8b35d..6fcd8bb 100644 --- a/Lantean.QBTMudBlade/Components/EnhancedErrorBoundary.cs +++ b/Lantean.QBTMudBlade/Components/EnhancedErrorBoundary.cs @@ -22,7 +22,7 @@ namespace Lantean.QBTMudBlade.Components protected override Task OnErrorAsync(Exception exception) { - Logger.LogError(exception, exception.Message); + Logger.LogError(exception, "An application error occurred: {message}.", exception.Message); _exceptions.Add(exception); if (Disabled) diff --git a/Lantean.QBTMudBlade/Components/Menu.razor b/Lantean.QBTMudBlade/Components/Menu.razor index f0a7343..0d8485b 100644 --- a/Lantean.QBTMudBlade/Components/Menu.razor +++ b/Lantean.QBTMudBlade/Components/Menu.razor @@ -1,3 +1,6 @@ - - - \ No newline at end of file +@if (_isVisible) +{ + + + +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Menu.razor.cs b/Lantean.QBTMudBlade/Components/Menu.razor.cs index 697d3f0..4685bd3 100644 --- a/Lantean.QBTMudBlade/Components/Menu.razor.cs +++ b/Lantean.QBTMudBlade/Components/Menu.razor.cs @@ -8,6 +8,10 @@ namespace Lantean.QBTMudBlade.Components { public partial class Menu { + private bool _isVisible = false; + + private Preferences? _preferences; + [Inject] protected NavigationManager NavigationManager { get; set; } = default!; @@ -17,6 +21,16 @@ namespace Lantean.QBTMudBlade.Components [Inject] protected IApiClient ApiClient { get; set; } = default!; + protected Preferences? Preferences => _preferences; + + public void ShowMenu(Preferences? preferences = null) + { + _isVisible = true; + _preferences = preferences; + + StateHasChanged(); + } + protected async Task ResetWebUI() { var preferences = new UpdatePreferences diff --git a/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs b/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs index b1467cd..2a262fc 100644 --- a/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs +++ b/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs @@ -90,21 +90,21 @@ namespace Lantean.QBTMudBlade.Components new("firstLastPiecePrio", "Download first and last pieces first", Icons.Material.Filled.Check, Color.Info, CreateCallback(DownloadFirstLast)), new("forceRecheck", "Force recheck", Icons.Material.Filled.Loop, Color.Info, CreateCallback(ForceRecheck), separatorBefore: true), new("forceReannounce", "Force reannounce", Icons.Material.Filled.BroadcastOnHome, Color.Info, CreateCallback(ForceReannounce)), - new("queue", "Queue", Icons.Material.Filled.Queue, Color.Transparent, new List - { + new("queue", "Queue", Icons.Material.Filled.Queue, Color.Transparent, + [ new("queueTop", "Move to top", Icons.Material.Filled.VerticalAlignTop, Color.Inherit, CreateCallback(MoveToTop)), new("queueUp", "Move up", Icons.Material.Filled.ArrowUpward, Color.Inherit, CreateCallback(MoveUp)), new("queueDown", "Move down", Icons.Material.Filled.ArrowDownward, Color.Inherit, CreateCallback(MoveDown)), new("queueBottom", "Move to bottom", Icons.Material.Filled.VerticalAlignBottom, Color.Inherit, CreateCallback(MoveToBottom)), - }, separatorBefore: true), - new("copy", "Copy", Icons.Material.Filled.FolderCopy, Color.Info, new List - { + ], separatorBefore: true), + new("copy", "Copy", Icons.Material.Filled.FolderCopy, Color.Info, + [ new("copyName", "Name", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Name))), new("copyHashv1", "Info hash v1", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV1))), 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("export", "Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)), ]; } diff --git a/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs b/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs index 5d6d823..fb3e323 100644 --- a/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs @@ -3,6 +3,7 @@ using Lantean.QBitTorrentClient.Models; using Lantean.QBTMudBlade.Components.UI; using Lantean.QBTMudBlade.Helpers; using Lantean.QBTMudBlade.Interop; +using Lantean.QBTMudBlade.Models; using Lantean.QBTMudBlade.Services; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; diff --git a/Lantean.QBTMudBlade/Components/UI/DynamicTable.razor.cs b/Lantean.QBTMudBlade/Components/UI/DynamicTable.razor.cs index 1d08e05..be40251 100644 --- a/Lantean.QBTMudBlade/Components/UI/DynamicTable.razor.cs +++ b/Lantean.QBTMudBlade/Components/UI/DynamicTable.razor.cs @@ -1,5 +1,6 @@ using Blazored.LocalStorage; using Lantean.QBTMudBlade.Helpers; +using Lantean.QBTMudBlade.Models; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using MudBlazor; diff --git a/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs b/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs index c704519..a6ab187 100644 --- a/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs @@ -1,5 +1,6 @@ using Lantean.QBitTorrentClient; using Lantean.QBitTorrentClient.Models; +using Lantean.QBTMudBlade.Models; using Lantean.QBTMudBlade.Services; using Microsoft.AspNetCore.Components; using System.Net; diff --git a/Lantean.QBTMudBlade/GlobalSuppressions.cs b/Lantean.QBTMudBlade/GlobalSuppressions.cs index d04d8b3..d7e85b9 100644 --- a/Lantean.QBTMudBlade/GlobalSuppressions.cs +++ b/Lantean.QBTMudBlade/GlobalSuppressions.cs @@ -5,4 +5,3 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:qtmud.Models.Torrent.#ctor(System.String,System.DateTimeOffset,System.Int64,System.Boolean,System.Single,System.String,System.Int64,System.DateTimeOffset,System.String,System.Int64,System.Int64,System.Int64,System.Int64,System.Int64,System.Boolean,System.Boolean,System.String,System.String,System.DateTimeOffset,System.String,System.Single,System.Int32,System.String,System.Int32,System.Int32,System.Int32,System.Int32,System.Single,System.Single,System.Single,System.String,System.DateTimeOffset,System.Int32,System.DateTimeOffset,System.Boolean,System.Int64,System.String,System.Boolean,System.Collections.Generic.IEnumerable{System.String},System.DateTimeOffset,System.Int64,System.String,System.Int64,System.Int64,System.Int64,System.Int64)")] \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Helpers/DialogHelper.cs b/Lantean.QBTMudBlade/Helpers/DialogHelper.cs index 7d665f0..c97da97 100644 --- a/Lantean.QBTMudBlade/Helpers/DialogHelper.cs +++ b/Lantean.QBTMudBlade/Helpers/DialogHelper.cs @@ -64,9 +64,14 @@ namespace Lantean.QBTMudBlade.Helpers } } - public static async Task InvokeAddTorrentLinkDialog(this IDialogService dialogService, IApiClient apiClient) + public static async Task InvokeAddTorrentLinkDialog(this IDialogService dialogService, IApiClient apiClient, string? url = null) { - var result = await dialogService.ShowAsync("Download from URLs", FormDialogOptions); + var parameters = new DialogParameters + { + { nameof(AddTorrentLinkDialog.Url), url } + }; + + var result = await dialogService.ShowAsync("Download from URLs", parameters, FormDialogOptions); var dialogResult = await result.Result; if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null) { diff --git a/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj b/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj index 97853f8..be62caa 100644 --- a/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj +++ b/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj @@ -11,11 +11,13 @@ - - + + - + + + diff --git a/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs index 91246e4..831cb83 100644 --- a/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs +++ b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs @@ -1,4 +1,5 @@ using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Components; using Lantean.QBTMudBlade.Helpers; using Lantean.QBTMudBlade.Models; using Lantean.QBTMudBlade.Services; @@ -28,6 +29,9 @@ namespace Lantean.QBTMudBlade.Layout [CascadingParameter(Name = "DrawerOpen")] public bool DrawerOpen { get; set; } + [CascadingParameter] + public Menu? Menu { get; set; } + protected MainData? MainData { get; set; } protected string Category { get; set; } = FilterHelper.CATEGORY_ALL; @@ -85,6 +89,8 @@ namespace Lantean.QBTMudBlade.Layout _refreshInterval = MainData.ServerState.RefreshInterval; IsAuthenticated = true; + + Menu?.ShowMenu(Preferences); } protected override async Task OnAfterRenderAsync(bool firstRender) diff --git a/Lantean.QBTMudBlade/Layout/MainLayout.razor b/Lantean.QBTMudBlade/Layout/MainLayout.razor index f87d20b..bd539e4 100644 --- a/Lantean.QBTMudBlade/Layout/MainLayout.razor +++ b/Lantean.QBTMudBlade/Layout/MainLayout.razor @@ -21,17 +21,16 @@ } - @if (ShowMenu) - { - - } + - @Body + + @Body + diff --git a/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs b/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs index 0cc1853..dd48b56 100644 --- a/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs +++ b/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs @@ -30,8 +30,6 @@ namespace Lantean.QBTMudBlade.Layout protected bool ErrorDrawerOpen { get; set; } = false; - protected bool ShowMenu { get; set; } = false; - public Guid Id => Guid.NewGuid(); protected EnhancedErrorBoundary? ErrorBoundary { get; set; } @@ -40,6 +38,8 @@ namespace Lantean.QBTMudBlade.Layout protected MudThemeProvider MudThemeProvider { get; set; } = default!; + private Menu Menu { get; set; } = default!; + ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new() { ReportRate = 50, @@ -62,11 +62,6 @@ namespace Lantean.QBTMudBlade.Layout protected override async Task OnParametersSetAsync() { - if (!ShowMenu) - { - ShowMenu = await ApiClient.CheckAuthState(); - } - var drawerOpen = await LocalStorage.GetItemAsync(_drawerOpenStorageKey); if (drawerOpen is not null) { diff --git a/Lantean.QBTMudBlade/Layout/OtherLayout.razor b/Lantean.QBTMudBlade/Layout/OtherLayout.razor index 74122d9..55c6356 100644 --- a/Lantean.QBTMudBlade/Layout/OtherLayout.razor +++ b/Lantean.QBTMudBlade/Layout/OtherLayout.razor @@ -3,7 +3,7 @@ - + diff --git a/Lantean.QBTMudBlade/Layout/OtherLayout.razor.cs b/Lantean.QBTMudBlade/Layout/OtherLayout.razor.cs index 249ef40..3536d60 100644 --- a/Lantean.QBTMudBlade/Layout/OtherLayout.razor.cs +++ b/Lantean.QBTMudBlade/Layout/OtherLayout.razor.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Components; +using Lantean.QBitTorrentClient.Models; +using Microsoft.AspNetCore.Components; namespace Lantean.QBTMudBlade.Layout { @@ -6,5 +7,8 @@ namespace Lantean.QBTMudBlade.Layout { [CascadingParameter(Name = "DrawerOpen")] public bool DrawerOpen { get; set; } + + [CascadingParameter] + public Preferences? Preferences { get; set; } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/TableHelper.cs b/Lantean.QBTMudBlade/Models/ColumnDefinition.cs similarity index 72% rename from Lantean.QBTMudBlade/TableHelper.cs rename to Lantean.QBTMudBlade/Models/ColumnDefinition.cs index 48e8cd7..057a432 100644 --- a/Lantean.QBTMudBlade/TableHelper.cs +++ b/Lantean.QBTMudBlade/Models/ColumnDefinition.cs @@ -1,16 +1,8 @@ using Microsoft.AspNetCore.Components; using MudBlazor; -namespace Lantean.QBTMudBlade +namespace Lantean.QBTMudBlade.Models { - public static class TableHelper - { - public static void CreateColumn(string name) - { - // do it - } - } - public class ColumnDefinition { public ColumnDefinition(string header, Func sortSelector, Func? formatter = null, string? tdClass = null, int? width = null) @@ -61,25 +53,4 @@ namespace Lantean.QBTMudBlade return new RowContext(Header, data, Formatter is null ? SortSelector : Formatter); } } - - public record RowContext - { - private readonly Func _valueGetter; - - public RowContext(string headerText, T data, Func valueGetter) - { - HeaderText = headerText; - Data = data; - _valueGetter = valueGetter; - } - - public string HeaderText { get; } - - public T Data { get; set; } - - public object? GetValue() - { - return _valueGetter(Data); - } - } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/FilterState.cs b/Lantean.QBTMudBlade/Models/FilterState.cs index fc2c8c1..5d7c2f8 100644 --- a/Lantean.QBTMudBlade/Models/FilterState.cs +++ b/Lantean.QBTMudBlade/Models/FilterState.cs @@ -1,6 +1,6 @@ namespace Lantean.QBTMudBlade.Models { - public struct FilterState + public readonly struct FilterState { public FilterState(string category, Status status, string tag, string tracker, bool useSubcategories, string? terms) { diff --git a/Lantean.QBTMudBlade/Models/LogForm.cs b/Lantean.QBTMudBlade/Models/LogForm.cs index 3d1adaa..5ed786f 100644 --- a/Lantean.QBTMudBlade/Models/LogForm.cs +++ b/Lantean.QBTMudBlade/Models/LogForm.cs @@ -9,7 +9,9 @@ public int? LastKnownId { get; set; } +#pragma warning disable IDE0028 // Simplify collection initialization - the SelectedValues of MudSelect has issues with the type being HashSet but it needs to be. public IEnumerable SelectedTypes { get; set; } = new HashSet(); +#pragma warning restore IDE0028 // Simplify collection initialization public string? Criteria { get; set; } } diff --git a/Lantean.QBTMudBlade/Models/RowContext.cs b/Lantean.QBTMudBlade/Models/RowContext.cs new file mode 100644 index 0000000..07ad6d1 --- /dev/null +++ b/Lantean.QBTMudBlade/Models/RowContext.cs @@ -0,0 +1,24 @@ +namespace Lantean.QBTMudBlade.Models +{ + + public record RowContext + { + private readonly Func _valueGetter; + + public RowContext(string headerText, T data, Func valueGetter) + { + HeaderText = headerText; + Data = data; + _valueGetter = valueGetter; + } + + public string HeaderText { get; } + + public T Data { get; set; } + + public object? GetValue() + { + return _valueGetter(Data); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Login.razor.cs b/Lantean.QBTMudBlade/Pages/Login.razor.cs index 93bed47..54ea2b8 100644 --- a/Lantean.QBTMudBlade/Pages/Login.razor.cs +++ b/Lantean.QBTMudBlade/Pages/Login.razor.cs @@ -48,7 +48,7 @@ namespace Lantean.QBTMudBlade.Pages #if DEBUG protected override Task OnInitializedAsync() { - return DoLogin("admin", "V9VpmhCvv"); + return DoLogin("admin", "dhT67PMAe"); } #endif } diff --git a/Lantean.QBTMudBlade/Pages/Options.razor.cs b/Lantean.QBTMudBlade/Pages/Options.razor.cs index f9bc6b2..5a1b93e 100644 --- a/Lantean.QBTMudBlade/Pages/Options.razor.cs +++ b/Lantean.QBTMudBlade/Pages/Options.razor.cs @@ -81,21 +81,8 @@ namespace Lantean.QBTMudBlade.Pages } } - protected async Task NavigateBack() + protected void NavigateBack() { - //if (UpdatePreferences is null) - //{ - // NavigationManager.NavigateTo("/"); - // return; - //} - - //await DialogService.ShowConfirmDialog( - // "Unsaved Changed", - // "Are you sure you want to leave without saving your changes?", - // () => NavigationManager.NavigateTo("/")); - - await Task.CompletedTask; - NavigationManager.NavigateTo("/"); } diff --git a/Lantean.QBTMudBlade/Pages/Rss.razor b/Lantean.QBTMudBlade/Pages/Rss.razor index e0abaa8..965584c 100644 --- a/Lantean.QBTMudBlade/Pages/Rss.razor +++ b/Lantean.QBTMudBlade/Pages/Rss.razor @@ -7,10 +7,66 @@ } + + + + + RSS - -

Coming soon.

-
+ + + + + @foreach (var (key, feed) in Items) + { + + } + + + + @if (SelectedFeed is not null && SelectedRssItem?.Articles is not null) + { + + @foreach (var article in SelectedRssItem.Articles) + { + + } + + } + else + { + + } + + + @if (SelectedFeed is not null && SelectedArticle is not null && SelectedRssArticle is not null) + { + + + + @SelectedRssArticle.Title + + + + Download + Open torrent URL + + + + + + @SelectedRssArticle.Date + @SelectedRssArticle.Description + + + } + else + { + + } + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Rss.razor.cs b/Lantean.QBTMudBlade/Pages/Rss.razor.cs index f7d98ef..d8e9b0b 100644 --- a/Lantean.QBTMudBlade/Pages/Rss.razor.cs +++ b/Lantean.QBTMudBlade/Pages/Rss.razor.cs @@ -1,4 +1,5 @@ using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Helpers; using Lantean.QBTMudBlade.Models; using Microsoft.AspNetCore.Components; using MudBlazor; @@ -31,9 +32,96 @@ namespace Lantean.QBTMudBlade.Pages protected ServerState? ServerState => MainData?.ServerState; + protected string? SelectedFeed { get; set; } + + protected string? SelectedArticle { get; set; } + + public IReadOnlyDictionary Items { get; private set; } = new Dictionary(); + + protected IReadOnlyList ColumnsSizes => GetColumnSizes(); + + private IReadOnlyList GetColumnSizes() + { + if (SelectedFeed is null) + { + return [12, 0, 0]; + } + + if (SelectedFeed is not null && SelectedArticle is null) + { + return [6, 6, 0]; + } + + return [4, 4, 4]; + } + + protected QBitTorrentClient.Models.RssItem? SelectedRssItem + { + get + { + if (SelectedFeed == null) + { + return null; + } + + Items.TryGetValue(SelectedFeed, out var feed); + + return feed; + } + } + + protected QBitTorrentClient.Models.RssArticle? SelectedRssArticle + { + get + { + return SelectedRssItem?.Articles?.FirstOrDefault(a => a.Id == SelectedArticle); + } + } + + protected void SelectedFeedChanged(string value) + { + SelectedFeed = value; + SelectedArticle = null; + } + + protected void SelectedArticleChanged(string value) + { + SelectedArticle = value; + } + + protected override async Task OnInitializedAsync() + { + Items = await ApiClient.GetAllRssItems(true); + } + + protected async Task DownloadItem(string? url) + { + await DialogService.InvokeAddTorrentLinkDialog(ApiClient, url); + } + protected void NavigateBack() { NavigationManager.NavigateTo("/"); } + + protected async Task NewSubscription() + { + await Task.CompletedTask; + } + + protected async Task MarkAsRead() + { + await Task.CompletedTask; + } + + protected async Task UpdateAll() + { + await Task.CompletedTask; + } + + protected async Task EditDownloadRules() + { + await Task.CompletedTask; + } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Search.razor.cs b/Lantean.QBTMudBlade/Pages/Search.razor.cs index bdf7281..e45a682 100644 --- a/Lantean.QBTMudBlade/Pages/Search.razor.cs +++ b/Lantean.QBTMudBlade/Pages/Search.razor.cs @@ -14,7 +14,7 @@ namespace Lantean.QBTMudBlade.Pages private int? _searchId; private bool _disposedValue; private readonly CancellationTokenSource _timerCancellationToken = new(); - private int _refreshInterval = 1500; + private readonly int _refreshInterval = 1500; private QBitTorrentClient.Models.SearchResults? _searchResults; diff --git a/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs b/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs index 4e98f49..d0af170 100644 --- a/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs +++ b/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs @@ -14,8 +14,8 @@ namespace Lantean.QBTMudBlade.Pages { private bool _disposedValue; - private static KeyboardEvent _addTorrentFileKey = new KeyboardEvent("a") { AltKey = true }; - private static KeyboardEvent _addTorrentLinkKey = new KeyboardEvent("l") { AltKey = true }; + private static readonly KeyboardEvent _addTorrentFileKey = new("a") { AltKey = true }; + private static readonly KeyboardEvent _addTorrentLinkKey = new("l") { AltKey = true }; [Inject] diff --git a/Lantean.QBTMudBlade/Program.cs b/Lantean.QBTMudBlade/Program.cs index a6fc983..9300caf 100644 --- a/Lantean.QBTMudBlade/Program.cs +++ b/Lantean.QBTMudBlade/Program.cs @@ -20,7 +20,9 @@ namespace Lantean.QBTMudBlade Uri baseAddress; #if DEBUG +#pragma warning disable S1075 // URIs should not be hardcoded - used for debugging only baseAddress = new Uri("http://localhost:8080"); +#pragma warning restore S1075 // URIs should not be hardcoded #else baseAddress = new Uri(builder.HostEnvironment.BaseAddress); #endif diff --git a/Lantean.QBTMudBlade/Services/DataManager.cs b/Lantean.QBTMudBlade/Services/DataManager.cs index 473f15b..c8ac3e4 100644 --- a/Lantean.QBTMudBlade/Services/DataManager.cs +++ b/Lantean.QBTMudBlade/Services/DataManager.cs @@ -69,9 +69,11 @@ namespace Lantean.QBTMudBlade.Services var serverState = CreateServerState(mainData.ServerState); - var tagState = new Dictionary>(tags.Count + 2); - tagState.Add(FilterHelper.TAG_ALL, torrents.Keys.ToHashSet()); - tagState.Add(FilterHelper.TAG_UNTAGGED, torrents.Values.Where(t => FilterHelper.FilterTag(t, FilterHelper.TAG_UNTAGGED)).ToHashesHashSet()); + var tagState = new Dictionary>(tags.Count + 2) + { + { FilterHelper.TAG_ALL, torrents.Keys.ToHashSet() }, + { FilterHelper.TAG_UNTAGGED, torrents.Values.Where(t => FilterHelper.FilterTag(t, FilterHelper.TAG_UNTAGGED)).ToHashesHashSet() } + }; foreach (var tag in tags) { tagState.Add(tag, torrents.Values.Where(t => FilterHelper.FilterTag(t, tag)).ToHashesHashSet()); diff --git a/Lantean.QBTMudBlade/Services/HttpLogger.cs b/Lantean.QBTMudBlade/Services/HttpLogger.cs index 545fcbd..4723cc0 100644 --- a/Lantean.QBTMudBlade/Services/HttpLogger.cs +++ b/Lantean.QBTMudBlade/Services/HttpLogger.cs @@ -13,22 +13,27 @@ namespace Lantean.QBTMudBlade.Services public object? LogRequestStart(HttpRequestMessage request) { - //_logger.LogInformation( - // "Sending '{Request.Method}' to '{Request.Host}{Request.Path}'", - // request.Method, - // request.RequestUri?.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped), - // request.RequestUri!.PathAndQuery); +#if DEBUG + _logger.LogInformation( + "Sending '{Request.Method}' to '{Request.Host}{Request.Path}'", + request.Method, + request.RequestUri?.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped), + request.RequestUri!.PathAndQuery); +#endif return null; + } public void LogRequestStop( object? context, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed) { - //_logger.LogInformation( - // "Received '{Response.StatusCodeInt} {Response.StatusCodeString}' after {Response.ElapsedMilliseconds}ms", - // (int)response.StatusCode, - // response.StatusCode, - // elapsed.TotalMilliseconds.ToString("F1")); +#if DEBUG + _logger.LogInformation( + "Received '{Response.StatusCodeInt} {Response.StatusCodeString}' after {Response.ElapsedMilliseconds}ms", + (int)response.StatusCode, + response.StatusCode, + elapsed.TotalMilliseconds.ToString("F1")); +#endif } public void LogRequestFailed( diff --git a/Lantean.QBTMudBlade/wwwroot/css/app.css b/Lantean.QBTMudBlade/wwwroot/css/app.css index 435c9ac..5194928 100644 --- a/Lantean.QBTMudBlade/wwwroot/css/app.css +++ b/Lantean.QBTMudBlade/wwwroot/css/app.css @@ -218,4 +218,8 @@ td.icon-cell { td .folder-button { padding: 6px 16px 6px 16px !important; -} \ No newline at end of file +} + +.rss-contents { + height: calc(100vh - 149px); +} diff --git a/Lantean.QBitTorrentClient/ApiClient.cs b/Lantean.QBitTorrentClient/ApiClient.cs index 56dea8c..c15d9fb 100644 --- a/Lantean.QBitTorrentClient/ApiClient.cs +++ b/Lantean.QBitTorrentClient/ApiClient.cs @@ -942,14 +942,146 @@ namespace Lantean.QBitTorrentClient #region RSS - // not implementing RSS right now + public async Task AddRssFolder(string path) + { + var content = new FormUrlEncodedBuilder() + .Add("path", path) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("rss/addFolder", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task AddRssFeed(string url, string? path = null) + { + var content = new FormUrlEncodedBuilder() + .Add("url", url) + .AddIfNotNullOrEmpty("path", path) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("rss/addFeed", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task RemoveRssItem(string path) + { + var content = new FormUrlEncodedBuilder() + .Add("path", path) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("rss/removeItem", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task MoveRssItem(string itemPath, string destPath) + { + var content = new FormUrlEncodedBuilder() + .Add("itemPath", itemPath) + .Add("destPath", destPath) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("rss/moveItem", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task> GetAllRssItems(bool? withData = null) + { + var content = new QueryBuilder() + .AddIfNotNullOrEmpty("withData", withData); + + var response = await _httpClient.GetAsync("rss/items", content); + + response.EnsureSuccessStatusCode(); + + return await GetJsonDictionary(response.Content); + } + + public async Task MarkRssItemAsRead(string itemPath, string? articleId = null) + { + var content = new FormUrlEncodedBuilder() + .Add("itemPath", itemPath) + .AddIfNotNullOrEmpty("articleId", articleId) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("rss/markAsRead", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task RefreshRssItem(string itemPath) + { + var content = new FormUrlEncodedBuilder() + .Add("itemPath", itemPath) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("rss/refresh", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task SetRssAutoDownloadingRule(string ruleName, AutoDownloadingRule ruleDef) + { + var content = new FormUrlEncodedBuilder() + .Add("ruleName", ruleName) + .Add("ruleName", JsonSerializer.Serialize(ruleDef)) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("rss/setRule", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task RenameRssAutoDownloadingRule(string ruleName, string newRuleName) + { + var content = new FormUrlEncodedBuilder() + .Add("ruleName", ruleName) + .Add("newRuleName", newRuleName) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("rss/renameRule", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task RemoveRssAutoDownloadingRule(string ruleName) + { + var content = new FormUrlEncodedBuilder() + .Add("ruleName", ruleName) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("rss/removeRule", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task> GetAllRssAutoDownloadingRules() + { + var response = await _httpClient.GetAsync("rss/rules"); + + response.EnsureSuccessStatusCode(); + + return await GetJsonDictionary(response.Content); + } + + public async Task>> GetRssMatchingArticles(string ruleName) + { + var response = await _httpClient.GetAsync("rss/matchingArticles"); + + response.EnsureSuccessStatusCode(); + + var dictionary = await GetJsonDictionary>(response.Content); + + return ((IDictionary>)dictionary.ToDictionary(d => d.Key, d => d.Value.ToList().AsReadOnly())).AsReadOnly(); + } #endregion RSS #region Search - - public async Task StartSearch(string pattern, IEnumerable plugins, string category = "all") { var content = new FormUrlEncodedBuilder() diff --git a/Lantean.QBitTorrentClient/FormUrlEncodedBuilder.cs b/Lantean.QBitTorrentClient/FormUrlEncodedBuilder.cs index 5097bc9..5b9a019 100644 --- a/Lantean.QBitTorrentClient/FormUrlEncodedBuilder.cs +++ b/Lantean.QBitTorrentClient/FormUrlEncodedBuilder.cs @@ -20,7 +20,7 @@ return this; } - public FormUrlEncodedBuilder AddIfNotNullOrEmpty(string key, string value) + public FormUrlEncodedBuilder AddIfNotNullOrEmpty(string key, string? value) { if (!string.IsNullOrEmpty(value)) { @@ -30,6 +30,16 @@ return this; } + public FormUrlEncodedBuilder AddIfNotNullOrEmpty(string key, T? value) where T : struct + { + if (value.HasValue) + { + _parameters.Add(new KeyValuePair(key, value.ToString()!)); + } + + return this; + } + public FormUrlEncodedContent ToFormUrlEncodedContent() { return new FormUrlEncodedContent(_parameters); diff --git a/Lantean.QBitTorrentClient/IApiClient.cs b/Lantean.QBitTorrentClient/IApiClient.cs index 978d339..f748484 100644 --- a/Lantean.QBitTorrentClient/IApiClient.cs +++ b/Lantean.QBitTorrentClient/IApiClient.cs @@ -172,7 +172,29 @@ namespace Lantean.QBitTorrentClient #region RSS - // not implementing RSS right now + Task AddRssFolder(string path); + + Task AddRssFeed(string url, string? path = null); + + Task RemoveRssItem(string path); + + Task MoveRssItem(string itemPath, string destPath); + + Task> GetAllRssItems(bool? withData = null); + + Task MarkRssItemAsRead(string itemPath, string? articleId = null); + + Task RefreshRssItem(string itemPath); + + Task SetRssAutoDownloadingRule(string ruleName, AutoDownloadingRule ruleDef); + + Task RenameRssAutoDownloadingRule(string ruleName, string newRuleName); + + Task RemoveRssAutoDownloadingRule(string ruleName); + + Task> GetAllRssAutoDownloadingRules(); + + Task>> GetRssMatchingArticles(string ruleName); #endregion RSS diff --git a/Lantean.QBitTorrentClient/Models/AutoDownloadingRule.cs b/Lantean.QBitTorrentClient/Models/AutoDownloadingRule.cs new file mode 100644 index 0000000..f417019 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/AutoDownloadingRule.cs @@ -0,0 +1,77 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record AutoDownloadingRule + { + [JsonConstructor] + public AutoDownloadingRule( + bool enabled, + string mustContain, + string mustNotContain, + bool useRegex, + string episodeFilter, + bool smartFilter, + IEnumerable previouslyMatchedEpisodes, + IEnumerable affectedFeeds, + int ignoreDays, + string lastMatch, + bool addPaused, + string assignedCategory, + string savePath) + { + Enabled = enabled; + MustContain = mustContain; + MustNotContain = mustNotContain; + UseRegex = useRegex; + EpisodeFilter = episodeFilter; + SmartFilter = smartFilter; + PreviouslyMatchedEpisodes = previouslyMatchedEpisodes; + AffectedFeeds = affectedFeeds; + IgnoreDays = ignoreDays; + LastMatch = lastMatch; + AddPaused = addPaused; + AssignedCategory = assignedCategory; + SavePath = savePath; + } + + [JsonPropertyName("enabled")] + public bool Enabled { get; } + + [JsonPropertyName("mustContain")] + public string MustContain { get; } + + [JsonPropertyName("mustNotContain")] + public string MustNotContain { get; } + + [JsonPropertyName("useRegex")] + public bool UseRegex { get; } + + [JsonPropertyName("episodeFilter")] + public string EpisodeFilter { get; } + + [JsonPropertyName("smartFilter")] + public bool SmartFilter { get; } + + [JsonPropertyName("previouslyMatchedEpisodes")] + public IEnumerable PreviouslyMatchedEpisodes { get; } + + [JsonPropertyName("affectedFeeds")] + public IEnumerable AffectedFeeds { get; } + + [JsonPropertyName("ignoreDays")] + public int IgnoreDays { get; } + + [JsonPropertyName("lastMatch")] + public string LastMatch { get; } + + [JsonPropertyName("addPaused")] + public bool AddPaused { get; } + + [JsonPropertyName("assignedCategory")] + public string AssignedCategory { get; } + + [JsonPropertyName("savePath")] + public string SavePath { get; } + } +} diff --git a/Lantean.QBitTorrentClient/Models/RssArticle.cs b/Lantean.QBitTorrentClient/Models/RssArticle.cs new file mode 100644 index 0000000..d172ea1 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/RssArticle.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public class RssArticle + { + [JsonConstructor] + public RssArticle( + string? category, + string? comments, + string? date, + string? description, + string? id, + string? link, + string? thumbnail, + string? title, + string? torrentURL) + { + Category = category; + Comments = comments; + Date = date; + Description = description; + Id = id; + Link = link; + Thumbnail = thumbnail; + Title = title; + TorrentURL = torrentURL; + } + + [JsonPropertyName("category")] + public string? Category { get; set; } + + [JsonPropertyName("comments")] + public string? Comments { get; set; } + + [JsonPropertyName("date")] + public string? Date { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("link")] + public string? Link { get; set; } + + [JsonPropertyName("thumbnail")] + public string? Thumbnail { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("torrentURL")] + public string? TorrentURL { get; set; } + } +} diff --git a/Lantean.QBitTorrentClient/Models/RssItem.cs b/Lantean.QBitTorrentClient/Models/RssItem.cs new file mode 100644 index 0000000..6b07038 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/RssItem.cs @@ -0,0 +1,47 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record RssItem + { + [JsonConstructor] + public RssItem( + IReadOnlyList? articles, + bool hasError, + bool isLoading, + string? lastBuildDate, + string? title, + string uid, + string url) + { + Articles = articles; + HasError = hasError; + IsLoading = isLoading; + LastBuildDate = lastBuildDate; + Title = title; + Uid = uid; + Url = url; + } + + [JsonPropertyName("articles")] + public IReadOnlyList? Articles { get; } + + [JsonPropertyName("hasError")] + public bool HasError { get; } + + [JsonPropertyName("IsLoading")] + public bool IsLoading { get; } + + [JsonPropertyName("lastBuildDate")] + public string? LastBuildDate { get; } + + [JsonPropertyName("title")] + public string? Title { get; } + + [JsonPropertyName("uid")] + public string Uid { get; } + + [JsonPropertyName("url")] + public string Url { get; } + } +} diff --git a/Lantean.QBitTorrentClient/QueryBuilder.cs b/Lantean.QBitTorrentClient/QueryBuilder.cs index 09e0df4..aefc7ef 100644 --- a/Lantean.QBitTorrentClient/QueryBuilder.cs +++ b/Lantean.QBitTorrentClient/QueryBuilder.cs @@ -22,7 +22,7 @@ namespace Lantean.QBitTorrentClient return this; } - public QueryBuilder AddIfNotNullOrEmpty(string key, string value) + public QueryBuilder AddIfNotNullOrEmpty(string key, string? value) { if (!string.IsNullOrEmpty(value)) { @@ -32,6 +32,16 @@ namespace Lantean.QBitTorrentClient return this; } + public QueryBuilder AddIfNotNullOrEmpty(string key, T? value) where T : struct + { + if (value.HasValue) + { + _parameters.Add(new KeyValuePair(key, value.ToString()!)); + } + + return this; + } + public string ToQueryString() { if (_parameters.Count == 0)