From f9847c60f5fb040ce49ad7b97836b32da9f6f92c Mon Sep 17 00:00:00 2001 From: ahjephson Date: Mon, 22 Apr 2024 14:15:07 +0100 Subject: [PATCH] Add project files. --- .dcignore | 39 + .../ContentItemSizeComparer.cs | 3 + .../Lantean.QBTBlazor.Test.csproj | 27 + Lantean.QBTBlazor.Test/UnitTest1.cs | 67 ++ Lantean.QBTBlazor.sln | 37 + Lantean.QBTMudBlade/App.razor | 12 + .../Dialogs/AddCategoryDialog.razor | 16 + .../Dialogs/AddCategoryDialog.razor.cs | 31 + .../Dialogs/AddTorrentFileDialog.razor | 24 + .../Dialogs/AddTorrentFileDialog.razor.cs | 34 + .../Dialogs/AddTorrentLinkDialog.razor | 14 + .../Dialogs/AddTorrentLinkDialog.razor.cs | 33 + .../Dialogs/AddTorrentOptions.razor | 62 + .../Dialogs/AddTorrentOptions.razor.cs | 79 ++ .../Dialogs/ColumnOptionsDialog.razor | 20 + .../Dialogs/ColumnOptionsDialog.razor.cs | 51 + .../Components/Dialogs/ConfirmDialog.razor | 9 + .../Components/Dialogs/ConfirmDialog.razor.cs | 31 + .../Components/Dialogs/DeleteDialog.razor | 15 + .../Components/Dialogs/DeleteDialog.razor.cs | 24 + .../Dialogs/FilterOptionsDialog.razor | 62 + .../Dialogs/FilterOptionsDialog.razor.cs | 127 ++ .../Dialogs/SingleFieldDialog.razor | 15 + .../Dialogs/SingleFieldDialog.razor.cs | 28 + .../Dialogs/SliderFieldDialog.razor | 18 + .../Dialogs/SliderFieldDialog.razor.cs | 34 + .../Components/Dialogs/StatisticsDialog.razor | 5 + .../Dialogs/StatisticsDialog.razor.cs | 11 + .../Components/ExtendedTable.razor | 63 + .../Components/ExtendedTable.razor.cs | 153 +++ .../Components/FakeNavLink.razor | 10 + .../Components/FakeNavLink.razor.cs | 72 ++ Lantean.QBTMudBlade/Components/FilesTab.razor | 99 ++ .../Components/FilesTab.razor.cs | 561 +++++++++ .../Components/FiltersNav.razor | 27 + .../Components/FiltersNav.razor.cs | 84 ++ .../Components/GeneralTab.razor | 97 ++ .../Components/GeneralTab.razor.cs | 101 ++ Lantean.QBTMudBlade/Components/Menu.razor | 9 + Lantean.QBTMudBlade/Components/Menu.razor.cs | 58 + .../Components/NonRendering.razor | 1 + .../Components/NonRendering.razor.cs | 16 + .../Components/Options/AdvancedOptions.razor | 14 + .../Options/AdvancedOptions.razor.cs | 10 + .../Components/Options/BehaviourOptions.razor | 69 ++ .../Options/BehaviourOptions.razor.cs | 99 ++ .../Options/BitTorrentOptions.razor | 14 + .../Options/BitTorrentOptions.razor.cs | 10 + .../Options/ConnectionOptions.razor | 72 ++ .../Options/ConnectionOptions.razor.cs | 330 ++++++ .../Components/Options/DownloadsOptions.razor | 287 +++++ .../Options/DownloadsOptions.razor.cs | 399 +++++++ .../Components/Options/Options.cs | 38 + .../Components/Options/RSSOptions.razor | 14 + .../Components/Options/RSSOptions.razor.cs | 10 + .../Components/Options/SpeedOptions.razor | 14 + .../Components/Options/SpeedOptions.razor.cs | 10 + .../Components/Options/WebUIOptions.razor | 14 + .../Components/Options/WebUIOptions.razor.cs | 10 + Lantean.QBTMudBlade/Components/PeersTab.razor | 32 + .../Components/PeersTab.razor.cs | 119 ++ .../Components/TorrentActions.razor | 88 ++ .../Components/TorrentActions.razor.cs | 325 +++++ .../Components/TorrentsListNav.razor | 18 + .../Components/TorrentsListNav.razor.cs | 22 + .../Components/TrackersTab.razor | 27 + .../Components/TrackersTab.razor.cs | 97 ++ .../Components/WebSeedsTab.razor | 8 + .../Components/WebSeedsTab.razor.cs | 92 ++ Lantean.QBTMudBlade/CookieHandler.cs | 14 + Lantean.QBTMudBlade/CustomIcons.cs | 7 + Lantean.QBTMudBlade/DialogHelper.cs | 229 ++++ Lantean.QBTMudBlade/DisplayHelpers.cs | 379 ++++++ Lantean.QBTMudBlade/ExpressionModifier.cs | 154 +++ Lantean.QBTMudBlade/Extensions.cs | 54 + .../Filter/FilterExpressionGenerator.cs | 134 +++ Lantean.QBTMudBlade/Filter/FilterOperator.cs | 137 +++ .../Filter/PropertyFilterDefinition.cs | 32 + Lantean.QBTMudBlade/FilterHelper.cs | 282 +++++ Lantean.QBTMudBlade/FilterState.cs | 24 + Lantean.QBTMudBlade/GlobalSuppressions.cs | 8 + .../Interop/BoundingClientRect.cs | 21 + Lantean.QBTMudBlade/Interop/InteropHelper.cs | 12 + .../Lantean.QBTMudBlade.csproj | 23 + .../Layout/DetailsLayout.razor | 9 + .../Layout/DetailsLayout.razor.cs | 29 + Lantean.QBTMudBlade/Layout/ListLayout.razor | 11 + .../Layout/ListLayout.razor.cs | 26 + .../Layout/LoggedInLayout.razor | 57 + .../Layout/LoggedInLayout.razor.cs | 173 +++ Lantean.QBTMudBlade/Layout/MainLayout.razor | 22 + .../Layout/MainLayout.razor.cs | 89 ++ .../Models/AddTorrentFileOptions.cs | 14 + .../Models/AddTorrentLinkOptions.cs | 12 + Lantean.QBTMudBlade/Models/Category.cs | 14 + Lantean.QBTMudBlade/Models/ContentItem.cs | 62 + Lantean.QBTMudBlade/Models/ContentItemType.cs | 8 + .../Models/GlobalTransferInfo.cs | 46 + Lantean.QBTMudBlade/Models/MainData.cs | 40 + Lantean.QBTMudBlade/Models/Peer.cs | 72 ++ Lantean.QBTMudBlade/Models/PeerList.cs | 12 + Lantean.QBTMudBlade/Models/Priority.cs | 11 + Lantean.QBTMudBlade/Models/ServerState.cs | 89 ++ Lantean.QBTMudBlade/Models/Status.cs | 19 + Lantean.QBTMudBlade/Models/Torrent.cs | 232 ++++ Lantean.QBTMudBlade/Models/TorrentOptions.cs | 65 + Lantean.QBTMudBlade/Pages/Details.razor | 36 + Lantean.QBTMudBlade/Pages/Details.razor.cs | 93 ++ Lantean.QBTMudBlade/Pages/Login.razor | 38 + Lantean.QBTMudBlade/Pages/Login.razor.cs | 67 ++ Lantean.QBTMudBlade/Pages/Main.razor | 4 + Lantean.QBTMudBlade/Pages/Main.razor.cs | 154 +++ Lantean.QBTMudBlade/Pages/Options.razor | 55 + Lantean.QBTMudBlade/Pages/Options.razor.cs | 134 +++ Lantean.QBTMudBlade/Pages/TorrentList.razor | 104 ++ .../Pages/TorrentList.razor.cs | 288 +++++ Lantean.QBTMudBlade/Program.cs | 45 + .../Properties/launchSettings.json | 31 + .../Services/ClipboardService.cs | 19 + Lantean.QBTMudBlade/Services/DataManager.cs | 1049 +++++++++++++++++ .../Services/IClipboardService.cs | 7 + Lantean.QBTMudBlade/Services/IDataManager.cs | 23 + Lantean.QBTMudBlade/TableHelper.cs | 81 ++ Lantean.QBTMudBlade/_Imports.razor | 15 + Lantean.QBTMudBlade/wwwroot/css/app.css | 112 ++ Lantean.QBTMudBlade/wwwroot/favicon.png | Bin 0 -> 1148 bytes Lantean.QBTMudBlade/wwwroot/icon-192.png | Bin 0 -> 2626 bytes Lantean.QBTMudBlade/wwwroot/index.html | 35 + Lantean.QBitTorrentClient/ApiClient.cs | 951 +++++++++++++++ .../ApiClientExtensions.cs | 109 ++ .../Converters/CommaSeparatedJsonConverter.cs | 37 + .../Converters/SaveLocationJsonConverter.cs | 40 + .../Converters/StringFloatJsonConverter.cs | 38 + .../FormUrlEncodedBuilder.cs | 38 + .../FormUrlEncodedBuilderExtensions.cs | 52 + .../HttpClientExtensions.cs | 15 + Lantean.QBitTorrentClient/IApiClient.cs | 179 +++ .../Lantean.QBitTorrentClient.csproj | 9 + Lantean.QBitTorrentClient/Limits.cs | 9 + Lantean.QBitTorrentClient/MockApiClient.cs | 367 ++++++ Lantean.QBitTorrentClient/Models/BuildInfo.cs | 37 + Lantean.QBitTorrentClient/Models/Category.cs | 22 + Lantean.QBitTorrentClient/Models/FileData.cs | 52 + .../Models/GlobalTransferInfo.cs | 52 + Lantean.QBitTorrentClient/Models/Log.cs | 32 + Lantean.QBitTorrentClient/Models/LogType.cs | 10 + Lantean.QBitTorrentClient/Models/MainData.cs | 65 + Lantean.QBitTorrentClient/Models/Peer.cs | 92 ++ Lantean.QBitTorrentClient/Models/PeerId.cs | 14 + Lantean.QBitTorrentClient/Models/PeerLog.cs | 37 + .../Models/PieceState.cs | 9 + .../Models/Preferences.cs | 1023 ++++++++++++++++ Lantean.QBitTorrentClient/Models/Priority.cs | 10 + .../Models/SaveLocation.cs | 82 ++ .../Models/ServerState.cs | 105 ++ Lantean.QBitTorrentClient/Models/Torrent.cs | 249 ++++ .../Models/TorrentPeers.cs | 37 + .../Models/TorrentProperties.cs | 187 +++ .../Models/TorrentTrackers.cs | 52 + .../Models/TrackerStatus.cs | 11 + .../Models/UpdatePreferences.cs | 613 ++++++++++ Lantean.QBitTorrentClient/Models/WebSeed.cs | 16 + .../MultipartFormDataContentExtensions.cs | 35 + Lantean.QBitTorrentClient/QueryBuilder.cs | 67 ++ .../QueryBuilderExtensions.cs | 40 + .../SerializerOptions.cs | 19 + 166 files changed, 14345 insertions(+) create mode 100644 .dcignore create mode 100644 Lantean.QBTBlazor.Test/ContentItemSizeComparer.cs create mode 100644 Lantean.QBTBlazor.Test/Lantean.QBTBlazor.Test.csproj create mode 100644 Lantean.QBTBlazor.Test/UnitTest1.cs create mode 100644 Lantean.QBTBlazor.sln create mode 100644 Lantean.QBTMudBlade/App.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/AddCategoryDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/AddCategoryDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/AddTorrentFileDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/AddTorrentFileDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/ConfirmDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/ConfirmDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/DeleteDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/DeleteDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/FilterOptionsDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/FilterOptionsDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/SingleFieldDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/SingleFieldDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/SliderFieldDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/SliderFieldDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/ExtendedTable.razor create mode 100644 Lantean.QBTMudBlade/Components/ExtendedTable.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/FakeNavLink.razor create mode 100644 Lantean.QBTMudBlade/Components/FakeNavLink.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/FilesTab.razor create mode 100644 Lantean.QBTMudBlade/Components/FilesTab.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/FiltersNav.razor create mode 100644 Lantean.QBTMudBlade/Components/FiltersNav.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/GeneralTab.razor create mode 100644 Lantean.QBTMudBlade/Components/GeneralTab.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Menu.razor create mode 100644 Lantean.QBTMudBlade/Components/Menu.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/NonRendering.razor create mode 100644 Lantean.QBTMudBlade/Components/NonRendering.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor create mode 100644 Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Options/BehaviourOptions.razor create mode 100644 Lantean.QBTMudBlade/Components/Options/BehaviourOptions.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Options/BitTorrentOptions.razor create mode 100644 Lantean.QBTMudBlade/Components/Options/BitTorrentOptions.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor create mode 100644 Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor create mode 100644 Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Options/Options.cs create mode 100644 Lantean.QBTMudBlade/Components/Options/RSSOptions.razor create mode 100644 Lantean.QBTMudBlade/Components/Options/RSSOptions.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Options/SpeedOptions.razor create mode 100644 Lantean.QBTMudBlade/Components/Options/SpeedOptions.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor create mode 100644 Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/PeersTab.razor create mode 100644 Lantean.QBTMudBlade/Components/PeersTab.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/TorrentActions.razor create mode 100644 Lantean.QBTMudBlade/Components/TorrentActions.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/TorrentsListNav.razor create mode 100644 Lantean.QBTMudBlade/Components/TorrentsListNav.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/TrackersTab.razor create mode 100644 Lantean.QBTMudBlade/Components/TrackersTab.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/WebSeedsTab.razor create mode 100644 Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs create mode 100644 Lantean.QBTMudBlade/CookieHandler.cs create mode 100644 Lantean.QBTMudBlade/CustomIcons.cs create mode 100644 Lantean.QBTMudBlade/DialogHelper.cs create mode 100644 Lantean.QBTMudBlade/DisplayHelpers.cs create mode 100644 Lantean.QBTMudBlade/ExpressionModifier.cs create mode 100644 Lantean.QBTMudBlade/Extensions.cs create mode 100644 Lantean.QBTMudBlade/Filter/FilterExpressionGenerator.cs create mode 100644 Lantean.QBTMudBlade/Filter/FilterOperator.cs create mode 100644 Lantean.QBTMudBlade/Filter/PropertyFilterDefinition.cs create mode 100644 Lantean.QBTMudBlade/FilterHelper.cs create mode 100644 Lantean.QBTMudBlade/FilterState.cs create mode 100644 Lantean.QBTMudBlade/GlobalSuppressions.cs create mode 100644 Lantean.QBTMudBlade/Interop/BoundingClientRect.cs create mode 100644 Lantean.QBTMudBlade/Interop/InteropHelper.cs create mode 100644 Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj create mode 100644 Lantean.QBTMudBlade/Layout/DetailsLayout.razor create mode 100644 Lantean.QBTMudBlade/Layout/DetailsLayout.razor.cs create mode 100644 Lantean.QBTMudBlade/Layout/ListLayout.razor create mode 100644 Lantean.QBTMudBlade/Layout/ListLayout.razor.cs create mode 100644 Lantean.QBTMudBlade/Layout/LoggedInLayout.razor create mode 100644 Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs create mode 100644 Lantean.QBTMudBlade/Layout/MainLayout.razor create mode 100644 Lantean.QBTMudBlade/Layout/MainLayout.razor.cs create mode 100644 Lantean.QBTMudBlade/Models/AddTorrentFileOptions.cs create mode 100644 Lantean.QBTMudBlade/Models/AddTorrentLinkOptions.cs create mode 100644 Lantean.QBTMudBlade/Models/Category.cs create mode 100644 Lantean.QBTMudBlade/Models/ContentItem.cs create mode 100644 Lantean.QBTMudBlade/Models/ContentItemType.cs create mode 100644 Lantean.QBTMudBlade/Models/GlobalTransferInfo.cs create mode 100644 Lantean.QBTMudBlade/Models/MainData.cs create mode 100644 Lantean.QBTMudBlade/Models/Peer.cs create mode 100644 Lantean.QBTMudBlade/Models/PeerList.cs create mode 100644 Lantean.QBTMudBlade/Models/Priority.cs create mode 100644 Lantean.QBTMudBlade/Models/ServerState.cs create mode 100644 Lantean.QBTMudBlade/Models/Status.cs create mode 100644 Lantean.QBTMudBlade/Models/Torrent.cs create mode 100644 Lantean.QBTMudBlade/Models/TorrentOptions.cs create mode 100644 Lantean.QBTMudBlade/Pages/Details.razor create mode 100644 Lantean.QBTMudBlade/Pages/Details.razor.cs create mode 100644 Lantean.QBTMudBlade/Pages/Login.razor create mode 100644 Lantean.QBTMudBlade/Pages/Login.razor.cs create mode 100644 Lantean.QBTMudBlade/Pages/Main.razor create mode 100644 Lantean.QBTMudBlade/Pages/Main.razor.cs create mode 100644 Lantean.QBTMudBlade/Pages/Options.razor create mode 100644 Lantean.QBTMudBlade/Pages/Options.razor.cs create mode 100644 Lantean.QBTMudBlade/Pages/TorrentList.razor create mode 100644 Lantean.QBTMudBlade/Pages/TorrentList.razor.cs create mode 100644 Lantean.QBTMudBlade/Program.cs create mode 100644 Lantean.QBTMudBlade/Properties/launchSettings.json create mode 100644 Lantean.QBTMudBlade/Services/ClipboardService.cs create mode 100644 Lantean.QBTMudBlade/Services/DataManager.cs create mode 100644 Lantean.QBTMudBlade/Services/IClipboardService.cs create mode 100644 Lantean.QBTMudBlade/Services/IDataManager.cs create mode 100644 Lantean.QBTMudBlade/TableHelper.cs create mode 100644 Lantean.QBTMudBlade/_Imports.razor create mode 100644 Lantean.QBTMudBlade/wwwroot/css/app.css create mode 100644 Lantean.QBTMudBlade/wwwroot/favicon.png create mode 100644 Lantean.QBTMudBlade/wwwroot/icon-192.png create mode 100644 Lantean.QBTMudBlade/wwwroot/index.html create mode 100644 Lantean.QBitTorrentClient/ApiClient.cs create mode 100644 Lantean.QBitTorrentClient/ApiClientExtensions.cs create mode 100644 Lantean.QBitTorrentClient/Converters/CommaSeparatedJsonConverter.cs create mode 100644 Lantean.QBitTorrentClient/Converters/SaveLocationJsonConverter.cs create mode 100644 Lantean.QBitTorrentClient/Converters/StringFloatJsonConverter.cs create mode 100644 Lantean.QBitTorrentClient/FormUrlEncodedBuilder.cs create mode 100644 Lantean.QBitTorrentClient/FormUrlEncodedBuilderExtensions.cs create mode 100644 Lantean.QBitTorrentClient/HttpClientExtensions.cs create mode 100644 Lantean.QBitTorrentClient/IApiClient.cs create mode 100644 Lantean.QBitTorrentClient/Lantean.QBitTorrentClient.csproj create mode 100644 Lantean.QBitTorrentClient/Limits.cs create mode 100644 Lantean.QBitTorrentClient/MockApiClient.cs create mode 100644 Lantean.QBitTorrentClient/Models/BuildInfo.cs create mode 100644 Lantean.QBitTorrentClient/Models/Category.cs create mode 100644 Lantean.QBitTorrentClient/Models/FileData.cs create mode 100644 Lantean.QBitTorrentClient/Models/GlobalTransferInfo.cs create mode 100644 Lantean.QBitTorrentClient/Models/Log.cs create mode 100644 Lantean.QBitTorrentClient/Models/LogType.cs create mode 100644 Lantean.QBitTorrentClient/Models/MainData.cs create mode 100644 Lantean.QBitTorrentClient/Models/Peer.cs create mode 100644 Lantean.QBitTorrentClient/Models/PeerId.cs create mode 100644 Lantean.QBitTorrentClient/Models/PeerLog.cs create mode 100644 Lantean.QBitTorrentClient/Models/PieceState.cs create mode 100644 Lantean.QBitTorrentClient/Models/Preferences.cs create mode 100644 Lantean.QBitTorrentClient/Models/Priority.cs create mode 100644 Lantean.QBitTorrentClient/Models/SaveLocation.cs create mode 100644 Lantean.QBitTorrentClient/Models/ServerState.cs create mode 100644 Lantean.QBitTorrentClient/Models/Torrent.cs create mode 100644 Lantean.QBitTorrentClient/Models/TorrentPeers.cs create mode 100644 Lantean.QBitTorrentClient/Models/TorrentProperties.cs create mode 100644 Lantean.QBitTorrentClient/Models/TorrentTrackers.cs create mode 100644 Lantean.QBitTorrentClient/Models/TrackerStatus.cs create mode 100644 Lantean.QBitTorrentClient/Models/UpdatePreferences.cs create mode 100644 Lantean.QBitTorrentClient/Models/WebSeed.cs create mode 100644 Lantean.QBitTorrentClient/MultipartFormDataContentExtensions.cs create mode 100644 Lantean.QBitTorrentClient/QueryBuilder.cs create mode 100644 Lantean.QBitTorrentClient/QueryBuilderExtensions.cs create mode 100644 Lantean.QBitTorrentClient/SerializerOptions.cs diff --git a/.dcignore b/.dcignore new file mode 100644 index 0000000..700ae42 --- /dev/null +++ b/.dcignore @@ -0,0 +1,39 @@ +# Write glob rules for ignored files. +# Check syntax on https://deepcode.freshdesk.com/support/solutions/articles/60000531055-how-can-i-ignore-files-or-directories- +# Check examples on https://github.com/github/gitignore + +# Hidden directories and files +.* + +# Common binary directories and files +[Bb]in/ +[Oo]bj/ +*.exe +*.dll + +# Logs and temporary files +[Tt]emp/ +*.log + +# Build directories +/build/ +/dist/ +/out/ + +# Node modules and package directories +node_modules/ +**/[Pp]ackages/* + +# Various cache directories +*.cache +/saved/ +/intermediates/ +/generated/ +/coverage/ +/tmp/ + +# Specific directory exclusions +/DocProject/Help/html/ + +# Ignore files from .vs directory +.vs/ diff --git a/Lantean.QBTBlazor.Test/ContentItemSizeComparer.cs b/Lantean.QBTBlazor.Test/ContentItemSizeComparer.cs new file mode 100644 index 0000000..c2b1801 --- /dev/null +++ b/Lantean.QBTBlazor.Test/ContentItemSizeComparer.cs @@ -0,0 +1,3 @@ +namespace Lantean.QBTFluent.Comparers +{ +} \ No newline at end of file diff --git a/Lantean.QBTBlazor.Test/Lantean.QBTBlazor.Test.csproj b/Lantean.QBTBlazor.Test/Lantean.QBTBlazor.Test.csproj new file mode 100644 index 0000000..3531ced --- /dev/null +++ b/Lantean.QBTBlazor.Test/Lantean.QBTBlazor.Test.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/Lantean.QBTBlazor.Test/UnitTest1.cs b/Lantean.QBTBlazor.Test/UnitTest1.cs new file mode 100644 index 0000000..8917a96 --- /dev/null +++ b/Lantean.QBTBlazor.Test/UnitTest1.cs @@ -0,0 +1,67 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBitTorrentClient.Models; +using System.Linq.Expressions; +using System.Text.Json; +using Xunit.Abstractions; + +namespace Lantean.QBTBlazor.Test +{ + public class UnitTest1 + { + private readonly ITestOutputHelper _testOutputHelper; + + public UnitTest1(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Fact] + public void Test() + { + Test2(a => a.Name); + } + + private void Test2(Expression> expr) + { + var body = expr.Body; + } + + [Fact] + public void Create() + { + var propInfo = typeof(TestClass).GetProperty("Name")!; + + ParameterExpression expression = Expression.Parameter(typeof(TestClass), "a"); + var propertyExpression = Expression.Property(expression, "Value"); + + var convertExpression = Expression.Convert(propertyExpression, typeof(object)); + + var l = Expression.Lambda>(convertExpression, expression); + + Expression> expr2 = a => a.Name; + + var x = l.Compile(); + var res = (long)x(new TestClass { Name = "Name", Value = 12 }); + Assert.Equal(12, res); + expr2.Compile(); + } + + [Fact] + public void ScanDir() + { + //Dictionary + var json = "{\r\n\t\"/this/is/path\": 1,\r\n\t\"/this/other\": 0,\r\n\t\"/home\": \"/path\"\r\n}"; + + var obj = JsonSerializer.Deserialize>(json, SerializerOptions.Options); + } + } + + public class TestClass + { + public string Name { get; set; } + + public string Description { get; set; } + + public long Value { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBTBlazor.sln b/Lantean.QBTBlazor.sln new file mode 100644 index 0000000..b218bcb --- /dev/null +++ b/Lantean.QBTBlazor.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34511.84 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTBlazor.Test", "Lantean.QBTBlazor.Test\Lantean.QBTBlazor.Test.csproj", "{715E075C-1D86-4A7F-BC72-E1E24A294F17}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBitTorrentClient", "Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj", "{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMudBlade", "Lantean.QBTMudBlade\Lantean.QBTMudBlade.csproj", "{83BC76CC-D51B-42AF-A6EE-FA400C300098}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {715E075C-1D86-4A7F-BC72-E1E24A294F17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {715E075C-1D86-4A7F-BC72-E1E24A294F17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {715E075C-1D86-4A7F-BC72-E1E24A294F17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {715E075C-1D86-4A7F-BC72-E1E24A294F17}.Release|Any CPU.Build.0 = Release|Any CPU + {F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}.Release|Any CPU.Build.0 = Release|Any CPU + {83BC76CC-D51B-42AF-A6EE-FA400C300098}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {82E46DB7-956A-4971-BB18-1F20650EC1A4} + EndGlobalSection +EndGlobal diff --git a/Lantean.QBTMudBlade/App.razor b/Lantean.QBTMudBlade/App.razor new file mode 100644 index 0000000..6fd3ed1 --- /dev/null +++ b/Lantean.QBTMudBlade/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddCategoryDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/AddCategoryDialog.razor new file mode 100644 index 0000000..3555e0f --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddCategoryDialog.razor @@ -0,0 +1,16 @@ + + + + + + + + + + + + + Cancel + Add + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddCategoryDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/AddCategoryDialog.razor.cs new file mode 100644 index 0000000..6f254d6 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddCategoryDialog.razor.cs @@ -0,0 +1,31 @@ +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class AddCategoryDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + protected string? Category { get; set; } + + protected string SavePath { get; set; } = ""; + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + if (Category is null) + { + return; + } + MudDialog.Close(DialogResult.Ok(new Category(Category, SavePath))); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentFileDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentFileDialog.razor new file mode 100644 index 0000000..3a0c2d2 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentFileDialog.razor @@ -0,0 +1,24 @@ + + + + + + + + Choose files + + + + + + + + + Close + Upload Torrents + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentFileDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentFileDialog.razor.cs new file mode 100644 index 0000000..e750260 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentFileDialog.razor.cs @@ -0,0 +1,34 @@ +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class AddTorrentFileDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + protected IReadOnlyList Files { get; set; } = []; + + protected AddTorrentOptions TorrentOptions { get; set; } = default!; + + protected void UploadFiles(IReadOnlyList files) + { + Files = files; + } + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + var options = new AddTorrentFileOptions(Files, TorrentOptions.GetTorrentOptions()); + MudDialog.Close(DialogResult.Ok(options)); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor new file mode 100644 index 0000000..b6ac2d1 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor @@ -0,0 +1,14 @@ + + + + + + + + + + + Close + Upload Torrents + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor.cs new file mode 100644 index 0000000..6a5e228 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor.cs @@ -0,0 +1,33 @@ +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class AddTorrentLinkDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + protected string? Urls { get; set; } + + protected AddTorrentOptions TorrentOptions { get; set; } = default!; + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + if (Urls is null) + { + MudDialog.Cancel(); + return; + } + var options = new AddTorrentLinkOptions(Urls, TorrentOptions.GetTorrentOptions()); + MudDialog.Close(DialogResult.Ok(options)); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor new file mode 100644 index 0000000..e912139 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor @@ -0,0 +1,62 @@ + + + Manual + Automatic + + + + + +@if (ShowCookieOption) +{ + + + +} + + + + + + @foreach (var category in Categories) + { + @category + } + + + + + + + + + + + None + Metadata received + Files checked + + + + + + + + + + Original + Create subfolder + Don't create subfolder' + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor.cs new file mode 100644 index 0000000..ae030b9 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor.cs @@ -0,0 +1,79 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class AddTorrentOptions + { + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Parameter] + public bool ShowCookieOption { get; set; } + + protected bool TorrentManagementMode { get; set; } + + protected string SavePath { get; set; } = default!; + + protected string? Cookie { get; set; } + + protected string? RenameTorrent { get; set; } + + protected IEnumerable Categories { get; set; } = []; + + protected string? Category { get; set; } + + protected bool StartTorrent { get; set; } = true; + + protected bool AddToTopOfQueue { get; set; } = true; + + protected string StopCondition { get; set; } = "None"; + + protected bool SkipHashCheck { get; set; } = false; + + protected string ContentLayout { get; set; } = "Original"; + + protected bool DownloadInSequentialOrder { get; set; } = false; + + protected bool DownloadFirstAndLastPiecesFirst { get; set; } = false; + + protected long DownloadLimit { get; set; } + + protected long UploadLimit { get; set; } + + protected override async Task OnInitializedAsync() + { + var categories = await ApiClient.GetAllCategories(); + Categories = categories.Select(c => c.Key).ToList(); + + var preferences = await ApiClient.GetApplicationPreferences(); + + TorrentManagementMode = preferences.AutoTmmEnabled; + SavePath = preferences.SavePath; + StartTorrent = !preferences.StartPausedEnabled; + AddToTopOfQueue = preferences.AddToTopOfQueue; + StopCondition = preferences.TorrentStopCondition; + ContentLayout = preferences.TorrentContentLayout; + } + + public TorrentOptions GetTorrentOptions() + { + return new TorrentOptions( + TorrentManagementMode, + SavePath, + Cookie, + RenameTorrent, + Category, + StartTorrent, + AddToTopOfQueue, + StopCondition, + SkipHashCheck, + ContentLayout, + DownloadInSequentialOrder, + DownloadFirstAndLastPiecesFirst, + DownloadLimit, + UploadLimit); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor new file mode 100644 index 0000000..ca404c0 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor @@ -0,0 +1,20 @@ +@typeparam T + + + + + + @foreach (var column in Columns) + { + + + + } + + + + + Cancel + Save + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor.cs new file mode 100644 index 0000000..d679ed0 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class ColumnOptionsDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + [EditorRequired] + public List> Columns { get; set; } = default!; + + protected HashSet SelectedColumns { get; set; } = []; + + protected override void OnParametersSet() + { + if (SelectedColumns.Count == 0) + { + foreach (var column in Columns.Where(c => c.Enabled)) + { + SelectedColumns.Add(column.Id); + } + } + } + + protected void SetSelected(bool selected, string id) + { + if (selected) + { + SelectedColumns.Add(id); + } + else + { + SelectedColumns.Remove(id); + } + } + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + MudDialog.Close(DialogResult.Ok(SelectedColumns)); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/ConfirmDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/ConfirmDialog.razor new file mode 100644 index 0000000..8b39277 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/ConfirmDialog.razor @@ -0,0 +1,9 @@ + + + @Content + + + @CancelText + @SuccessText + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/ConfirmDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/ConfirmDialog.razor.cs new file mode 100644 index 0000000..71b3b93 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/ConfirmDialog.razor.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class ConfirmDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public string Content { get; set; } = default!; + + [Parameter] + public string? SuccessText { get; set; } = "Ok"; + + [Parameter] + public string? CancelText { get; set; } = "Cancel"; + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + MudDialog.Close(DialogResult.Ok(true)); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/DeleteDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/DeleteDialog.razor new file mode 100644 index 0000000..f33c319 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/DeleteDialog.razor @@ -0,0 +1,15 @@ + + + Are you sure you want to remove the selected torrents from the transfer list? + + + + + + + + + Cancel + Remove + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/DeleteDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/DeleteDialog.razor.cs new file mode 100644 index 0000000..18a4462 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/DeleteDialog.razor.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class DeleteDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + protected bool DeleteFiles { get; set; } + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + MudDialog.Close(DialogResult.Ok(DeleteFiles)); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/FilterOptionsDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/FilterOptionsDialog.razor new file mode 100644 index 0000000..036a0fa --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/FilterOptionsDialog.razor @@ -0,0 +1,62 @@ +@typeparam T + + + + + @foreach (var definition in FilterDefinitions ?? []) + { + + @definition.Column + + + + @foreach (var op in Filter.FilterOperator.GetOperatorByDataType(definition.ColumnType)) + { + @op + } + + + + + + + + + + } + + + @foreach (var propertyName in GetAvailablePropertyNames()) + { + + } + + + + + @if (ColumnType is null) + { + Please select a column. + } + else + { + foreach (var op in Filter.FilterOperator.GetOperatorByDataType(ColumnType)) + { + @op + } + } + + + + + + + + + + + + Cancel + Save + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/FilterOptionsDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/FilterOptionsDialog.razor.cs new file mode 100644 index 0000000..2a48b2f --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/FilterOptionsDialog.razor.cs @@ -0,0 +1,127 @@ +using Lantean.QBTMudBlade.Filter; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; +using System.Linq.Expressions; +using System.Reflection; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class FilterOptionsDialog + { + private static readonly IReadOnlyList _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); + + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + protected IReadOnlyList Columns => _properties; + + [Parameter] + public List>? FilterDefinitions { get; set; } + + protected override void OnParametersSet() + { + // as + } + + protected void RemoveDefinition(PropertyFilterDefinition definition) + { + if (FilterDefinitions is null) + { + return; + } + FilterDefinitions.Remove(definition); + } + + protected void DefinitionOperatorChanged(PropertyFilterDefinition definition, string @operator) + { + var existingDefinition = FilterDefinitions?.Find(d => d == definition); + if (existingDefinition is null) + { + return; + } + + existingDefinition.Operator = @operator; + } + + protected void DefinitionValueChanged(PropertyFilterDefinition definition, object? value) + { + var existingDefinition = FilterDefinitions?.Find(d => d == definition); + if (existingDefinition is null) + { + return; + } + + existingDefinition.Value = value; + } + + protected string? Column { get; set; } + protected Type? ColumnType { get; set; } + protected string? Operator { get; set; } + protected string? Value { get; set; } + + protected void ColumnChanged(string column) + { + Column = column; + ColumnType = _properties.FirstOrDefault(p => p.Name == column)?.PropertyType; + } + + protected IEnumerable GetAvailablePropertyNames() + { + foreach (var propertyName in _properties.Select(p => p.Name)) + { + if (!(FilterDefinitions?.Exists(d => d.Column == propertyName) ?? false)) + { + yield return propertyName; + } + } + } + + protected void OperatorChanged(string @operator) + { + Operator = @operator; + } + + protected void ValueChanged(string value) + { + Value = value; + } + + protected async Task AddDefinition() + { + if (Column is null || Operator is null || (FilterDefinitions?.Exists(d => d.Column == Column) ?? false)) + { + return; + } + + CreateAndAdd(Column, Operator, Value); + + await InvokeAsync(StateHasChanged); + } + + private void CreateAndAdd(string column, string @operator, object? value) + { + FilterDefinitions ??= []; + FilterDefinitions.Add(new PropertyFilterDefinition(column, @operator, value)); + + Column = null; + Operator = null; + Value = null; + } + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + if (Column is not null && Operator is not null && !(FilterDefinitions?.Exists(d => d.Column == Column) ?? false)) + { + CreateAndAdd(Column, Operator, Value); + } + + MudDialog.Close(DialogResult.Ok(FilterDefinitions)); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/SingleFieldDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/SingleFieldDialog.razor new file mode 100644 index 0000000..995b06f --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/SingleFieldDialog.razor @@ -0,0 +1,15 @@ +@typeparam T + + + + + + + + + + + Cancel + Save + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/SingleFieldDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/SingleFieldDialog.razor.cs new file mode 100644 index 0000000..3db7c35 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/SingleFieldDialog.razor.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class SingleFieldDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public string Label { get; set; } = default!; + + [Parameter] + public T? Value { get; set; } + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + MudDialog.Close(DialogResult.Ok(Value)); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/SliderFieldDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/SliderFieldDialog.razor new file mode 100644 index 0000000..08db8d2 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/SliderFieldDialog.razor @@ -0,0 +1,18 @@ +@typeparam T + + + + + + + + + + + + + + Cancel + Save + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/SliderFieldDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/SliderFieldDialog.razor.cs new file mode 100644 index 0000000..00bd0f8 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/SliderFieldDialog.razor.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class SliderFieldDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public string Label { get; set; } = default!; + + [Parameter] + public T? Value { get; set; } + + [Parameter] + public T? Min { get; set; } + + [Parameter] + public T? Max { get; set; } + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + MudDialog.Close(DialogResult.Ok(Value)); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor new file mode 100644 index 0000000..2351153 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor @@ -0,0 +1,5 @@ + + + Statistics + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor.cs new file mode 100644 index 0000000..0d1f363 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class StatisticsDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/ExtendedTable.razor b/Lantean.QBTMudBlade/Components/ExtendedTable.razor new file mode 100644 index 0000000..074de2a --- /dev/null +++ b/Lantean.QBTMudBlade/Components/ExtendedTable.razor @@ -0,0 +1,63 @@ +@inherits MudTable +@typeparam T + +@{ + base.BuildRenderTree(__builder); +} + +@code { + private RenderFragment ColGroupFragment(IEnumerable> columns) => + @ + @if (MultiSelection) + { + + } + + @foreach (var column in columns) + { + var style = column.Width.HasValue ? $"width: {column.Width.Value}px" : null; + + } + ; + + private RenderFragment HeaderContentFragment(IEnumerable> columns) => + @ + @foreach (var column in columns) + { + + @if (column.SortSelector is not null) + { + @column.Header + } + else + { + @column.Header + } + + } + ; + + private RenderFragment RowTemplateFragment(IEnumerable> columns) => data => + @ + @foreach (var column in columns) + { + + @column.RowTemplate(column.GetRowContext(data)) + + } + ; + + + private RenderFragment RowTemplateFragment2(IEnumerable> columns) + { + return context => __builder => + { + foreach (var column in columns) + { + + @column.RowTemplate(column.GetRowContext(context)) + + } + }; + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/ExtendedTable.razor.cs b/Lantean.QBTMudBlade/Components/ExtendedTable.razor.cs new file mode 100644 index 0000000..6998d8d --- /dev/null +++ b/Lantean.QBTMudBlade/Components/ExtendedTable.razor.cs @@ -0,0 +1,153 @@ +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class ExtendedTable : MudTable + { + [Parameter] + public IEnumerable>? ColumnDefinitions { get; set; } + + [Parameter] + public HashSet> SelectedColumns { get; set; } = []; + + private Func? _sortSelector; + private SortDirection _sortDirection; + + private IEnumerable? _selectedColumns; + + protected override void OnParametersSet() + { + if (ColumnDefinitions is not null) + { + var activeColumns = GetActiveColummns(ColumnDefinitions); + ColGroup ??= ColGroupFragment(activeColumns); + HeaderContent ??= HeaderContentFragment(activeColumns); + RowTemplate ??= RowTemplateFragment(activeColumns); + _selectedColumns ??= ColumnDefinitions.Where(c => c.Enabled).Select(c => c.Id).ToList(); + _sortSelector ??= ColumnDefinitions.First(c => c.Enabled).SortSelector; + Items = GetOrderedItems(Items, _sortSelector); + } + base.OnParametersSet(); + } + + private IEnumerable? GetOrderedItems(IEnumerable? items, Func sortSelector) + { + if (items is null) + { + return null; + } + + return items.OrderByDirection(_sortDirection, sortSelector); + } + + private void SetSort(Func sortSelector, SortDirection sortDirection) + { + _sortSelector = sortSelector; + _sortDirection = sortDirection; + } + + private IEnumerable>? GetColumns() + { + if (ColumnDefinitions is null) + { + return null; + } + + return GetActiveColummns(ColumnDefinitions); + } + + private IEnumerable> GetActiveColummns(IEnumerable> columns) + { + if (_selectedColumns is null) + { + return columns; + } + return columns.Where(c => _selectedColumns.Contains(c.Id)); + } + + //private RenderFragment CreateColGroup() + //{ + // return builder => + // { + // var selectedColumns = GetColumns(); + // if (selectedColumns is null) + // { + // return; + // } + + // if (MultiSelection) + // { + // builder.OpenElement(0, "col"); + // builder.CloseElement(); + // } + + // int sequence = 1; + // foreach (var width in selectedColumns.Select(c => c.Width)) + // { + // builder.OpenElement(sequence++, "col"); + // if (width.HasValue) + // { + // builder.AddAttribute(sequence++, "style", $"width: {width.Value}px"); + // } + // builder.CloseElement(); + // } + // }; + //} + + //private RenderFragment CreateHeaderContent() + //{ + // return builder => + // { + // var selectedColumns = GetColumns(); + // if (selectedColumns is null) + // { + // return; + // } + + // int sequence = 0; + // foreach (var columnDefinition in selectedColumns) + // { + // builder.OpenComponent(sequence); + // if (columnDefinition.SortSelector is not null) + // { + // builder.OpenComponent>(sequence++); + // builder.AddAttribute(sequence++, "SortDirectionChanged", EventCallback.Factory.Create(this, c => SetSort(columnDefinition.SortSelector, c))); + // RenderFragment childContent = b => b.AddContent(0, columnDefinition.Header); + // builder.AddAttribute(sequence++, "ChildContent", childContent); + // builder.CloseComponent(); + // } + // else + // { + // RenderFragment childContent = b => b.AddContent(0, columnDefinition.Header); + // builder.AddAttribute(sequence++, "ChildContent", childContent); + // } + // builder.CloseComponent(); + // } + // }; + //} + + //private RenderFragment CreateRowTemplate() + //{ + // return context => builder => + // { + // var selectedColumns = GetColumns(); + // if (selectedColumns is null) + // { + // return; + // } + + // int sequence = 0; + // foreach (var columnDefinition in selectedColumns) + // { + // builder.OpenComponent(sequence++); + // builder.AddAttribute(sequence++, "DataLabel", columnDefinition.Header); + // builder.AddAttribute(sequence++, "Class", columnDefinition.Class); + // RenderFragment childContent = b => b.AddContent(0, columnDefinition.RowTemplate(columnDefinition.GetRowContext(context))); + // builder.AddAttribute(sequence++, "ChildContent", childContent); + // builder.CloseComponent(); + // } + // }; + //} + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/FakeNavLink.razor b/Lantean.QBTMudBlade/Components/FakeNavLink.razor new file mode 100644 index 0000000..e8fce5a --- /dev/null +++ b/Lantean.QBTMudBlade/Components/FakeNavLink.razor @@ -0,0 +1,10 @@ +
+ @if (!string.IsNullOrEmpty(Icon)) + { + + } + +
\ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/FakeNavLink.razor.cs b/Lantean.QBTMudBlade/Components/FakeNavLink.razor.cs new file mode 100644 index 0000000..9641806 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/FakeNavLink.razor.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; +using MudBlazor.Utilities; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class FakeNavLink + { + [Parameter] + public bool Active { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string? Class { get; set; } + + [Parameter] + public bool DisableRipple { get; set; } + + /// + /// Icon to use if set. + /// + [Parameter] + public string? Icon { get; set; } + + /// + /// The color of the icon. It supports the theme colors, default value uses the themes drawer icon color. + /// + [Parameter] + public Color IconColor { get; set; } = Color.Default; + + + [Parameter] + public string? Target { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + protected string Classname => + new CssBuilder("mud-nav-item") + .AddClass($"mud-ripple", !DisableRipple && !Disabled) + .AddClass(Class) + .Build(); + + protected string LinkClassname => + new CssBuilder("mud-nav-link") + .AddClass($"mud-nav-link-disabled", Disabled) + .AddClass("active", Active) + .Build(); + + protected string IconClassname => + new CssBuilder("mud-nav-link-icon") + .AddClass($"mud-nav-link-icon-default", IconColor == Color.Default) + .Build(); + + protected async Task OnClickHandler(MouseEventArgs ev) + { + if (Disabled) + { + return; + } + + await OnClick.InvokeAsync(ev); + } + } +} diff --git a/Lantean.QBTMudBlade/Components/FilesTab.razor b/Lantean.QBTMudBlade/Components/FilesTab.razor new file mode 100644 index 0000000..c00dc56 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/FilesTab.razor @@ -0,0 +1,99 @@ + + + + + + + + + Less Than 100% Availability + Less than 80% Availability + Currently Filtered Files + + + Less Than 100% Availability + Less than 80% Availability + Currently Filtered Files + + + + + + + + + + @foreach (var column in GetColumns()) + { + var style = column.Width.HasValue ? $"width: {column.Width.Value}px" : null; + + } + + + @foreach (var column in GetColumns()) + { + + @if (column.SortSelector is not null) + { + @column.Header + } + else + { + @column.Header + } + + } + + + @foreach (var column in GetColumns()) + { + + @column.RowTemplate(column.GetRowContext(context)) + + } + + + +@code { + private RenderFragment> NameColumn + { + get + { + return context => __builder => + { +
+ @if (context.Data.IsFolder) + { + + + } + @context.Data.DisplayName +
; + }; + } + } + + private RenderFragment> PriorityColumn + { + get + { + return context => __builder => + { + + Do not download + Normal + High + Maximum + + }; + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/FilesTab.razor.cs b/Lantean.QBTMudBlade/Components/FilesTab.razor.cs new file mode 100644 index 0000000..005bba3 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/FilesTab.razor.cs @@ -0,0 +1,561 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Components.Dialogs; +using Lantean.QBTMudBlade.Filter; +using Lantean.QBTMudBlade.Models; +using Lantean.QBTMudBlade.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; +using System.Collections.ObjectModel; +using System.Net; +using static MudBlazor.CategoryTypes; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class FilesTab : IAsyncDisposable + { + private readonly CancellationTokenSource _timerCancellationToken = new(); + private bool _disposedValue; + + private Func SortSelector { get; set; } = c => c.Name; + + private SortDirection SortDirection { get; set; } = SortDirection.Ascending; + + [Parameter] + public bool Active { get; set; } + + [Parameter, EditorRequired] + public string? Hash { get; set; } + + [CascadingParameter] + public int RefreshInterval { get; set; } + + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDialogService DialogService { get; set; } = default!; + + [Inject] + protected IDataManager DataManager { get; set; } = default!; + + protected HashSet ExpandedNodes { get; set; } = []; + + protected Dictionary? FileList { get; set; } + + protected IEnumerable Files => GetFiles(); + + protected HashSet SelectedItems { get; set; } = []; + + protected List> _columns = []; + + protected ContentItem? SelectedItem { get; set; } + + protected string? SearchText { get; set; } + + protected int? _selectedIndex { get; set; } + + protected HashSet SelectedColumns { get; set; } + + public IEnumerable>? Filters { get; set; } + + public FilesTab() + { + _columns.Add(CreateColumnDefinition("Name", c => c.Name, NameColumn, width: 200, initialDirection: SortDirection.Ascending)); + _columns.Add(CreateColumnDefinition("Total Size", c => c.Size, c => DisplayHelpers.Size(c.Size))); + _columns.Add(CreateColumnDefinition("Progress", c => c.Progress, c => DisplayHelpers.Percentage(c.Progress))); + _columns.Add(CreateColumnDefinition("Priority", c => c.Priority, PriorityColumn)); + _columns.Add(CreateColumnDefinition("Remaining", c => c.Remaining, c => DisplayHelpers.Size(c.Remaining))); + _columns.Add(CreateColumnDefinition("Availability", c => c.Availability, c => c.Availability.ToString("0.00"))); + + SelectedColumns = _columns.Where(c => c.Enabled).Select(c => c.Id).ToHashSet(); + } + + protected IEnumerable> GetColumns() + { + return _columns.Where(c => SelectedColumns.Contains(c.Id)); + } + + protected async Task ColumnOptions() + { + DialogParameters parameters = new DialogParameters + { + { "Columns", _columns } + }; + + var reference = await DialogService.ShowAsync>("ColumnOptions", parameters, DialogHelper.FormDialogOptions); + + var result = await reference.Result; + if (result.Canceled) + { + return; + } + + SelectedColumns = (HashSet)result.Data; + } + + protected async Task ShowFilterDialog() + { + var parameters = new DialogParameters + { + { nameof(FilterOptionsDialog.FilterDefinitions), Filters }, + }; + + var result = await DialogService.ShowAsync>("Filters", parameters, DialogHelper.FormDialogOptions); + + var dialogResult = await result.Result; + if (dialogResult.Canceled) + { + return; + } + + var filterDefinitions = (List>?)dialogResult.Data; + if (filterDefinitions is null) + { + return; + } + + var filters = new List>(); + foreach (var filterDefinition in filterDefinitions) + { + var expression = Filter.FilterExpressionGenerator.GenerateExpression(filterDefinition, false); + filters.Add(expression.Compile()); + } + + Filters = filters; + } + + protected async Task RemoveFilter() + { + Filters = null; + await InvokeAsync(StateHasChanged); + } + + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + + protected static float CalculateProgress(IEnumerable items) + { + return (float)items.Sum(i => i.Downloaded) / items.Sum(i => i.Size); + } + + protected static Priority GetPriority(IEnumerable items) + { + var distinctPriorities = items.Select(i => i.Priority).Distinct(); + if (distinctPriorities.Count() == 1) + { + return distinctPriorities.First(); + } + + return Priority.Mixed; + } + + protected virtual async Task DisposeAsync(bool disposing) + { + if (!_disposedValue) + { + if (disposing && Files is not null) + { + _timerCancellationToken.Cancel(); + _timerCancellationToken.Dispose(); + + await Task.Delay(0); + } + + _disposedValue = true; + } + } + + protected async Task SearchTextChanged(string value) + { + SearchText = value; + await InvokeAsync(StateHasChanged); + if (FileList is null) + { + return; + } + SelectedItems = FileList.Values.Where(f => f.Priority != Priority.DoNotDownload).ToHashSet(); + } + + protected async Task EnabledValueChanged(ContentItem contentItem, bool value) + { + if (Hash is null) + { + return; + } + + await ApiClient.SetFilePriority(Hash, [contentItem.Index], MapPriority(value ? Priority.Normal : Priority.DoNotDownload)); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + { + return; + } + + using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval))) + { + while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync()) + { + if (Active && Hash is not null) + { + IReadOnlyList files; + try + { + files = await ApiClient.GetTorrentContents(Hash); + } + catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden) + { + _timerCancellationToken.CancelIfNotDisposed(); + return; + } + + if (FileList is null) + { + FileList = DataManager.CreateContentsList(files); + } + else + { + DataManager.MergeContentsList(files, FileList); + } + } + + await InvokeAsync(StateHasChanged); + } + } + } + + protected override async Task OnParametersSetAsync() + { + if (Hash is null) + { + return; + } + + if (!Active) + { + return; + } + + var contents = await ApiClient.GetTorrentContents(Hash); + FileList = DataManager.CreateContentsList(contents); + + SelectedItems = FileList.Values.Where(f => f.Priority != Priority.DoNotDownload).ToHashSet(); + } + + protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority) + { + if (Hash is null) + { + return; + } + + IEnumerable fileIndexes; + if (contentItem.IsFolder) + { + fileIndexes = GetChildren(contentItem).Where(c => !c.IsFolder).Select(c => c.Index); + } + else + { + fileIndexes = [contentItem.Index]; + } + + await ApiClient.SetFilePriority(Hash, fileIndexes, MapPriority(priority)); + } + + protected string RowClass(ContentItem contentItem, int index) + { + if (contentItem.Level == 0) + { + return "d-table-row"; + } + if (ExpandedNodes.Contains(contentItem.Path)) + { + return "d-table-row"; + } + return "d-none"; + } + + protected async Task RenameFile() + { + if (Hash is null || FileList is null || _selectedIndex is null) + { + return; + } + var contentItem = FileList.Values.FirstOrDefault(c => c.Index == _selectedIndex.Value); + if (contentItem is null) + { + return; + } + var name = contentItem.GetFileName(); + await DialogService.ShowSingleFieldDialog("Rename", "New name", name, async v => await ApiClient.RenameFile(Hash, contentItem.Name, contentItem.Path + v)); + } + + protected void RowClick(TableRowClickEventArgs eventArgs) + { + _selectedIndex = eventArgs.Item.Index; + } + + protected string RowStyle(ContentItem item, int index) + { + var style = "user-select: none; cursor: pointer;"; + if (_selectedIndex == item.Index) + { + style += " background: #D3D3D3"; + } + return style; + } + + protected async Task SelectedItemsChanged(HashSet selectedItems) + { + if (Hash is null || Files is null) + { + return; + } + + var unselectedItems = Files.Except(SelectedItems); + + if (unselectedItems.Any()) + { + await ApiClient.SetFilePriority(Hash, unselectedItems.Select(c => c.Index), QBitTorrentClient.Models.Priority.DoNotDownload); + + foreach (var item in unselectedItems) + { + Files.First(f => f == item).Priority = Priority.DoNotDownload; + } + + await InvokeAsync(StateHasChanged); + } + + var existingDoNotDownloads = Files.Where(f => f.Priority == Priority.DoNotDownload); + var newlySelectedFiles = selectedItems.Where(f => existingDoNotDownloads.Contains(f)); + + if (newlySelectedFiles.Any()) + { + await ApiClient.SetFilePriority(Hash, newlySelectedFiles.Select(c => c.Index), QBitTorrentClient.Models.Priority.Normal); + + foreach (var item in newlySelectedFiles) + { + Files.First(f => f == item).Priority = Priority.Normal; + } + + await InvokeAsync(StateHasChanged); + } + } + + private void SetSort(Func sortSelector, SortDirection sortDirection) + { + SortSelector = sortSelector; + SortDirection = sortDirection; + } + + protected void ToggleNode(ContentItem contentItem, MouseEventArgs args) + { + if (ExpandedNodes.Contains(contentItem.Name)) + { + ExpandedNodes.Remove(contentItem.Name); + } + else + { + ExpandedNodes.Add(contentItem.Name); + } + } + + private static QBitTorrentClient.Models.Priority MapPriority(Priority priority) + { + return (QBitTorrentClient.Models.Priority)(int)priority; + } + + private IEnumerable GetChildren(ContentItem contentItem) + { + if (!contentItem.IsFolder || Files is null) + { + return []; + } + + return Files.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder); + } + + private IEnumerable GetDescendants(ContentItem folder, int level) + { + level++; + var descendantsKey = folder.GetDescendantsKey(level); + foreach (var item in FileList!.Values.Where(f => f.Name.StartsWith(descendantsKey)).OrderByDirection(SortDirection, SortSelector)) + { + if (item.IsFolder) + { + var descendants = GetDescendants(item, level); + // if the filter returns some resutls then show folder item + if (descendants.Any()) + { + yield return item; + } + // then show children + foreach (var descendant in descendants) + { + yield return descendant; + } + } + else + { + if (FilterContentItem(item)) + { + yield return item; + } + } + } + } + + private bool FilterContentItem(ContentItem item) + { + if (Filters is not null) + { + foreach (var filter in Filters) + { + var result = filter(item); + if (!result) + { + return false; + } + } + } + + if (!FilterHelper.FilterTerms(item.Name, SearchText)) + { + return false; + } + + return true; + } + + private ReadOnlyCollection GetFiles() + { + if (FileList is null) + { + return new ReadOnlyCollection([]); + } + + var maxLevel = FileList.Values.Max(f => f.Level); + // this is a flat file structure + if (maxLevel == 0) + { + return FileList.Values.Where(FilterContentItem).OrderByDirection(SortDirection, SortSelector).ToList().AsReadOnly(); + } + + var list = new List(); + + var folders = FileList.Values.Where(c => c.IsFolder && c.Level == 0).OrderByDirection(SortDirection, SortSelector).ToList(); + foreach (var folder in folders) + { + list.Add(folder); + var level = 0; + var descendants = GetDescendants(folder, level); + foreach (var descendant in descendants) + { + list.Add(descendant); + } + } + + return list.AsReadOnly(); + } + + protected async Task DoNotDownloadLessThan100PercentAvailability() + { + await LessThanXAvailability(1f, QBitTorrentClient.Models.Priority.DoNotDownload); + } + + protected async Task DoNotDownloadLessThan80PercentAvailability() + { + await LessThanXAvailability(0.8f, QBitTorrentClient.Models.Priority.DoNotDownload); + } + + protected async Task DoNotDownloadCurrentlyFilteredFiles() + { + await CurrentlyFilteredFiles(QBitTorrentClient.Models.Priority.DoNotDownload); + } + + protected async Task NormalPriorityLessThan100PercentAvailability() + { + await LessThanXAvailability(1f, QBitTorrentClient.Models.Priority.Normal); + } + + protected async Task NormalPriorityLessThan80PercentAvailability() + { + await LessThanXAvailability(0.8f, QBitTorrentClient.Models.Priority.Normal); + } + + protected async Task NormalPriorityCurrentlyFilteredFiles() + { + await CurrentlyFilteredFiles(QBitTorrentClient.Models.Priority.Normal); + } + + private async Task LessThanXAvailability(float value, QBitTorrentClient.Models.Priority priority) + { + if (Hash is null || FileList is null) + { + return; + } + + var files = FileList.Values.Where(f => f.Availability < value).Select(f => f.Index); + + if (!files.Any()) + { + return; + } + + await ApiClient.SetFilePriority(Hash, files, priority); + } + + protected async Task CurrentlyFilteredFiles(QBitTorrentClient.Models.Priority priority) + { + if (Hash is null || FileList is null) + { + return; + } + + var files = GetFiles().Select(f => f.Index); + + if (!files.Any()) + { + return; + } + + await ApiClient.SetFilePriority(Hash, files, priority); + } + + private static ColumnDefinition CreateColumnDefinition(string name, Func selector, RenderFragment> rowTemplate, int? width = null, string? tdClass = null, bool enabled = true, SortDirection initialDirection = SortDirection.None) + { + var cd = new ColumnDefinition(name, selector, rowTemplate); + cd.Class = "no-wrap"; + if (tdClass is not null) + { + cd.Class += " " + tdClass; + } + cd.Width = width; + cd.Enabled = enabled; + cd.InitialDirection = initialDirection; + + return cd; + } + + private static ColumnDefinition CreateColumnDefinition(string name, Func selector, Func? formatter = null, int? width = null, string? tdClass = null, bool enabled = true, SortDirection initialDirection = SortDirection.None) + { + var cd = new ColumnDefinition(name, selector, formatter); + cd.Class = "no-wrap"; + if (tdClass is not null) + { + cd.Class += " " + tdClass; + } + cd.Width = width; + cd.Enabled = enabled; + cd.InitialDirection = initialDirection; + + return cd; + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/FiltersNav.razor b/Lantean.QBTMudBlade/Components/FiltersNav.razor new file mode 100644 index 0000000..cfed30b --- /dev/null +++ b/Lantean.QBTMudBlade/Components/FiltersNav.razor @@ -0,0 +1,27 @@ + + + @foreach (var (status, count) in Statuses) + { + var (icon, color) = DisplayHelpers.GetStatusIcon(status); + @($"{status.GetStatusName()} ({count})") + } + + + @foreach (var (category, count) in Categories) + { + @($"{category} ({count})") + } + + + @foreach (var (tag, count) in Tags) + { + @($"{tag} ({count})") + } + + + @foreach (var (tracker, count) in Trackers) + { + @($"{GetHostName(tracker)} ({count})") + } + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/FiltersNav.razor.cs b/Lantean.QBTMudBlade/Components/FiltersNav.razor.cs new file mode 100644 index 0000000..4349177 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/FiltersNav.razor.cs @@ -0,0 +1,84 @@ +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class FiltersNav + { + private bool _statusExpanded = true; + private bool _categoriesExpanded = true; + private bool _tagsExpanded = true; + private bool _trackersExpanded = true; + + protected string Status { get; set; } = Models.Status.All.ToString(); + + protected string Category { get; set; } = FilterHelper.CATEGORY_ALL; + + protected string Tag { get; set; } = FilterHelper.TAG_ALL; + + protected string Tracker { get; set; } = FilterHelper.TRACKER_ALL; + + [CascadingParameter] + public MainData? MainData { get; set; } + + [Parameter] + public EventCallback CategoryChanged { get; set; } + + [Parameter] + public EventCallback StatusChanged { get; set; } + + [Parameter] + public EventCallback TagChanged { get; set; } + + [Parameter] + public EventCallback TrackerChanged { get; set; } + + public Dictionary Tags => MainData?.TagState.ToDictionary(d => d.Key, d => d.Value.Count) ?? []; + + public Dictionary Categories => MainData?.CategoriesState.ToDictionary(d => d.Key, d => d.Value.Count) ?? []; + + public Dictionary Trackers => MainData?.TrackersState.ToDictionary(d => d.Key, d => d.Value.Count) ?? []; + + public Dictionary Statuses => MainData?.StatusState.ToDictionary(d => d.Key, d => d.Value.Count) ?? []; + + protected async Task StatusValueChanged(string value) + { + Status = value; + await StatusChanged.InvokeAsync(Enum.Parse(value)); + } + + protected async Task CategoryValueChanged(string value) + { + Category = value; + await CategoryChanged.InvokeAsync(value); + } + + protected async Task TagValueChanged(string value) + { + Tag = value; + await TagChanged.InvokeAsync(value); + } + + protected async Task TrackerValueChanged(string value) + { + Tracker = value; + await TrackerChanged.InvokeAsync(value); + } + + protected static string GetHostName(string tracker) + { + try + { + var uri = new Uri(tracker); + return uri.Host; + } + catch + { + return tracker; + } + } + + + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/GeneralTab.razor b/Lantean.QBTMudBlade/Components/GeneralTab.razor new file mode 100644 index 0000000..dde794e --- /dev/null +++ b/Lantean.QBTMudBlade/Components/GeneralTab.razor @@ -0,0 +1,97 @@ +
Transfer
+ + + @DisplayHelpers.Duration(Properties?.TimeElapsed) + + + @DisplayHelpers.Duration(Properties?.EstimatedTimeOfArrival) + + + @DisplayHelpers.Duration(Properties?.Connections) @DisplayHelpers.EmptyIfNull(Properties?.ConnectionsLimit, "(", " max)") + + + + @DisplayHelpers.Size(Properties?.TotalDownloaded) @DisplayHelpers.Size(Properties?.TotalDownloadedSession, "(", " this session)") + + + @DisplayHelpers.Size(Properties?.TotalUploaded) @DisplayHelpers.Size(Properties?.TotalUploaded, "(", " this session)") + + + @DisplayHelpers.Size(Properties?.Seeds) @DisplayHelpers.EmptyIfNull(Properties?.Seeds, "(", " total)") + + + + @DisplayHelpers.Speed(Properties?.DownloadSpeed) @DisplayHelpers.Speed(Properties?.DownloadSpeedAverage, "(", " avg.)") + + + @DisplayHelpers.Speed(Properties?.UploadSpeed) @DisplayHelpers.Speed(Properties?.UploadSpeedAverage, "(", " avg.)") + + + @DisplayHelpers.Size(Properties?.Peers) @DisplayHelpers.EmptyIfNull(Properties?.Peers, "(", " total)") + + + + @DisplayHelpers.Speed(Properties?.DownloadLimit) + + + @DisplayHelpers.Speed(Properties?.UploadLimit) + + + @DisplayHelpers.Size(Properties?.TotalWasted) + + + + @Properties?.ShareRatio.ToString("0.00") + + + @DisplayHelpers.Duration(Properties?.Reannounce) + + + @DisplayHelpers.DateTime(Properties?.LastSeen, "Never") + + + + +
Information
+ + + @DisplayHelpers.Size(Properties?.TotalSize) + + + + @if (Properties is not null) + { + @Properties.PiecesNum x @DisplayHelpers.Size(Properties.PieceSize) (have @Properties.PiecesHave) + } + + + + @Properties?.CreatedBy + + + + @DisplayHelpers.DateTime(Properties?.AdditionDate) + + + @DisplayHelpers.DateTime(Properties?.CompletionDate) + + + @DisplayHelpers.DateTime(Properties?.CreationDate) + + + + @Properties?.InfoHashV1 + + + + @Properties?.InfoHashV2 + + + + @Properties?.SavePath + + + + @Properties?.Comment + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs b/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs new file mode 100644 index 0000000..4e409f5 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs @@ -0,0 +1,101 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBitTorrentClient.Models; +using Lantean.QBTMudBlade.Services; +using Microsoft.AspNetCore.Components; +using System.Net; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class GeneralTab : IAsyncDisposable + { + private readonly CancellationTokenSource _timerCancellationToken = new(); + private bool _disposedValue; + + [Parameter, EditorRequired] + public string? Hash { get; set; } + + [Parameter] + public bool Active { get; set; } + + [CascadingParameter] + public int RefreshInterval { get; set; } + + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDataManager DataManager { get; set; } = default!; + + protected IReadOnlyList Pieces { get; set; } = []; + + protected TorrentProperties Properties { get; set; } = default!; + + protected override async Task OnParametersSetAsync() + { + if (Hash is null) + { + return; + } + + if (!Active) + { + return; + } + + Pieces = await ApiClient.GetTorrentPieceStates(Hash); + Properties = await ApiClient.GetTorrentProperties(Hash); + + await InvokeAsync(StateHasChanged); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval))) + { + while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync()) + { + if (Active && Hash is not null) + { + try + { + Pieces = await ApiClient.GetTorrentPieceStates(Hash); + Properties = await ApiClient.GetTorrentProperties(Hash); + } + catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden) + { + _timerCancellationToken.CancelIfNotDisposed(); + return; + } + } + + await InvokeAsync(StateHasChanged); + } + } + } + } + + protected virtual async Task DisposeAsync(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _timerCancellationToken.Cancel(); + _timerCancellationToken.Dispose(); + await Task.Delay(0); + } + + _disposedValue = true; + } + } + + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Menu.razor b/Lantean.QBTMudBlade/Components/Menu.razor new file mode 100644 index 0000000..cc94784 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Menu.razor @@ -0,0 +1,9 @@ + + Statistics + + Settings + Reset Web UI + + Logout + Exit qBittorrent + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Menu.razor.cs b/Lantean.QBTMudBlade/Components/Menu.razor.cs new file mode 100644 index 0000000..9eece45 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Menu.razor.cs @@ -0,0 +1,58 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBitTorrentClient.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class Menu + { + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [Inject] + protected IDialogService DialogService { get; set; } = default!; + + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + protected async Task ResetWebUI() + { + var preferences = new UpdatePreferences + { + AlternativeWebuiPath = null, + AlternativeWebuiEnabled = false, + }; + + await ApiClient.SetApplicationPreferences(preferences); + + NavigationManager.NavigateTo("/", true); + } + + protected void Settings() + { + NavigationManager.NavigateTo("/options"); + } + + protected void Statistics() + { + NavigationManager.NavigateTo("/statistics"); + } + + protected async Task Logout() + { + await DialogService.ShowConfirmDialog("Logout?", "Are you sure you want to logout?", async () => + { + await ApiClient.Logout(); + + NavigationManager.NavigateTo("/login", true); + }); + } + + protected async Task Exit() + { + await DialogService.ShowConfirmDialog("Quit?", "Are you sure you want to exit qBittorrent?", ApiClient.Shutdown); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/NonRendering.razor b/Lantean.QBTMudBlade/Components/NonRendering.razor new file mode 100644 index 0000000..e40766b --- /dev/null +++ b/Lantean.QBTMudBlade/Components/NonRendering.razor @@ -0,0 +1 @@ +@ChildContent \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/NonRendering.razor.cs b/Lantean.QBTMudBlade/Components/NonRendering.razor.cs new file mode 100644 index 0000000..f40dfb5 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/NonRendering.razor.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Components; + +namespace Lantean.QBTMudBlade.Components +{ + /// + /// A simple razor wrapper that only renders the child content without any additonal html markup + /// + public partial class NonRendering + { + /// + /// The child content to be rendered + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + } +} diff --git a/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor b/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor new file mode 100644 index 0000000..67b5042 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor @@ -0,0 +1,14 @@ +@inherits Options + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor.cs new file mode 100644 index 0000000..7a33834 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor.cs @@ -0,0 +1,10 @@ +namespace Lantean.QBTMudBlade.Components.Options +{ + public partial class AdvancedOptions : Options + { + protected override bool SetOptions() + { + return true; + } + } +} diff --git a/Lantean.QBTMudBlade/Components/Options/BehaviourOptions.razor b/Lantean.QBTMudBlade/Components/Options/BehaviourOptions.razor new file mode 100644 index 0000000..f0df2d7 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/BehaviourOptions.razor @@ -0,0 +1,69 @@ +@inherits Options + + + + + Language + + + + + + + English + + + + + + + + + + Log File + + + + + + + + + + + + + + + + + + + + + + + + + + + days + months + years + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/BehaviourOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/BehaviourOptions.razor.cs new file mode 100644 index 0000000..9732a06 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/BehaviourOptions.razor.cs @@ -0,0 +1,99 @@ +using Lantean.QBitTorrentClient.Models; + +namespace Lantean.QBTMudBlade.Components.Options +{ + public partial class BehaviourOptions : Options + { + protected bool FileLogEnabled { get; set; } + + protected string? FileLogPath { get; set; } + + protected bool FileLogBackupEnabled { get; set; } + + protected int FileLogMaxSize { get; set; } + + protected bool FileLogDeleteOld { get; set; } + + protected int FileLogAge { get; set; } + + protected int FileLogAgeType { get; set; } + + protected bool PerformanceWarning { get; set; } + + protected override bool SetOptions() + { + if (Preferences is null) + { + return false; + } + + FileLogEnabled = Preferences.FileLogEnabled; + FileLogPath = Preferences.FileLogPath; + FileLogBackupEnabled = Preferences.FileLogBackupEnabled; + FileLogMaxSize = Preferences.FileLogMaxSize; + FileLogDeleteOld = Preferences.FileLogDeleteOld; + FileLogAge = Preferences.FileLogAge; + FileLogAgeType = Preferences.FileLogAgeType; + PerformanceWarning = Preferences.PerformanceWarning; + + return true; + } + + protected async Task FileLogEnabledChanged(bool value) + { + FileLogEnabled = value; + UpdatePreferences.FileLogEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + await InvokeAsync(StateHasChanged); + } + + protected async Task FileLogPathChanged(string value) + { + FileLogPath = value; + UpdatePreferences.FileLogPath = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task FileLogBackupEnabledChanged(bool value) + { + FileLogBackupEnabled = value; + UpdatePreferences.FileLogBackupEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task FileLogMaxSizeChanged(int value) + { + FileLogMaxSize = value; + UpdatePreferences.FileLogMaxSize = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task FileLogDeleteOldChanged(bool value) + { + FileLogDeleteOld = value; + UpdatePreferences.FileLogDeleteOld = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task FileLogAgeChanged(int value) + { + FileLogAge = value; + UpdatePreferences.FileLogAge = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task FileLogAgeTypeChanged(int value) + { + FileLogAgeType = value; + UpdatePreferences.FileLogAgeType = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task PerformanceWarningChanged(bool value) + { + PerformanceWarning = value; + UpdatePreferences.PerformanceWarning = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/BitTorrentOptions.razor b/Lantean.QBTMudBlade/Components/Options/BitTorrentOptions.razor new file mode 100644 index 0000000..67b5042 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/BitTorrentOptions.razor @@ -0,0 +1,14 @@ +@inherits Options + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/BitTorrentOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/BitTorrentOptions.razor.cs new file mode 100644 index 0000000..aef6f6f --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/BitTorrentOptions.razor.cs @@ -0,0 +1,10 @@ +namespace Lantean.QBTMudBlade.Components.Options +{ + public partial class BitTorrentOptions : Options + { + protected override bool SetOptions() + { + return true; + } + } +} diff --git a/Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor b/Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor new file mode 100644 index 0000000..fddb7a0 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor @@ -0,0 +1,72 @@ +@inherits Options + + + + + + + TCP and μTP + TCP + μTP + + + + + + + + + + Listening Port + + + + + + + + + Random + + + + + + + + + + + + Connections Limits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor.cs new file mode 100644 index 0000000..e1a142c --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor.cs @@ -0,0 +1,330 @@ +using System.Numerics; + +namespace Lantean.QBTMudBlade.Components.Options +{ + public partial class ConnectionOptions : Options + { + protected int BittorrentProtocol { get; private set; } + protected int ListenPort { get; private set; } + protected bool Upnp { get; private set; } + protected bool MaxConnecEnabled { get; private set; } + protected int MaxConnec { get; private set; } + protected bool MaxConnecPerTorrentEnabled { get; private set; } + protected int MaxConnecPerTorrent { get; private set; } + protected bool MaxUploadsEnabled { get; private set; } + protected int MaxUploads { get; private set; } + protected bool MaxUploadsPerTorrentEnabled { get; private set; } + protected int MaxUploadsPerTorrent { get; private set; } + protected bool I2pEnabled { get; private set; } + protected string? I2pAddress { get; private set; } + protected int I2pPort { get; private set; } + protected bool I2pMixedMode { get; private set; } + protected string? ProxyType { get; private set; } + protected string? ProxyIp { get; private set; } + protected int ProxyPort { get; private set; } + protected bool ProxyAuthEnabled { get; private set; } + protected string? ProxyUsername { get; private set; } + protected string? ProxyPassword { get; private set; } + protected bool ProxyHostnameLookup { get; private set; } + protected bool ProxyBittorrent { get; private set; } + protected bool ProxyPeerConnections { get; private set; } + protected bool ProxyRss { get; private set; } + protected bool ProxyMisc { get; private set; } + protected bool IpFilterEnabled { get; private set; } + protected string? IpFilterPath { get; private set; } + protected bool IpFilterTrackers { get; private set; } + protected string? BannedIPs { get; private set; } + + protected override bool SetOptions() + { + if (Preferences is null) + { + return false; + } + + BittorrentProtocol = Preferences.BittorrentProtocol; + ListenPort = Preferences.ListenPort; + Upnp = Preferences.Upnp; + if (Preferences.MaxConnec > 0) + { + MaxConnecEnabled = true; + MaxConnec = Preferences.MaxConnec; + } + else + { + MaxConnecEnabled = false; + MaxConnec = 500; + } + + if (Preferences.MaxConnecPerTorrent > 0) + { + MaxConnecPerTorrentEnabled = true; + MaxConnecPerTorrent = Preferences.MaxConnecPerTorrent; + } + else + { + MaxConnecPerTorrentEnabled = false; + MaxConnecPerTorrent = 100; + } + + if (Preferences.MaxUploads > 0) + { + MaxUploadsEnabled = true; + MaxUploads = Preferences.MaxUploads; + } + else + { + MaxUploadsEnabled = false; + MaxUploads = 20; + } + + if (Preferences.MaxUploadsPerTorrent > 0) + { + MaxUploadsPerTorrentEnabled = true; + MaxUploadsPerTorrent = Preferences.MaxUploadsPerTorrent; + } + else + { + MaxUploadsPerTorrentEnabled = false; + MaxUploadsPerTorrent = 4; + } + + I2pEnabled = Preferences.I2pEnabled; + I2pAddress = Preferences.I2pAddress; + I2pPort = Preferences.I2pPort; + I2pMixedMode = Preferences.I2pMixedMode; + + ProxyType = Preferences.ProxyType; + ProxyIp = Preferences.ProxyIp; + ProxyPort = Preferences.ProxyPort; + ProxyAuthEnabled = Preferences.ProxyAuthEnabled; + ProxyUsername = Preferences.ProxyUsername; + ProxyPassword = Preferences.ProxyPassword; + ProxyHostnameLookup = Preferences.ProxyHostnameLookup; + ProxyBittorrent = Preferences.ProxyBittorrent; + ProxyPeerConnections = Preferences.ProxyPeerConnections; + ProxyRss = Preferences.ProxyRss; + ProxyMisc = Preferences.ProxyMisc; + + IpFilterEnabled = Preferences.IpFilterEnabled; + IpFilterPath = Preferences.IpFilterPath; + IpFilterTrackers = Preferences.IpFilterTrackers; + BannedIPs = Preferences.BannedIPs; + + return true; + } + + protected async Task BittorrentProtocolChanged(int value) + { + BittorrentProtocol = value; + UpdatePreferences.BittorrentProtocol = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ListenPortChanged(int value) + { + ListenPort = value; + UpdatePreferences.ListenPort = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task UpnpChanged(bool value) + { + Upnp = value; + UpdatePreferences.Upnp = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected void MaxConnecEnabledChanged(bool value) + { + MaxConnecEnabled = value; + } + + protected async Task MaxConnecChanged(int value) + { + MaxConnec = value; + UpdatePreferences.MaxConnec = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected void MaxConnecPerTorrentEnabledChanged(bool value) + { + MaxConnecPerTorrentEnabled = value; + } + + protected async Task MaxConnecPerTorrentChanged(int value) + { + MaxConnecPerTorrent = value; + UpdatePreferences.MaxConnecPerTorrent = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected void MaxUploadsEnabledChanged(bool value) + { + MaxUploadsEnabled = value; + } + + protected async Task MaxUploadsChanged(int value) + { + MaxUploads = value; + UpdatePreferences.MaxUploads = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected void MaxUploadsPerTorrentEnabledChanged(bool value) + { + MaxUploadsPerTorrentEnabled = value; + } + + protected async Task MaxUploadsPerTorrentChanged(int value) + { + MaxUploadsPerTorrent = value; + UpdatePreferences.MaxUploadsPerTorrent = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task I2pEnabledChanged(bool value) + { + I2pEnabled = value; + UpdatePreferences.I2pEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task I2pAddressChanged(string value) + { + I2pAddress = value; + UpdatePreferences.I2pAddress = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task I2pPortChanged(int value) + { + I2pPort = value; + UpdatePreferences.I2pPort = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task I2pMixedModeChanged(bool value) + { + I2pMixedMode = value; + UpdatePreferences.I2pMixedMode = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyTypeChanged(string value) + { + ProxyType = value; + UpdatePreferences.ProxyType = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyIpChanged(string value) + { + ProxyIp = value; + UpdatePreferences.ProxyIp = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyPortChanged(int value) + { + ProxyPort = value; + UpdatePreferences.ProxyPort = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyAuthEnabledChanged(bool value) + { + ProxyAuthEnabled = value; + UpdatePreferences.ProxyAuthEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyUsernameChanged(string value) + { + ProxyUsername = value; + UpdatePreferences.ProxyUsername = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyPasswordChanged(string value) + { + ProxyPassword = value; + UpdatePreferences.ProxyPassword = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyHostnameLookupChanged(bool value) + { + ProxyHostnameLookup = value; + UpdatePreferences.ProxyHostnameLookup = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyBittorrentChanged(bool value) + { + ProxyBittorrent = value; + UpdatePreferences.ProxyBittorrent = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyPeerConnectionsChanged(bool value) + { + ProxyPeerConnections = value; + UpdatePreferences.ProxyPeerConnections = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyRssChanged(bool value) + { + ProxyRss = value; + UpdatePreferences.ProxyRss = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ProxyMiscChanged(bool value) + { + ProxyMisc = value; + UpdatePreferences.ProxyMisc = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task IpFilterEnabledChanged(bool value) + { + IpFilterEnabled = value; + UpdatePreferences.IpFilterEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task IpFilterPathChanged(string value) + { + IpFilterPath = value; + UpdatePreferences.IpFilterPath = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task IpFilterTrackersChanged(bool value) + { + IpFilterTrackers = value; + UpdatePreferences.IpFilterTrackers = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task BannedIPsChanged(string value) + { + BannedIPs = value; + UpdatePreferences.BannedIPs = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected const int MinPortValue = 1024; + protected const int MaxPortValue = 65535; + + protected async Task GenerateRandomPort() + { + var random = new Random(); + var port = random.Next(MinPortValue, MaxPortValue); + + await ListenPortChanged(port); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor b/Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor new file mode 100644 index 0000000..d9306fd --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor @@ -0,0 +1,287 @@ +@inherits Options + + + + + When adding a torrent + + + + + + + Original + Create subfolder + Don't create subfolder + + + + + + + + + + + None + Metadata received + Files Checked + + + + + + + + + + + + + + + + + + + + + + + + + + Saving Management + + + + + + + Manual + Automatic + + + + + Relocate torrent + Switch torrent to Manual Mode + + + + + Relocate affected torrents + Switch affected torrents to Manual Mode + + + + + Relocate affected torrents + Switch affected torrents to Manual Mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Automatically add torrents from + + + + + + + Monitored Folder + Override Save Location + + + + @foreach (var item in ScanDirs) + { + + + + + + + + + Monitored folder + Default save location + Other... + + + @if (item.Value.SavePath is not null) + { + + + + } + + + + + } + @for (int i = 0; i < AddedScanDirs.Count; i++) + { + var item = AddedScanDirs[i]; + var index = i; + var isLast = i == AddedScanDirs.Count - 1; + + + + + + + + + + Monitored folder + Default save location + Other... + + + @if (item.Value.SavePath is not null) + { + + + + } + @if (isLast) + { + + + + } + + + + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Run exernal program + + + + + + + + + + + + + + + + + + Supported parameters (case sensitive): + + %N: Torrent name + %L: Category + %G: Tags (separated by comma) + %F: Content path (same as root path for multifile torrent) + %R: Root path (first torrent subdirectory path) + %D: Save path + %C: Number of files + %Z: Torrent size (bytes) + %T: Current tracker + %I: Info hash v1 + %J: Info hash v2 + %K: Torrent ID + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor.cs new file mode 100644 index 0000000..6e6ef8d --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor.cs @@ -0,0 +1,399 @@ +using Lantean.QBitTorrentClient.Models; + +namespace Lantean.QBTMudBlade.Components.Options +{ + public partial class DownloadsOptions : Options + { + protected string? TorrentContentLayout { get; set; } + protected bool AddToTopOfQueue { get; set; } + protected bool StartPausedEnabled { get; set; } + protected string? TorrentStopCondition { get; set; } + protected bool AutoDeleteMode { get; set; } + protected bool PreallocateAll { get; set; } + protected bool IncompleteFilesExt { get; set; } + protected bool AutoTmmEnabled { get; set; } + protected bool TorrentChangedTmmEnabled { get; set; } + protected bool SavePathChangedTmmEnabled { get; set; } + protected bool CategoryChangedTmmEnabled { get; set; } + protected bool UseSubcategories { get; set; } + protected string? SavePath { get; set; } + protected bool TempPathEnabled { get; set; } + protected string? TempPath { get; set; } + protected bool ExportDirEnabled { get; set; } + protected string? ExportDir { get; set; } + protected bool ExportDirFinEnabled { get; set; } + protected string? ExportDirFin { get; set; } + protected Dictionary ScanDirs { get; set; } = []; + protected bool ExcludedFileNamesEnabled { get; set; } + protected string? ExcludedFileNames { get; set; } + protected bool MailNotificationEnabled { get; set; } + protected string? MailNotificationSender { get; set; } + protected string? MailNotificationEmail { get; set; } + protected string? MailNotificationSmtp { get; set; } + protected bool MailNotificationSslEnabled { get; set; } + protected bool MailNotificationAuthEnabled { get; set; } + protected string? MailNotificationUsername { get; set; } + protected string? MailNotificationPassword { get; set; } + protected bool AutorunOnTorrentAddedEnabled { get; set; } + protected string? AutorunOnTorrentAddedProgram { get; set; } + protected bool AutorunEnabled { get; set; } + protected string? AutorunProgram { get; set; } + + protected List> AddedScanDirs { get; set; } = []; + + protected override bool SetOptions() + { + if (Preferences is null) + { + return false; + } + + // when adding a torrent + TorrentContentLayout = Preferences.TorrentContentLayout; + AddToTopOfQueue = Preferences.AddToTopOfQueue; + StartPausedEnabled = Preferences.StartPausedEnabled; + TorrentStopCondition = Preferences.TorrentStopCondition; + AutoDeleteMode = Preferences.AutoDeleteMode == 1; + PreallocateAll = Preferences.PreallocateAll; + IncompleteFilesExt = Preferences.IncompleteFilesExt; + + // saving management + AutoTmmEnabled = Preferences.AutoTmmEnabled; + TorrentChangedTmmEnabled = Preferences.TorrentChangedTmmEnabled; + SavePathChangedTmmEnabled = Preferences.SavePathChangedTmmEnabled; + CategoryChangedTmmEnabled = Preferences.CategoryChangedTmmEnabled; + UseSubcategories = Preferences.UseSubcategories; + SavePath = Preferences.SavePath; + TempPathEnabled = Preferences.TempPathEnabled; + TempPath = Preferences.TempPath; + ExportDir = Preferences.ExportDir; + ExportDirEnabled = !string.IsNullOrEmpty(Preferences.ExportDir); + ExportDirFin = Preferences.ExportDirFin; + ExportDirFinEnabled = !string.IsNullOrEmpty(Preferences.ExportDirFin); + + ScanDirs.Clear(); + foreach (var dir in Preferences.ScanDirs) + { + ScanDirs.Add(dir.Key, dir.Value); + } + + ExcludedFileNamesEnabled = Preferences.ExcludedFileNamesEnabled; + ExcludedFileNames = Preferences.ExcludedFileNames; + + // email notification + MailNotificationEnabled = Preferences.MailNotificationEnabled; + MailNotificationSender = Preferences.MailNotificationSender; + MailNotificationEmail = Preferences.MailNotificationEmail; + MailNotificationSmtp = Preferences.MailNotificationSmtp; + MailNotificationSslEnabled = Preferences.MailNotificationSslEnabled; + MailNotificationAuthEnabled = Preferences.MailNotificationAuthEnabled; + MailNotificationUsername = Preferences.MailNotificationUsername; + MailNotificationPassword = Preferences.MailNotificationPassword; + + // autorun + AutorunOnTorrentAddedEnabled = Preferences.AutorunOnTorrentAddedEnabled; + AutorunOnTorrentAddedProgram = Preferences.AutorunOnTorrentAddedProgram; + AutorunEnabled = Preferences.AutorunEnabled; + AutorunProgram = Preferences.AutorunProgram; + + AddedScanDirs.Clear(); + AddDefaultScanDir(); + + return true; + } + + protected async Task TorrentContentLayoutChanged(string value) + { + TorrentContentLayout = value; + UpdatePreferences.TorrentContentLayout = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task AddToTopOfQueueChanged(bool value) + { + AddToTopOfQueue = value; + UpdatePreferences.AddToTopOfQueue = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task StartPausedEnabledChanged(bool value) + { + StartPausedEnabled = value; + UpdatePreferences.StartPausedEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task TorrentStopConditionChanged(string value) + { + TorrentStopCondition = value; + UpdatePreferences.TorrentStopCondition = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task AutoDeleteModeChanged(bool value) + { + AutoDeleteMode = value; + UpdatePreferences.AutoDeleteMode = value ? 1 : 0; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task PreallocateAllChanged(bool value) + { + PreallocateAll = value; + UpdatePreferences.PreallocateAll = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task IncompleteFilesExtChanged(bool value) + { + IncompleteFilesExt = value; + UpdatePreferences.IncompleteFilesExt = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task AutoTmmEnabledChanged(bool value) + { + AutoTmmEnabled = value; + UpdatePreferences.AutoTmmEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task TorrentChangedTmmEnabledChanged(bool value) + { + TorrentChangedTmmEnabled = value; + UpdatePreferences.TorrentChangedTmmEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task SavePathChangedTmmEnabledChanged(bool value) + { + SavePathChangedTmmEnabled = value; + UpdatePreferences.SavePathChangedTmmEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task CategoryChangedTmmEnabledChanged(bool value) + { + CategoryChangedTmmEnabled = value; + UpdatePreferences.CategoryChangedTmmEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task UseSubcategoriesChanged(bool value) + { + UseSubcategories = value; + UpdatePreferences.UseSubcategories = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task SavePathChanged(string value) + { + SavePath = value; + UpdatePreferences.SavePath = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task TempPathEnabledChanged(bool value) + { + TempPathEnabled = value; + UpdatePreferences.TempPathEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task TempPathChanged(string value) + { + TempPath = value; + UpdatePreferences.TempPath = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected void ExportDirEnabledChanged(bool value) + { + ExportDirEnabled = value; + } + + protected async Task ExportDirChanged(string value) + { + ExportDir = value; + UpdatePreferences.ExportDir = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected void ExportDirFinEnabledChanged(bool value) + { + ExportDirFinEnabled = value; + } + + protected async Task ExportDirFinChanged(string value) + { + ExportDirFin = value; + UpdatePreferences.ExportDirFin = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ScanDirsKeyChanged(string key, string value) + { + if (ScanDirs.Remove(key, out var location)) + { + ScanDirs[value] = location; + } + UpdatePreferences.ScanDirs = ScanDirs; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ScanDirsValueChanged(string key, string value) + { + ScanDirs[key] = SaveLocation.Create(value); + UpdatePreferences.ScanDirs = ScanDirs; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task AddedScanDirsKeyChanged(int index, string key) + { + if (key == "") + { + return; + } + + ScanDirs.Add(key, AddedScanDirs[index].Value); + AddedScanDirs.RemoveAt(index); + UpdatePreferences.ScanDirs = ScanDirs; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + + if (AddedScanDirs.Count == 0) + { + AddDefaultScanDir(); + } + } + + protected void AddedScanDirsValueChanged(int index, string value) + { + var existing = AddedScanDirs[index]; + AddedScanDirs[index] = new KeyValuePair(existing.Key, SaveLocation.Create(value)); + } + + protected void AddNewScanDir() + { + AddDefaultScanDir(); + } + + protected async Task ExcludedFileNamesEnabledChanged(bool value) + { + ExcludedFileNamesEnabled = value; + UpdatePreferences.ExcludedFileNamesEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task ExcludedFileNamesChanged(string value) + { + ExcludedFileNames = value; + UpdatePreferences.ExcludedFileNames = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task MailNotificationEnabledChanged(bool value) + { + MailNotificationEnabled = value; + UpdatePreferences.MailNotificationEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task MailNotificationSenderChanged(string value) + { + MailNotificationSender = value; + UpdatePreferences.MailNotificationSender = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task MailNotificationEmailChanged(string value) + { + MailNotificationEmail = value; + UpdatePreferences.MailNotificationEmail = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task MailNotificationSmtpChanged(string value) + { + MailNotificationSmtp = value; + UpdatePreferences.MailNotificationSmtp = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task MailNotificationSslEnabledChanged(bool value) + { + MailNotificationSslEnabled = value; + UpdatePreferences.MailNotificationSslEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task MailNotificationAuthEnabledChanged(bool value) + { + MailNotificationAuthEnabled = value; + UpdatePreferences.MailNotificationAuthEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task MailNotificationUsernameChanged(string value) + { + MailNotificationUsername = value; + UpdatePreferences.MailNotificationUsername = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task MailNotificationPasswordChanged(string value) + { + MailNotificationPassword = value; + UpdatePreferences.MailNotificationPassword = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task AutorunOnTorrentAddedEnabledChanged(bool value) + { + AutorunOnTorrentAddedEnabled = value; + UpdatePreferences.AutorunOnTorrentAddedEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task AutorunOnTorrentAddedProgramChanged(string value) + { + AutorunOnTorrentAddedProgram = value; + UpdatePreferences.AutorunOnTorrentAddedProgram = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task AutorunEnabledChanged(bool value) + { + AutorunEnabled = value; + UpdatePreferences.AutorunEnabled = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + protected async Task AutorunProgramChanged(string value) + { + AutorunProgram = value; + UpdatePreferences.AutorunProgram = value; + await PreferencesChanged.InvokeAsync(UpdatePreferences); + } + + private void AddDefaultScanDir() + { + AddedScanDirs.Add(new KeyValuePair("", SaveLocation.Create(1))); + } + + protected Func IsValidNewKey => IsValidNewKeyFunc; + + private string? IsValidNewKeyFunc(string? key) + { + if (key is null) + { + return null; + } + if (ScanDirs.ContainsKey(key)) + { + return "A folder with this path already exists"; + } + + return null; + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/Options.cs b/Lantean.QBTMudBlade/Components/Options/Options.cs new file mode 100644 index 0000000..690e495 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/Options.cs @@ -0,0 +1,38 @@ +using Lantean.QBitTorrentClient.Models; +using Microsoft.AspNetCore.Components; + +namespace Lantean.QBTMudBlade.Components.Options +{ + public abstract class Options : ComponentBase + { + private bool _preferencesRead; + protected UpdatePreferences UpdatePreferences { get; set; } = new UpdatePreferences(); + + [Parameter] + [EditorRequired] + public Preferences? Preferences { get; set; } + + [Parameter] + [EditorRequired] + public EventCallback PreferencesChanged { get; set; } + + public async Task ResetAsync() + { + SetOptions(); + + await InvokeAsync(StateHasChanged); + } + + protected override void OnParametersSet() + { + if (_preferencesRead) + { + return; + } + + _preferencesRead = SetOptions(); + } + + protected abstract bool SetOptions(); + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor b/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor new file mode 100644 index 0000000..67b5042 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor @@ -0,0 +1,14 @@ +@inherits Options + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor.cs new file mode 100644 index 0000000..54a1bff --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor.cs @@ -0,0 +1,10 @@ +namespace Lantean.QBTMudBlade.Components.Options +{ + public partial class RSSOptions : Options + { + protected override bool SetOptions() + { + return true; + } + } +} diff --git a/Lantean.QBTMudBlade/Components/Options/SpeedOptions.razor b/Lantean.QBTMudBlade/Components/Options/SpeedOptions.razor new file mode 100644 index 0000000..67b5042 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/SpeedOptions.razor @@ -0,0 +1,14 @@ +@inherits Options + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/SpeedOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/SpeedOptions.razor.cs new file mode 100644 index 0000000..df5ec5a --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/SpeedOptions.razor.cs @@ -0,0 +1,10 @@ +namespace Lantean.QBTMudBlade.Components.Options +{ + public partial class SpeedOptions : Options + { + protected override bool SetOptions() + { + return true; + } + } +} diff --git a/Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor b/Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor new file mode 100644 index 0000000..67b5042 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor @@ -0,0 +1,14 @@ +@inherits Options + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor.cs new file mode 100644 index 0000000..789b07d --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor.cs @@ -0,0 +1,10 @@ +namespace Lantean.QBTMudBlade.Components.Options +{ + public partial class WebUIOptions : Options + { + protected override bool SetOptions() + { + return true; + } + } +} diff --git a/Lantean.QBTMudBlade/Components/PeersTab.razor b/Lantean.QBTMudBlade/Components/PeersTab.razor new file mode 100644 index 0000000..c920a4a --- /dev/null +++ b/Lantean.QBTMudBlade/Components/PeersTab.razor @@ -0,0 +1,32 @@ + + + Country/Region + IP + Port + Connection + Flags + Client + Progress + Download Speed + Upload Speed + Downloaded + Uploaded + Relevance + Files + + + + @context.IPAddress + @context.Port + @context.Connection + @context.Flags + @context.Client + @DisplayHelpers.Percentage(context.Progress) + @DisplayHelpers.Speed(context.DownloadSpeed) + @DisplayHelpers.Speed(context.UploadSpeed) + @DisplayHelpers.Size(context.Downloaded) + @DisplayHelpers.Size(context.Uploaded) + @DisplayHelpers.Percentage(context.Relevance) + @context.Files + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/PeersTab.razor.cs b/Lantean.QBTMudBlade/Components/PeersTab.razor.cs new file mode 100644 index 0000000..fbd6b12 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/PeersTab.razor.cs @@ -0,0 +1,119 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Models; +using Lantean.QBTMudBlade.Services; +using Microsoft.AspNetCore.Components; +using System.Net; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class PeersTab : IAsyncDisposable + { + private bool _disposedValue; + + [Parameter, EditorRequired] + public string? Hash { get; set; } + + [Parameter] + public bool Active { get; set; } + + [CascadingParameter] + public int RefreshInterval { get; set; } + + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDataManager DataManager { get; set; } = default!; + + protected PeerList? PeerList { get; set; } + + protected IEnumerable Peers => PeerList?.Peers.Select(p => p.Value) ?? []; + + private int _requestId = 0; + private readonly CancellationTokenSource _timerCancellationToken = new(); + + protected override async Task OnParametersSetAsync() + { + if (Hash is null) + { + return; + } + + if (!Active) + { + return; + } + + var torrentPeers = await ApiClient.GetTorrentPeersData(Hash, _requestId); + PeerList = DataManager.CreatePeerList(torrentPeers); + _requestId = torrentPeers.RequestId; + + await InvokeAsync(StateHasChanged); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval))) + { + while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync()) + { + if (Hash is null) + { + return; + } + + if (Active) + { + QBitTorrentClient.Models.TorrentPeers peers; + try + { + peers = await ApiClient.GetTorrentPeersData(Hash, _requestId); + } + catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden) + { + _timerCancellationToken.CancelIfNotDisposed(); + return; + } + if (PeerList is null || peers.FullUpdate) + { + PeerList = DataManager.CreatePeerList(peers); + } + else + { + DataManager.MergeTorrentPeers(peers, PeerList); + } + + _requestId = peers.RequestId; + } + await InvokeAsync(StateHasChanged); + } + } + } + } + + protected virtual async Task DisposeAsync(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _timerCancellationToken.Cancel(); + _timerCancellationToken.Dispose(); + + await Task.Delay(0); + } + + _disposedValue = true; + } + } + + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/TorrentActions.razor b/Lantean.QBTMudBlade/Components/TorrentActions.razor new file mode 100644 index 0000000..3dfdb28 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/TorrentActions.razor @@ -0,0 +1,88 @@ +@if (Type == ParentType.StandaloneToolbar) +{ + + @ToolbarContent + +} +else if (Type == ParentType.Toolbar) +{ + @ToolbarContent +} +else +{ + + @foreach (var option in GetOptions()) + { + + @if (option is Divider) + { + + } + else if (!option.Children.Any()) + { + @option.Name + } + else + { + + + @option.Name + + + + @foreach (var childItem in option.Children) + { + @ChildItem(childItem) + } + + + } + + } + +} + +@code { + private RenderFragment ToolbarContent => + @ + @foreach (var option in GetOptions()) + { + @if (option is Divider) + { + + } + else if (!option.Children.Any()) + { + if (option.Icon is null) + { + @option.Name + } + else + { + + } + } + else + { + + @foreach (var childItem in option.Children) + { + @ChildItem(childItem) + } + + } + } + ; + + private RenderFragment ChildItem(Action option) => + @ + @if (option is Divider) + { + + } + else + { + @option.Name + } + ; +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs b/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs new file mode 100644 index 0000000..e6b4d30 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs @@ -0,0 +1,325 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Models; +using Lantean.QBTMudBlade.Services; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class TorrentActions + { + [Inject] + public IApiClient ApiClient { get; set; } = default!; + + [Inject] + public NavigationManager NavigationManager { get; set; } = default!; + + [Inject] + public IDialogService DialogService { get; set; } = default!; + + [Inject] + public ISnackbar Snackbar { get; set; } = default!; + + [Inject] + public IDataManager DataManager { get; set; } = default!; + + [Inject] + public IClipboardService ClipboardService { get; set; } = default!; + + [Parameter] + [EditorRequired] + public IEnumerable Hashes { get; set; } = default!; + + /// + /// If true this component will render as a otherwise will render as a . + /// + [Parameter] + public ParentType Type { get; set; } + + [CascadingParameter] + public MainData MainData { get; set; } = default!; + + protected async Task Pause() + { + await ApiClient.PauseTorrents(Hashes); + Snackbar.Add("Torrent paused."); + } + + protected async Task Resume() + { + await ApiClient.ResumeTorrents(Hashes); + Snackbar.Add("Torrent resumed."); + } + + protected async Task Remove() + { + await DialogService.InvokeDeleteTorrentDialog(ApiClient, Hashes.ToArray()); + } + + protected async Task SetLocation() + { + string? savePath = null; + if (Hashes.Any() && MainData.Torrents.TryGetValue(Hashes.First(), out var torrent)) + { + savePath = torrent.SavePath; + } + await DialogService.ShowSingleFieldDialog("Set Location", "Location", savePath, v => ApiClient.SetTorrentLocation(v, null, Hashes.ToArray())); + } + + protected async Task Rename() + { + string? name = null; + string hash = Hashes.First(); + if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent)) + { + name = torrent.Name; + } + await DialogService.ShowSingleFieldDialog("Rename", "Location", name, v => ApiClient.SetTorrentName(v, hash)); + } + + protected async Task RenameFiles() + { + await DialogService.InvokeRenameFilesDialog(ApiClient, Hashes.First()); + } + + protected async Task SetCategory(string category) + { + await ApiClient.SetTorrentCategory(category, null, Hashes.ToArray()); + } + + protected async Task AddCategory() + { + await DialogService.InvokeAddCategoryDialog(ApiClient, Hashes); + } + + protected async Task ResetCategory() + { + await ApiClient.SetTorrentCategory("", null, Hashes.ToArray()); + } + + protected async Task AddTag() + { + await DialogService.ShowSingleFieldDialog("Add Tags", "Comma-separated tags", "", v => ApiClient.AddTorrentTags(v.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries), null, Hashes.ToArray())); + } + + protected async Task RemoveTags() + { + var torrents = GetTorrents(); + + foreach (var torrent in torrents) + { + await ApiClient.RemoveTorrentTags(torrent.Tags, null, torrent.Hash); + } + } + + protected async Task ToggleTag(string tag) + { + var torrents = GetTorrents(); + + await ApiClient.RemoveTorrentTag(tag, torrents.Where(t => t.Tags.Contains(tag)).Select(t => t.Hash)); + await ApiClient.AddTorrentTag(tag, torrents.Where(t => !t.Tags.Contains(tag)).Select(t => t.Hash)); + } + + protected async Task ToggleAutoTMM() + { + var torrents = GetTorrents(); + + await ApiClient.SetAutomaticTorrentManagement(false, null, torrents.Where(t => t.AutomaticTorrentManagement).Select(t => t.Hash).ToArray()); + await ApiClient.SetAutomaticTorrentManagement(true, null, torrents.Where(t => !t.AutomaticTorrentManagement).Select(t => t.Hash).ToArray()); + } + + protected async Task LimitUploadRate() + { + long uploadLimit = -1; + string hash = Hashes.First(); + if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent)) + { + uploadLimit = torrent.UploadLimit; + } + + await DialogService.InvokeUploadRateDialog(ApiClient, uploadLimit, Hashes); + } + + protected async Task LimitShareRatio() + { + float ratioLimit = -1; + string hash = Hashes.First(); + if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent)) + { + ratioLimit = torrent.RatioLimit; + } + + await DialogService.InvokeShareRatioDialog(ApiClient, ratioLimit, Hashes); + } + + protected async Task ToggleSuperSeeding() + { + var torrents = GetTorrents(); + + await ApiClient.SetSuperSeeding(false, null, torrents.Where(t => t.SuperSeeding).Select(t => t.Hash).ToArray()); + await ApiClient.SetSuperSeeding(true, null, torrents.Where(t => !t.SuperSeeding).Select(t => t.Hash).ToArray()); + } + + protected async Task ForceRecheck() + { + await ApiClient.RecheckTorrents(null, Hashes.ToArray()); + } + + protected async Task ForceReannounce() + { + await ApiClient.ReannounceTorrents(null, Hashes.ToArray()); + } + + protected async Task MoveToTop() + { + await ApiClient.MaximalTorrentPriority(null, Hashes.ToArray()); + } + + protected async Task MoveUp() + { + await ApiClient.IncreaseTorrentPriority(null, Hashes.ToArray()); + } + + protected async Task MoveDown() + { + await ApiClient.DecreaseTorrentPriority(null, Hashes.ToArray()); + } + + protected async Task MoveToBottom() + { + await ApiClient.MinimalTorrentPriority(null, Hashes.ToArray()); + } + + protected async Task Copy(string value) + { + await ClipboardService.WriteToClipboard(value); + } + + protected async Task Export() + { + await Task.Delay(5); + } + + private IEnumerable GetTorrents() + { + foreach (var hash in Hashes) + { + if (MainData.Torrents.TryGetValue(hash, out var torrent)) + { + yield return torrent; + } + } + } + + private IEnumerable GetOptions() + { + if (!Hashes.Any()) + { + return []; + } + + var firstTorrent = MainData.Torrents[Hashes.First()]; + + var categories = new List + { + new Action("New", Icons.Material.Filled.Add, Color.Info, EventCallback.Factory.Create(this, AddCategory)), + new Action("Reset", Icons.Material.Filled.Remove, Color.Error, EventCallback.Factory.Create(this, ResetCategory)), + new Divider() + }; + categories.AddRange(MainData.Categories.Select(c => new Action(c.Value.Name, Icons.Material.Filled.List, Color.Info, EventCallback.Factory.Create(this, () => SetCategory(c.Key))))); + + var tags = new List + { + new Action("Add", Icons.Material.Filled.Add, Color.Info, EventCallback.Factory.Create(this, AddTag)), + new Action("Remove All", Icons.Material.Filled.Remove, Color.Error, EventCallback.Factory.Create(this, RemoveTags)), + new Divider() + }; + tags.AddRange(MainData.Tags.Select(t => new Action(t, firstTorrent.Tags.Contains(t) ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank, Color.Default, EventCallback.Factory.Create(this, () => ToggleTag(t))))); + + var options = new List + { + new Action("Pause", Icons.Material.Filled.Pause, Color.Warning, EventCallback.Factory.Create(this, Pause)), + new Action("Resume", Icons.Material.Filled.PlayArrow, Color.Success, EventCallback.Factory.Create(this, Resume)), + new Divider(), + new Action("Remove", Icons.Material.Filled.Delete, Color.Error, EventCallback.Factory.Create(this, Remove)), + new Divider(), + new Action("Set location", Icons.Material.Filled.MyLocation, Color.Info, EventCallback.Factory.Create(this, SetLocation)), + new Action("Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, EventCallback.Factory.Create(this, Rename)), + new Action("Category", Icons.Material.Filled.List, Color.Info, categories), + new Action("Tags", Icons.Material.Filled.Label, Color.Info, tags), + new Action("Automatic Torrent Management", Icons.Material.Filled.Check, firstTorrent.AutomaticTorrentManagement ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleAutoTMM)), + new Divider(), + new Action("Limit upload rate", Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info, EventCallback.Factory.Create(this, LimitUploadRate)), + new Action("Limit share ratio", Icons.Material.Filled.Percent, Color.Warning, EventCallback.Factory.Create(this, LimitShareRatio)), + new Action("Super seeding mode", Icons.Material.Filled.Check, firstTorrent.SuperSeeding ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleSuperSeeding)), + new Divider(), + new Action("Force recheck", Icons.Material.Filled.Loop, Color.Info, EventCallback.Factory.Create(this, ForceRecheck)), + new Action("Force reannounce", Icons.Material.Filled.BroadcastOnHome, Color.Info, EventCallback.Factory.Create(this, ForceReannounce)), + new Divider(), + new Action("Queue", Icons.Material.Filled.Queue, Color.Transparent, new List + { + new Action("Move to top", Icons.Material.Filled.VerticalAlignTop, Color.Inherit, EventCallback.Factory.Create(this, MoveToTop)), + new Action("Move up", Icons.Material.Filled.ArrowUpward, Color.Inherit, EventCallback.Factory.Create(this, MoveUp)), + new Action("Move down", Icons.Material.Filled.ArrowDownward, Color.Inherit, EventCallback.Factory.Create(this, MoveDown)), + new Action("Move to bottom", Icons.Material.Filled.VerticalAlignBottom, Color.Inherit, EventCallback.Factory.Create(this, MoveToBottom)), + }), + new Action("Copy", Icons.Material.Filled.FolderCopy, Color.Info, new List + { + new Action("Name", Icons.Material.Filled.TextFields, Color.Info, EventCallback.Factory.Create(this, () => Copy(firstTorrent.Name))), + new Action("Info hash v1", Icons.Material.Filled.Tag, Color.Info, EventCallback.Factory.Create(this, () => Copy(firstTorrent.InfoHashV1))), + new Action("Info hash v2", Icons.Material.Filled.Tag, Color.Info, EventCallback.Factory.Create(this, () => Copy(firstTorrent.InfoHashV2))), + new Action("Magnet link", Icons.Material.Filled.TextFields, Color.Info, EventCallback.Factory.Create(this, () => Copy(firstTorrent.MagnetUri))), + new Action("Torrent ID", Icons.Material.Filled.TextFields, Color.Info, EventCallback.Factory.Create(this, () => Copy(firstTorrent.Hash))), + }), + new Action("Export", Icons.Material.Filled.SaveAlt, Color.Info, EventCallback.Factory.Create(this, Export)), + }; + + return options; + } + } + + public enum ParentType + { + Toolbar, + StandaloneToolbar, + Menu, + } + + public class Divider : Action + { + public Divider() : base("-", default!, Color.Default, default(EventCallback)) + { + } + } + + public class Action + { + public Action(string name, string icon, Color color, EventCallback callback) + { + Name = name; + Icon = icon; + Color = color; + Callback = callback; + Children = []; + } + + public Action(string name, string icon, Color color, IEnumerable children) + { + Name = name; + Icon = icon; + Color = color; + Callback = default; + Children = children; + } + + public string Name { get; } + + public string Icon { get; } + + public Color Color { get; } + + public EventCallback Callback { get; } + + public IEnumerable Children { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/TorrentsListNav.razor b/Lantean.QBTMudBlade/Components/TorrentsListNav.razor new file mode 100644 index 0000000..80ac49f --- /dev/null +++ b/Lantean.QBTMudBlade/Components/TorrentsListNav.razor @@ -0,0 +1,18 @@ + + Back + + @if (Torrents is null) + { + @for (var i = 0; i < 10; i++) + { + + } + } + else + { + foreach (var torrent in Torrents) + { + @torrent.Name + } + } + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/TorrentsListNav.razor.cs b/Lantean.QBTMudBlade/Components/TorrentsListNav.razor.cs new file mode 100644 index 0000000..bb228a4 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/TorrentsListNav.razor.cs @@ -0,0 +1,22 @@ +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class TorrentsListNav + { + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [Parameter] + public IEnumerable? Torrents { get; set; } + + [Parameter] + public string? SelectedTorrent { get; set; } + + protected void NavigateBack() + { + NavigationManager.NavigateTo("/"); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/TrackersTab.razor b/Lantean.QBTMudBlade/Components/TrackersTab.razor new file mode 100644 index 0000000..6e3174e --- /dev/null +++ b/Lantean.QBTMudBlade/Components/TrackersTab.razor @@ -0,0 +1,27 @@ + + + Tier + URL + Status + Peers + Seeds + Leeches + Times Downloaded + Message + + + + @if (context.Tier >= 0) + { + @context.Tier + } + + @context.Url + @context.Status + @context.Peers + @context.Seeds + @context.Leeches + @context.Downloads + @context.Message + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs b/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs new file mode 100644 index 0000000..e98f51c --- /dev/null +++ b/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs @@ -0,0 +1,97 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBitTorrentClient.Models; +using Lantean.QBTMudBlade.Services; +using Microsoft.AspNetCore.Components; +using System.Net; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class TrackersTab : IAsyncDisposable + { + private readonly CancellationTokenSource _timerCancellationToken = new(); + private bool _disposedValue; + + [Parameter, EditorRequired] + public string? Hash { get; set; } + + [Parameter] + public bool Active { get; set; } + + [CascadingParameter] + public int RefreshInterval { get; set; } + + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDataManager DataManager { get; set; } = default!; + + protected IReadOnlyList? Trackers { get; set; } + + protected override async Task OnParametersSetAsync() + { + if (Hash is null) + { + return; + } + + if (!Active) + { + return; + } + + Trackers = await ApiClient.GetTorrentTrackers(Hash); + + await InvokeAsync(StateHasChanged); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval))) + { + while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync()) + { + if (Active && Hash is not null) + { + try + { + Trackers = await ApiClient.GetTorrentTrackers(Hash); + } + catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden) + { + _timerCancellationToken.CancelIfNotDisposed(); + return; + } + } + + await InvokeAsync(StateHasChanged); + } + } + } + } + + protected virtual async Task DisposeAsync(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _timerCancellationToken.Cancel(); + _timerCancellationToken.Dispose(); + await Task.Delay(0); + } + + _disposedValue = true; + } + } + + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/WebSeedsTab.razor b/Lantean.QBTMudBlade/Components/WebSeedsTab.razor new file mode 100644 index 0000000..fc88514 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/WebSeedsTab.razor @@ -0,0 +1,8 @@ + + + URL + + + @context.Url + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs b/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs new file mode 100644 index 0000000..c505102 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs @@ -0,0 +1,92 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBitTorrentClient.Models; +using Lantean.QBTMudBlade.Services; +using Microsoft.AspNetCore.Components; +using System.Net; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class WebSeedsTab : IAsyncDisposable + { + private readonly CancellationTokenSource _timerCancellationToken = new(); + private bool _disposedValue; + + [Parameter] + public bool Active { get; set; } + + [Parameter, EditorRequired] + public string? Hash { get; set; } + + [CascadingParameter] + public int RefreshInterval { get; set; } + + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDataManager DataManager { get; set; } = default!; + + protected IReadOnlyList? WebSeeds { get; set; } + + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual async Task DisposeAsync(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _timerCancellationToken.Cancel(); + _timerCancellationToken.Dispose(); + await Task.Delay(0); + } + + _disposedValue = true; + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval))) + { + while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync()) + { + if (Active && Hash is not null) + { + try + { + WebSeeds = await ApiClient.GetTorrentWebSeeds(Hash); + } + catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden) + { + _timerCancellationToken.CancelIfNotDisposed(); + return; + } + } + + await InvokeAsync(StateHasChanged); + } + } + } + } + + protected override async Task OnParametersSetAsync() + { + if (Hash is null) + { + return; + } + + WebSeeds = await ApiClient.GetTorrentWebSeeds(Hash); + + await InvokeAsync(StateHasChanged); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/CookieHandler.cs b/Lantean.QBTMudBlade/CookieHandler.cs new file mode 100644 index 0000000..9eb8f3e --- /dev/null +++ b/Lantean.QBTMudBlade/CookieHandler.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Http; + +namespace Lantean.QBTMudBlade +{ + public class CookieHandler : DelegatingHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); + + return await base.SendAsync(request, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/CustomIcons.cs b/Lantean.QBTMudBlade/CustomIcons.cs new file mode 100644 index 0000000..7c4350b --- /dev/null +++ b/Lantean.QBTMudBlade/CustomIcons.cs @@ -0,0 +1,7 @@ +namespace Lantean.QBTMudBlade +{ + public static class CustomIcons + { + public const string Magnet = @""; + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/DialogHelper.cs b/Lantean.QBTMudBlade/DialogHelper.cs new file mode 100644 index 0000000..6fa4393 --- /dev/null +++ b/Lantean.QBTMudBlade/DialogHelper.cs @@ -0,0 +1,229 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Components.Dialogs; +using Lantean.QBTMudBlade.Filter; +using Lantean.QBTMudBlade.Models; +using MudBlazor; + +namespace Lantean.QBTMudBlade +{ + public static class DialogHelper + { + public static readonly DialogOptions FormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, ClassBackground = "background-blur" }; + + private static readonly DialogOptions _confirmDialogOptions = new() { ClassBackground = "background-blur" }; + + public static async Task InvokeAddTorrentFileDialog(this IDialogService dialogService, IApiClient apiClient) + { + var result = await dialogService.ShowAsync("Upload local torrent", FormDialogOptions); + var dialogResult = await result.Result; + if (dialogResult.Canceled) + { + return; + } + + var options = (AddTorrentFileOptions)dialogResult.Data; + + var streams = new List(); + + var files = new Dictionary(); + foreach (var file in options.Files) + { + var stream = file.OpenReadStream(); + streams.Add(stream); + files.Add(file.Name, stream); + } + + await apiClient.AddTorrent( + urls: null, + files, + options.SavePath, + options.Cookie, + options.Category, + tags: null, + options.SkipHashCheck, + !options.StartTorrent, + options.ContentLayout, + options.RenameTorrent, + options.UploadLimit, + options.DownloadLimit, + ratioLimit: null, + seedingTimeLimit: null, + options.TorrentManagementMode, + options.DownloadInSequentialOrder, + options.DownloadFirstAndLastPiecesFirst); + + foreach (var stream in streams) + { + await stream.DisposeAsync(); + } + } + + public static async Task InvokeAddTorrentLinkDialog(this IDialogService dialogService, IApiClient apiClient) + { + var result = await dialogService.ShowAsync("Download from URLs", FormDialogOptions); + var dialogResult = await result.Result; + if (dialogResult.Canceled) + { + return; + } + + var options = (AddTorrentLinkOptions)dialogResult.Data; + + await apiClient.AddTorrent( + urls: options.Urls, + torrents: null, + options.SavePath, + options.Cookie, + options.Category, + tags: null, + options.SkipHashCheck, + !options.StartTorrent, + options.ContentLayout, + options.RenameTorrent, + options.UploadLimit, + options.DownloadLimit, + ratioLimit: null, + seedingTimeLimit: null, + options.TorrentManagementMode, + options.DownloadInSequentialOrder, + options.DownloadFirstAndLastPiecesFirst); + } + + public static async Task InvokeDeleteTorrentDialog(this IDialogService dialogService, IApiClient apiClient, params string[] hashes) + { + var reference = await dialogService.ShowAsync($"Remove torrent{(hashes.Length == 1 ? "" : "s")}?"); + var result = await reference.Result; + if (result.Canceled) + { + return; + } + + await apiClient.DeleteTorrents(hashes, (bool)result.Data); + } + + public static async Task InvokeRenameFilesDialog(this IDialogService dialogService, IApiClient apiClient, string hash) + { + await Task.Delay(0); + } + + public static async Task InvokeAddCategoryDialog(this IDialogService dialogService, IApiClient apiClient, IEnumerable? hashes = null) + { + var reference = await dialogService.ShowAsync("New Category"); + var result = await reference.Result; + if (result.Canceled) + { + return; + } + + var category = (Category)result.Data; + + await apiClient.AddCategory(category.Name, category.SavePath); + + if (hashes is not null) + { + await apiClient.SetTorrentCategory(category.Name, null, hashes.ToArray()); + } + } + + public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Func onSuccess) + { + var parameters = new DialogParameters + { + { nameof(ConfirmDialog.Content), content } + }; + var result = await dialogService.ShowAsync(title, parameters, _confirmDialogOptions); + + var dialogResult = await result.Result; + if (dialogResult.Canceled) + { + return; + } + + await onSuccess(); + } + + public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Action onSuccess) + { + await ShowConfirmDialog(dialogService, title, content, () => + { + onSuccess(); + + return Task.CompletedTask; + }); + } + + public static async Task ShowSingleFieldDialog(this IDialogService dialogService, string title, string label, T? value, Func onSuccess) + { + var parameters = new DialogParameters + { + { nameof(SingleFieldDialog.Label), label }, + { nameof(SingleFieldDialog.Value), value } + }; + var result = await dialogService.ShowAsync>(title, parameters, _confirmDialogOptions); + + var dialogResult = await result.Result; + if (dialogResult.Canceled) + { + return; + } + + await onSuccess((T)dialogResult.Data); + } + + public static async Task InvokeUploadRateDialog(this IDialogService dialogService, IApiClient apiClient, long rate, IEnumerable hashes) + { + var parameters = new DialogParameters + { + { nameof(SliderFieldDialog.Value), rate }, + { nameof(SliderFieldDialog.Min), 0 }, + { nameof(SliderFieldDialog.Max), 100 }, + }; + var result = await dialogService.ShowAsync>("Upload Rate", parameters, FormDialogOptions); + + var dialogResult = await result.Result; + if (dialogResult.Canceled) + { + return; + } + + await apiClient.SetTorrentUploadLimit((long)dialogResult.Data, null, hashes.ToArray()); + } + + public static async Task InvokeShareRatioDialog(this IDialogService dialogService, IApiClient apiClient, float ratio, IEnumerable hashes) + { + var parameters = new DialogParameters + { + { nameof(SliderFieldDialog.Value), ratio }, + { nameof(SliderFieldDialog.Min), 0 }, + { nameof(SliderFieldDialog.Max), 100 }, + }; + var result = await dialogService.ShowAsync>("Upload Rate", parameters, FormDialogOptions); + + var dialogResult = await result.Result; + if (dialogResult.Canceled) + { + return; + } + + await apiClient.SetTorrentShareLimit((float)dialogResult.Data, 0, null, hashes.ToArray()); + } + + public static async Task>?> ShowFilterOptionsDialog(this IDialogService dialogService, List>? propertyFilterDefinitions) + { + var parameters = new DialogParameters + { + { nameof(FilterOptionsDialog.FilterDefinitions), propertyFilterDefinitions }, + }; + + var result = await dialogService.ShowAsync>("Filters", parameters, FormDialogOptions); + + var dialogResult = await result.Result; + if (dialogResult.Canceled) + { + return null; + } + + return (List>?)dialogResult.Data; + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/DisplayHelpers.cs b/Lantean.QBTMudBlade/DisplayHelpers.cs new file mode 100644 index 0000000..7f0ccdc --- /dev/null +++ b/Lantean.QBTMudBlade/DisplayHelpers.cs @@ -0,0 +1,379 @@ +using ByteSizeLib; +using Lantean.QBTMudBlade.Models; +using MudBlazor; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; + +namespace Lantean.QBTMudBlade +{ + public static class DisplayHelpers + { + /// + /// Formats a time period in seconds into an appropriate unit based on the size. + /// + /// + /// + /// + /// + public static string Duration(long? seconds, string? prefix = null, string? suffix = null) + { + if (seconds is null) + { + return ""; + } + + if (seconds == 8640000) + { + return "∞"; + } + + var time = TimeSpan.FromSeconds(seconds.Value); + var sb = new StringBuilder(); + if (prefix is not null) + { + sb.Append(prefix); + } + if (time.Days > 0) + { + sb.Append(time.Days); + sb.Append('d'); + + if (time.Hours != 0) + { + sb.Append(' '); + sb.Append(time.Hours); + sb.Append('h'); + } + } + else if (time.Hours > 0) + { + sb.Append(time.Hours); + sb.Append('h'); + + if (time.Minutes != 0) + { + sb.Append(' '); + sb.Append(time.Minutes); + sb.Append('m'); + } + } + else + { + sb.Append(time.Minutes); + sb.Append('m'); + } + if (suffix is not null) + { + sb.Append(' '); + sb.Append(suffix); + } + return sb.ToString(); + } + + /// + /// Formats a file size in bytes into an appropriate unit based on the size. + /// + /// + /// + /// + /// + public static string Size(long? size, string? prefix = null, string? suffix = null) + { + if (size is null) + { + return ""; + } + + var stringBuilder = new StringBuilder(); + if (prefix is not null) + { + stringBuilder.Append(prefix); + } + stringBuilder.Append(ByteSize.FromBytes(size.Value).ToString("#.##")); + if (suffix is not null) + { + stringBuilder.Append(suffix); + } + return stringBuilder.ToString(); + } + + /// + /// Formats a file size in bytes into an appropriate unit based on the size. + /// + /// + /// + /// + /// + public static string Size(object? sizeValue, string? prefix = null, string? suffix = null) + { + if (sizeValue is not long size) + { + return ""; + } + + return Size(size); + } + + /// + /// Formats a transfer speed in bytes/s into an appropriate unit based on the size. + /// + /// + /// + /// + /// + public static string Speed(long? size, string? prefix = null, string? suffix = null) + { + if (size is null) + { + return ""; + } + + if (size == -1) + { + return "∞"; + } + + var stringBuilder = new StringBuilder(); + if (prefix is not null) + { + stringBuilder.Append(prefix); + } + stringBuilder.Append(ByteSize.FromBytes(size.Value).ToString("#.##")); + stringBuilder.Append("/s"); + if (suffix is not null) + { + stringBuilder.Append(suffix); + } + return stringBuilder.ToString(); + } + + /// + /// Formats a value into an empty string if null, otherwise the value. + /// + /// + /// + /// + /// + /// + public static string EmptyIfNull(T? value, string? prefix = null, string? suffix = null, [StringSyntax("NumericFormat")] string? format = null) where T : struct, IConvertible + { + if (value is null) + { + return ""; + } + + var stringBuilder = new StringBuilder(); + if (prefix is not null) + { + stringBuilder.Append(prefix); + } + + if (format is not null) + { + if (value is long longValue) + { + stringBuilder.Append(longValue.ToString(format)); + } + else if (value is int intValue) + { + stringBuilder.Append(intValue.ToString(format)); + } + else if (value is float floatValue) + { + stringBuilder.Append(floatValue.ToString(format)); + } + else if (value is double doubleValue) + { + stringBuilder.Append(doubleValue.ToString(format)); + } + else if (value is decimal decimalValue) + { + stringBuilder.Append(decimalValue.ToString(format)); + } + else if (value is short shortValue) + { + stringBuilder.Append(shortValue.ToString(format)); + } + else + { + stringBuilder.Append(value.Value); + } + } + else + { + stringBuilder.Append(value.Value); + } + + if (suffix is not null) + { + stringBuilder.Append(suffix); + } + return stringBuilder.ToString(); + } + + /// + /// Formats a value into an empty string if null, otherwise the value. + /// + /// + /// + /// + /// + /// + public static string EmptyIfNull(string? value, string? prefix = null, string? suffix = null) + { + if (value is null) + { + return ""; + } + + var stringBuilder = new StringBuilder(); + if (prefix is not null) + { + stringBuilder.Append(prefix); + } + stringBuilder.Append(value); + if (suffix is not null) + { + stringBuilder.Append(suffix); + } + return stringBuilder.ToString(); + } + + /// + /// Formats a unix time (in seconds) into a local date time. + /// + /// + /// + /// + public static string DateTime(long? value, string negativeDescription = "") + { + if (value is null) + { + return ""; + } + + if (value.Value == -1) + { + return negativeDescription; + } + + var dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(value.Value); + + return dateTimeOffset.ToLocalTime().ToString(); + } + + /// + /// Formats a value into a percentage or empty string if null. + /// + /// + /// + public static string Percentage(float? value) + { + if (value is null) + { + return ""; + } + + if (value == 0) + { + return "0%"; + } + + return value.Value.ToString("0.#%"); + } + + public static string State(string? state) + { + var status = state switch + { + "downloading" => "Downloading", + "stalledDL" => "Stalled", + "metaDL" => "Downloading metadata", + "forcedMetaDL" => "[F] Downloading metadata", + "forcedDL" => "[F] Downloading", + "uploading" or "stalledUP" => "Seeding", + "forcedUP" => "[F] Seeding", + "queuedDL" or "queuedUP" => "Queued", + "checkingDL" or "checkingUP" => "Checking", + "queuedForChecking" => "Queued for checking", + "checkingResumeData" => "Checking resume data", + "pausedDL" => "Paused", + "pausedUP" => "Completed", + "moving" => "Moving", + "missingFiles" => "Missing Files", + "error" => "Errored", + _ => "Unknown", + }; + + return status; + } + + public static (string, Color) GetStateIcon(string? state) + { + switch (state) + { + case "forcedDL": + case "metaDL": + case "forcedMetaDL": + case "downloading": + return (Icons.Material.Filled.Downloading, Color.Success); + case "forcedUP": + case "uploading": + return (Icons.Material.Filled.Upload, Color.Info); + case "stalledUP": + return (Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info); + case "stalledDL": + return (Icons.Material.Filled.KeyboardDoubleArrowDown, Color.Success); + case "pausedDL": + return (Icons.Material.Filled.Pause, Color.Success); + case "pausedUP": + return (Icons.Material.Filled.Pause, Color.Info); + case "queuedDL": + case "queuedUP": + return (Icons.Material.Filled.Queue, Color.Default); + case "checkingDL": + case "checkingUP": + return (Icons.Material.Filled.Loop, Color.Info); + case "queuedForChecking": + case "checkingResumeData": + return (Icons.Material.Filled.Loop, Color.Warning); + case "moving": + return (Icons.Material.Filled.Moving, Color.Info); + case "error": + case "unknown": + case "missingFiles": + return (Icons.Material.Filled.Error, Color.Error); + default: + return (Icons.Material.Filled.QuestionMark, Color.Warning); + } + } + + public static (string, Color) GetStatusIcon(string statusValue) + { + var status = Enum.Parse(statusValue); + return GetStatusIcon(status); + } + + private static (string, Color) GetStatusIcon(Status status) + { + return status switch + { + Status.All => (Icons.Material.Filled.AllOut, Color.Warning), + 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.Active => (Icons.Material.Filled.Sort, Color.Success), + Status.Inactive => (Icons.Material.Filled.Sort, Color.Error), + Status.Stalled => (Icons.Material.Filled.Sort, Color.Info), + Status.StalledUploading => (Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info), + Status.StalledDownloading => (Icons.Material.Filled.KeyboardDoubleArrowDown, Color.Success), + Status.Checking => (Icons.Material.Filled.Loop, Color.Info), + Status.Errored => (Icons.Material.Filled.Error, Color.Error), + _ => (Icons.Material.Filled.QuestionMark, Color.Inherit), + }; + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/ExpressionModifier.cs b/Lantean.QBTMudBlade/ExpressionModifier.cs new file mode 100644 index 0000000..cd150bb --- /dev/null +++ b/Lantean.QBTMudBlade/ExpressionModifier.cs @@ -0,0 +1,154 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +namespace Lantean.QBTMudBlade +{ + internal static class ExpressionModifier + { + internal static Expression> Modify(this Expression firstExpression, Expression secondExpression) + { + var bodyIdentifier = new ExpressionBodyIdentifier(); + var body = bodyIdentifier.Identify(firstExpression); + var parameterIdentifier = new ExpressionParameterIdentifier(); + var parameter = (ParameterExpression)parameterIdentifier.Identify(firstExpression); + var body2 = bodyIdentifier.Identify(secondExpression); + var parameter2 = (ParameterExpression)parameterIdentifier.Identify(secondExpression); + + var treeModifier = new ExpressionReplacer(parameter2, body); + return Expression.Lambda>(treeModifier.Visit(body2), parameter); + } + + internal static Expression ReplaceBinary(this Expression exp, ExpressionType from, ExpressionType to) + { + var binaryReplacer = new BinaryReplacer(from, to); + return binaryReplacer.Visit(exp); + } + + public static Expression> GenerateBinary(this Expression expression, ExpressionType binaryOperation, object? value) + { + var bodyIdentifier = new ExpressionBodyIdentifier(); + var body = bodyIdentifier.Identify(expression); + var parameterIdentifier = new ExpressionParameterIdentifier(); + var parameter = (ParameterExpression)parameterIdentifier.Identify(expression); + BinaryExpression? binaryExpression; + + if (Nullable.GetUnderlyingType(body.Type) is not null) + { + // property type is nullable... + binaryExpression = Expression.MakeBinary(binaryOperation, body, Expression.Convert(Expression.Constant(value), body.Type)); + } + else + { + if (value is null) + { + // We can short circuit here because the value to be compared is null and the property type is not nullable. + return x => true; + } + + binaryExpression = Expression.MakeBinary(binaryOperation, body, Expression.Convert(Expression.Constant(value), body.Type)); + } + + return Expression.Lambda>(binaryExpression, parameter); + } + + public static Expression> ChangeExpressionReturnType(this Expression expression) + { + var bodyIdentifier = new ExpressionBodyIdentifier(); + var body = bodyIdentifier.Identify(expression); + var parameterIdentifier = new ExpressionParameterIdentifier(); + var parameter = (ParameterExpression)parameterIdentifier.Identify(expression); + + if (body.Type is U) + { + // Expression already has the right type. + return Expression.Lambda>(body, parameter); + } + + // Change parameter. + var converted = Expression.Convert(body, typeof(U)); + return Expression.Lambda>(converted, parameter); + } + + public static (Expression>, Type) CreatePropertySelector(string propertyName) + { + var type = typeof(T); + var propertyInfo = type.GetProperty(propertyName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + if (propertyInfo is null) + { + throw new InvalidOperationException($"Unable to match property {propertyName} for {type.Name}"); + } + var parameterExpression = Expression.Parameter(type); + var propertyExpression = Expression.Property(parameterExpression, propertyInfo); + var convertExpression = Expression.Convert(propertyExpression, typeof(object)); + + return (Expression.Lambda>(convertExpression, parameterExpression), propertyInfo.PropertyType); + } + } + + internal class ExpressionReplacer : ExpressionVisitor + { + private readonly Expression _from; + private readonly Expression _to; + + public ExpressionReplacer(Expression from, Expression to) + { + _from = from; + _to = to; + } + + [return: NotNullIfNotNull(nameof(node))] + public override Expression? Visit(Expression? node) + { + if (node == _from) return _to; + return base.Visit(node); + } + } + + internal class ExpressionBodyIdentifier : ExpressionVisitor + { + public Expression Identify(Expression node) + { + return base.Visit(node); + } + + protected override Expression VisitLambda(Expression node) + { + return node.Body; + } + } + + internal class ExpressionParameterIdentifier : ExpressionVisitor + { + public Expression Identify(Expression node) + { + return base.Visit(node); + } + + protected override Expression VisitLambda(Expression node) + { + return node.Parameters[0]; + } + } + + internal class BinaryReplacer : ExpressionVisitor + { + private readonly ExpressionType _from; + private readonly ExpressionType _to; + + public BinaryReplacer(ExpressionType from, ExpressionType to) + { + _from = from; + _to = to; + } + + protected override Expression VisitBinary(BinaryExpression node) + { + if (node.NodeType == _from) + { + return Expression.MakeBinary(_to, node.Left, node.Right); + } + + return base.VisitBinary(node); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Extensions.cs b/Lantean.QBTMudBlade/Extensions.cs new file mode 100644 index 0000000..a7a4c93 --- /dev/null +++ b/Lantean.QBTMudBlade/Extensions.cs @@ -0,0 +1,54 @@ +using Lantean.QBTMudBlade; +using Lantean.QBTMudBlade.Models; + +namespace Lantean.QBTMudBlade +{ + public static class Extensions + { + public const char DirectorySeparator = '/'; + + public static string GetDirectoryPath(this string pathAndFileName) + { + return string.Join(DirectorySeparator, pathAndFileName.Split(DirectorySeparator)[..^1]); + } + + public static string GetDirectoryPath(this ContentItem contentItem) + { + return contentItem.Name.GetDirectoryPath(); + } + + public static string GetFileName(this string pathAndFileName) + { + return pathAndFileName.Split(DirectorySeparator)[^1]; + } + + public static string GetFileName(this ContentItem contentItem) + { + return contentItem.Name.GetFileName(); + } + + public static string GetDescendantsKey(this string pathAndFileName, int? level = null) + { + var paths = pathAndFileName.Split(DirectorySeparator); + var index = level is null ? new Index(1, true) : new Index(level.Value); + return string.Join(DirectorySeparator, paths[0..index]) + DirectorySeparator; + } + + public static string GetDescendantsKey(this ContentItem contentItem, int? level = null) + { + return contentItem.Name.GetDescendantsKey(level); + } + + public static void CancelIfNotDisposed(this CancellationTokenSource cancellationTokenSource) + { + try + { + cancellationTokenSource.Cancel(); + } + catch (ObjectDisposedException) + { + // disposed + } + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Filter/FilterExpressionGenerator.cs b/Lantean.QBTMudBlade/Filter/FilterExpressionGenerator.cs new file mode 100644 index 0000000..e7dcdb9 --- /dev/null +++ b/Lantean.QBTMudBlade/Filter/FilterExpressionGenerator.cs @@ -0,0 +1,134 @@ +using MudBlazor; +using System.Linq.Expressions; + +namespace Lantean.QBTMudBlade.Filter +{ + public static class FilterExpressionGenerator + { + public static Expression> GenerateExpression(PropertyFilterDefinition filter, bool caseSensitive) + { + var propertyExpression = filter.Expression; + + if (propertyExpression is null) + { + return x => true; + } + + var fieldType = FieldType.Identify(filter.ColumnType); + + if (fieldType.IsString) + { + var value = filter.Value?.ToString(); + + if (value is null && filter.Operator != FilterOperator.String.Empty && filter.Operator != FilterOperator.String.NotEmpty) + { + return x => true; + } + + var stringComparer = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + + return filter.Operator switch + { + FilterOperator.String.Contains => + propertyExpression.Modify((Expression>)(x => x != null && value != null && x.Contains(value, stringComparer))), + FilterOperator.String.NotContains => + propertyExpression.Modify((Expression>)(x => x != null && value != null && !x.Contains(value, stringComparer))), + FilterOperator.String.Equal => + propertyExpression.Modify((Expression>)(x => x != null && x.Equals(value, stringComparer))), + FilterOperator.String.NotEqual => + propertyExpression.Modify((Expression>)(x => x != null && !x.Equals(value, stringComparer))), + FilterOperator.String.StartsWith => + propertyExpression.Modify((Expression>)(x => x != null && value != null && x.StartsWith(value, stringComparer))), + FilterOperator.String.EndsWith => + propertyExpression.Modify((Expression>)(x => x != null && value != null && x.EndsWith(value, stringComparer))), + FilterOperator.String.Empty => propertyExpression.Modify((Expression>)(x => string.IsNullOrWhiteSpace(x))), + FilterOperator.String.NotEmpty => propertyExpression.Modify((Expression>)(x => !string.IsNullOrWhiteSpace(x))), + _ => x => true + }; + } + + if (fieldType.IsNumber) + { + if (filter.Value is null && filter.Operator != FilterOperator.Number.Empty && filter.Operator != FilterOperator.Number.NotEmpty) + { + return x => true; + } + + return filter.Operator switch + { + FilterOperator.Number.Equal => propertyExpression.GenerateBinary(ExpressionType.Equal, filter.Value), + FilterOperator.Number.NotEqual => propertyExpression.GenerateBinary(ExpressionType.NotEqual, filter.Value), + FilterOperator.Number.GreaterThan => propertyExpression.GenerateBinary(ExpressionType.GreaterThan, filter.Value), + FilterOperator.Number.GreaterThanOrEqual => propertyExpression.GenerateBinary(ExpressionType.GreaterThanOrEqual, filter.Value), + FilterOperator.Number.LessThan => propertyExpression.GenerateBinary(ExpressionType.LessThan, filter.Value), + FilterOperator.Number.LessThanOrEqual => propertyExpression.GenerateBinary(ExpressionType.LessThanOrEqual, filter.Value), + FilterOperator.Number.Empty => propertyExpression.GenerateBinary(ExpressionType.Equal, null), + FilterOperator.Number.NotEmpty => propertyExpression.GenerateBinary(ExpressionType.NotEqual, null), + _ => x => true + }; + } + + if (fieldType.IsDateTime) + { + if (filter.Value is null && filter.Operator != FilterOperator.DateTime.Empty && filter.Operator != FilterOperator.DateTime.NotEmpty) + { + return x => true; + } + + return filter.Operator switch + { + FilterOperator.DateTime.Is => propertyExpression.GenerateBinary(ExpressionType.Equal, filter.Value), + FilterOperator.DateTime.IsNot => propertyExpression.GenerateBinary(ExpressionType.NotEqual, filter.Value), + FilterOperator.DateTime.After => propertyExpression.GenerateBinary(ExpressionType.GreaterThan, filter.Value), + FilterOperator.DateTime.OnOrAfter => propertyExpression.GenerateBinary(ExpressionType.GreaterThanOrEqual, filter.Value), + FilterOperator.DateTime.Before => propertyExpression.GenerateBinary(ExpressionType.LessThan, filter.Value), + FilterOperator.DateTime.OnOrBefore => propertyExpression.GenerateBinary(ExpressionType.LessThanOrEqual, filter.Value), + FilterOperator.DateTime.Empty => propertyExpression.GenerateBinary(ExpressionType.Equal, null), + FilterOperator.DateTime.NotEmpty => propertyExpression.GenerateBinary(ExpressionType.NotEqual, null), + _ => x => true + }; + } + + if (fieldType.IsBoolean) + { + if (filter.Value is null) + { + return x => true; + } + + return filter.Operator switch + { + FilterOperator.Boolean.Is => propertyExpression.GenerateBinary(ExpressionType.Equal, filter.Value), + _ => x => true + }; + } + + if (fieldType.IsEnum) + { + if (filter.Value is null) + { + return x => true; + } + + return filter.Operator switch + { + FilterOperator.Enum.Is => propertyExpression.GenerateBinary(ExpressionType.Equal, filter.Value), + FilterOperator.Enum.IsNot => propertyExpression.GenerateBinary(ExpressionType.NotEqual, filter.Value), + _ => x => true + }; + } + + if (fieldType.IsGuid) + { + return filter.Operator switch + { + FilterOperator.Guid.Equal => propertyExpression.GenerateBinary(ExpressionType.Equal, filter.Value), + FilterOperator.Guid.NotEqual => propertyExpression.GenerateBinary(ExpressionType.NotEqual, filter.Value), + _ => x => true + }; + } + + return x => true; + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Filter/FilterOperator.cs b/Lantean.QBTMudBlade/Filter/FilterOperator.cs new file mode 100644 index 0000000..a4ff8b5 --- /dev/null +++ b/Lantean.QBTMudBlade/Filter/FilterOperator.cs @@ -0,0 +1,137 @@ +using MudBlazor; + +namespace Lantean.QBTMudBlade.Filter +{ + public static class FilterOperator + { + public static class String + { + public const string Contains = "contains"; + public const string NotContains = "not contains"; + public const string Equal = "equals"; + public const string NotEqual = "not equals"; + public const string StartsWith = "starts with"; + public const string EndsWith = "ends with"; + public const string Empty = "is empty"; + public const string NotEmpty = "is not empty"; + } + + public static class Number + { + public const string Equal = "="; + public const string NotEqual = "!="; + public const string GreaterThan = ">"; + public const string GreaterThanOrEqual = ">="; + public const string LessThan = "<"; + public const string LessThanOrEqual = "<="; + public const string Empty = "is empty"; + public const string NotEmpty = "is not empty"; + } + + public static class Enum + { + public const string Is = "is"; + public const string IsNot = "is not"; + } + + public static class Boolean + { + public const string Is = "is"; + } + + public static class DateTime + { + public const string Is = "is"; + public const string IsNot = "is not"; + public const string After = "is after"; + public const string OnOrAfter = "is on or after"; + public const string Before = "is before"; + public const string OnOrBefore = "is on or before"; + public const string Empty = "is empty"; + public const string NotEmpty = "is not empty"; + } + + public static class Guid + { + public const string Equal = "equals"; + public const string NotEqual = "not equals"; + } + + internal static string[] GetOperatorByDataType(Type type) + { + var fieldType = FieldType.Identify(type); + return GetOperatorByDataType(fieldType); + } + + internal static string[] GetOperatorByDataType(FieldType fieldType) + { + if (fieldType.IsString) + { + return new[] + { + String.Contains, + String.NotContains, + String.Equal, + String.NotEqual, + String.StartsWith, + String.EndsWith, + String.Empty, + String.NotEmpty, + }; + } + if (fieldType.IsNumber) + { + return new[] + { + Number.Equal, + Number.NotEqual, + Number.GreaterThan, + Number.GreaterThanOrEqual, + Number.LessThan, + Number.LessThanOrEqual, + Number.Empty, + Number.NotEmpty, + }; + } + if (fieldType.IsEnum) + { + return new[] { + Enum.Is, + Enum.IsNot, + }; + } + if (fieldType.IsBoolean) + { + return new[] + { + Boolean.Is, + }; + } + if (fieldType.IsDateTime) + { + return new[] + { + DateTime.Is, + DateTime.IsNot, + DateTime.After, + DateTime.OnOrAfter, + DateTime.Before, + DateTime.OnOrBefore, + DateTime.Empty, + DateTime.NotEmpty, + }; + } + if (fieldType.IsGuid) + { + return new[] + { + Guid.Equal, + Guid.NotEqual, + }; + } + + // default + return Array.Empty(); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Filter/PropertyFilterDefinition.cs b/Lantean.QBTMudBlade/Filter/PropertyFilterDefinition.cs new file mode 100644 index 0000000..5469abb --- /dev/null +++ b/Lantean.QBTMudBlade/Filter/PropertyFilterDefinition.cs @@ -0,0 +1,32 @@ +using MudBlazor; +using System.Linq.Expressions; +using System.Reflection; + +namespace Lantean.QBTMudBlade.Filter +{ + public record PropertyFilterDefinition + { + public PropertyFilterDefinition(string column, string @operator, object? value) + { + var (expression, propertyType) = ExpressionModifier.CreatePropertySelector(column); + + Column = column; + ColumnType = propertyType; + Operator = @operator; + Value = value; + Expression = expression; + } + + public string Column { get; } + + public Type ColumnType { get; } + + public string Operator { get; set; } + + public object? Value { get; set; } + + public Expression> Expression { get; } + } + + +} diff --git a/Lantean.QBTMudBlade/FilterHelper.cs b/Lantean.QBTMudBlade/FilterHelper.cs new file mode 100644 index 0000000..71b4fcb --- /dev/null +++ b/Lantean.QBTMudBlade/FilterHelper.cs @@ -0,0 +1,282 @@ +using Lantean.QBTMudBlade.Models; + +namespace Lantean.QBTMudBlade +{ + public static class FilterHelper + { + public const string TAG_ALL = "All"; + public const string TAG_UNTAGGED = "Untagged"; + public const string CATEGORY_ALL = "All"; + public const string CATEGORY_UNCATEGORIZED = "Uncategorized"; + public const string TRACKER_ALL = "All"; + public const string TRACKER_TRACKERLESS = "Trackerless"; + + public static IEnumerable Filter(this IEnumerable torrents, FilterState filterState) + { + return torrents.Where(t => FilterStatus(t, filterState.Status)) + .Where(t => FilterTag(t, filterState.Tag)) + .Where(t => FilterCategory(t, filterState.Category, filterState.UseSubcategories)) + .Where(t => FilterTracker(t, filterState.Tracker)) + .Where(t => FilterTerms(t.Name, filterState.Terms)); + } + + public static HashSet ToHashesHashSet(this IEnumerable torrents) + { + return torrents.Select(t => t.Hash).ToHashSet(); + } + + public static bool AddIfTrue(this HashSet hashSet, string value, bool condition) + { + if (condition) + { + return hashSet.Add(value); + } + + return false; + } + + public static bool RemoveIfTrue(this HashSet hashSet, string value, bool condition) + { + if (condition) + { + return hashSet.Remove(value); + } + + return false; + } + + public static bool AddIfTrueOrRemove(this HashSet hashSet, string value, bool condition) + { + if (condition) + { + return hashSet.Add(value); + } + else + { + return hashSet.Remove(value); + } + } + + public static bool ContainsAllTerms(string text, IEnumerable terms) + { + return terms.Any(t => + { + var term = t; + var isTermRequired = term[0] == '+'; + var isTermExcluded = term[0] == '-'; + + if (isTermRequired || isTermExcluded) + { + if (term.Length == 1) + { + return true; + } + term = term[1..]; + } + + var textContainsTerm = text.Contains(term, StringComparison.OrdinalIgnoreCase); + return isTermExcluded ? !textContainsTerm : textContainsTerm; + }); + } + + public static bool FilterTerms(string field, string? terms) + { + if (terms is null || terms == "") + { + return true; + } + + return ContainsAllTerms(field, terms.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } + + public static bool FilterTerms(Torrent torrent, string? terms) + { + if (terms is null || terms == "") + { + return true; + } + + return ContainsAllTerms(torrent.Name, terms.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } + + public static bool FilterTracker(Torrent torrent, string tracker) + { + if (tracker == TRACKER_ALL) + { + return true; + } + + if (tracker == TRACKER_TRACKERLESS) + { + return torrent.Tracker == ""; + } + + return torrent.Tracker == tracker; + } + + public static bool FilterCategory(Torrent torrent, string category, bool useSubcategories) + { + switch (category) + { + case CATEGORY_ALL: + break; + + case CATEGORY_UNCATEGORIZED: + if (!string.IsNullOrEmpty(torrent.Category)) + { + return false; + } + break; + + default: + if (!useSubcategories) + { + if (torrent.Category != category) + { + return false; + } + else + { + if (!torrent.Category.StartsWith(category)) + { + return false; + } + } + } + break; + } + + return true; + } + + public static bool FilterTag(Torrent torrent, string tag) + { + if (tag == TAG_ALL) + { + return true; + } + + if (tag == TAG_UNTAGGED) + { + return torrent.Tags.Count == 0; + } + + return torrent.Tags.Contains(tag); + } + + public static bool FilterStatus(Torrent torrent, Status status) + { + var state = torrent.State; + bool inactive = false; + switch (status) + { + case Status.All: + return true; + + case Status.Downloading: + if (state != "downloading" && !state.Contains("DL")) + { + return false; + } + break; + + case Status.Seeding: + if (state != "uploading" && state != "forcedUP" && state != "stalledUP" && state != "queuedUP" && state != "checkingUP") + { + return false; + } + break; + + case Status.Completed: + if (state != "uploading" && !state.Contains("UL")) + { + return false; + } + break; + + case Status.Resumed: + if (!state.Contains("resumed")) + { + return false; + } + break; + + case Status.Paused: + if (!state.Contains("paused")) + { + return false; + } + break; + + case Status.Inactive: + case Status.Active: + if (status == Status.Inactive) + { + inactive = true; + } + bool check; + if (state == "stalledDL") + { + check = torrent.UploadSpeed > 0; + } + else + { + check = state == "metaDL" || state == "forcedMetaDL" || state == "downloading" || state == "forcedDL" || state == "uploading" || state == "forcedUP"; + } + + if (check == inactive) + { + return false; + } + break; + + case Status.Stalled: + if (state != "stalledUP" && state != "stalledDL") + { + return false; + } + break; + + case Status.StalledUploading: + if (state != "stalledUP") + { + return false; + } + break; + + case Status.StalledDownloading: + if (state != "stalledDL") + { + return false; + } + break; + + case Status.Checking: + if (state != "checkingUP" && state != "checkingDL" && state != "checkingResumeData") + { + return false; + } + break; + + case Status.Errored: + if (state != "error" && state != "unknown" && state != "missingFiles") + { + return false; + } + break; + } + + return true; + } + + public static string GetStatusName(this string status) + { + return status switch + { + nameof(Status.StalledUploading) => "Stalled Uploading", + nameof(Status.StalledDownloading) => "Stalled Downloading", + _ => status, + }; + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/FilterState.cs b/Lantean.QBTMudBlade/FilterState.cs new file mode 100644 index 0000000..79f2760 --- /dev/null +++ b/Lantean.QBTMudBlade/FilterState.cs @@ -0,0 +1,24 @@ +using Lantean.QBTMudBlade.Models; + +namespace Lantean.QBTMudBlade +{ + public struct FilterState + { + public FilterState(string category, Status status, string tag, string tracker, bool useSubcategories, string? terms) + { + Category = category; + Status = status; + Tag = tag; + Tracker = tracker; + UseSubcategories = useSubcategories; + Terms = terms; + } + + public string Category { get; } = "all"; + public Status Status { get; } = Status.All; + public string Tag { get; } = "all"; + public string Tracker { get; } = "all"; + public bool UseSubcategories { get; } + public string? Terms { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/GlobalSuppressions.cs b/Lantean.QBTMudBlade/GlobalSuppressions.cs new file mode 100644 index 0000000..d04d8b3 --- /dev/null +++ b/Lantean.QBTMudBlade/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +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/Interop/BoundingClientRect.cs b/Lantean.QBTMudBlade/Interop/BoundingClientRect.cs new file mode 100644 index 0000000..557e455 --- /dev/null +++ b/Lantean.QBTMudBlade/Interop/BoundingClientRect.cs @@ -0,0 +1,21 @@ +namespace Lantean.QBTMudBlade.Interop +{ + public class BoundingClientRect + { + public int Bottom { get; set; } + + public int Top { get; set; } + + public int Left { get; set; } + + public int Right { get; set; } + + public int Width { get; set; } + + public int Height { get; set; } + + public int X { get; set; } + + public int Y { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Interop/InteropHelper.cs b/Lantean.QBTMudBlade/Interop/InteropHelper.cs new file mode 100644 index 0000000..53f4566 --- /dev/null +++ b/Lantean.QBTMudBlade/Interop/InteropHelper.cs @@ -0,0 +1,12 @@ +using Microsoft.JSInterop; + +namespace Lantean.QBTMudBlade.Interop +{ + public static class InteropHelper + { + public static async Task GetBoundingClientRect(this IJSRuntime runtime, string id) + { + return await runtime.InvokeAsync("qbt.getBoundingClientRect", id); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj b/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj new file mode 100644 index 0000000..31be248 --- /dev/null +++ b/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + diff --git a/Lantean.QBTMudBlade/Layout/DetailsLayout.razor b/Lantean.QBTMudBlade/Layout/DetailsLayout.razor new file mode 100644 index 0000000..4006999 --- /dev/null +++ b/Lantean.QBTMudBlade/Layout/DetailsLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase +@layout LoggedInLayout + + + + + + @Body + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Layout/DetailsLayout.razor.cs b/Lantean.QBTMudBlade/Layout/DetailsLayout.razor.cs new file mode 100644 index 0000000..2cfb928 --- /dev/null +++ b/Lantean.QBTMudBlade/Layout/DetailsLayout.razor.cs @@ -0,0 +1,29 @@ +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; + +namespace Lantean.QBTMudBlade.Layout +{ + public partial class DetailsLayout + { + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + [CascadingParameter] + public IEnumerable? Torrents { get; set; } + + protected string? SelectedTorrent { get; set; } + + protected override void OnParametersSet() + { + if (Body?.Target is not RouteView routeView || routeView.RouteData.RouteValues is null) + { + return; + } + + if (routeView.RouteData.RouteValues.TryGetValue("hash", out var hash)) + { + SelectedTorrent = hash?.ToString(); + } + } + } +} diff --git a/Lantean.QBTMudBlade/Layout/ListLayout.razor b/Lantean.QBTMudBlade/Layout/ListLayout.razor new file mode 100644 index 0000000..366f86f --- /dev/null +++ b/Lantean.QBTMudBlade/Layout/ListLayout.razor @@ -0,0 +1,11 @@ +@inherits LayoutComponentBase +@layout LoggedInLayout + + + + + + + @Body + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Layout/ListLayout.razor.cs b/Lantean.QBTMudBlade/Layout/ListLayout.razor.cs new file mode 100644 index 0000000..710823c --- /dev/null +++ b/Lantean.QBTMudBlade/Layout/ListLayout.razor.cs @@ -0,0 +1,26 @@ +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; + +namespace Lantean.QBTMudBlade.Layout +{ + public partial class ListLayout + { + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + [CascadingParameter(Name = "StatusChanged")] + public EventCallback StatusChanged { get; set; } + + [CascadingParameter(Name = "CategoryChanged")] + public EventCallback CategoryChanged { get; set; } + + [CascadingParameter(Name = "TagChanged")] + public EventCallback TagChanged { get; set; } + + [CascadingParameter(Name = "TrackerChanged")] + public EventCallback TrackerChanged { get; set; } + + [CascadingParameter(Name = "SearchTermChanged")] + public EventCallback SearchTermChanged { get; set; } + } +} diff --git a/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor new file mode 100644 index 0000000..e60ee77 --- /dev/null +++ b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor @@ -0,0 +1,57 @@ +@inherits LayoutComponentBase +@layout MainLayout + +qBittorrent @Version Web UI + +@if (!IsAuthenticated) +{ + + return; +} + + + + + + + + + + @Body + + + + + + + + @if (MainData?.LostConnection == true) + { + qBittorrent client is not reachable + } + + @DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ") + + DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes + + @{ + var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus); + } + + + + + + + @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s") + @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoData, "(", ")") + + + + + @DisplayHelpers.Size(MainData?.ServerState.UploadInfoSpeed, null, "/s") + @DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")") + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs new file mode 100644 index 0000000..a51f76f --- /dev/null +++ b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs @@ -0,0 +1,173 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Models; +using Lantean.QBTMudBlade.Services; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Layout +{ + public partial class LoggedInLayout : IDisposable + { + private readonly bool _refreshEnabled = true; + + private int _requestId = 0; + private bool _disposedValue; + private readonly CancellationTokenSource _timerCancellationToken = new(); + private int _refreshInterval = 1500; + + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDataManager DataManager { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + protected MainData? MainData { get; set; } + + protected string Category { get; set; } = FilterHelper.CATEGORY_ALL; + + protected string Tag { get; set; } = FilterHelper.TAG_ALL; + + protected string Tracker { get; set; } = FilterHelper.TRACKER_ALL; + + protected Status Status { get; set; } = Status.All; + + protected string Version { get; set; } = ""; + + protected string? SearchText { get; set; } + + protected IEnumerable Torrents => GetTorrents(); + + protected bool IsAuthenticated { get; set; } + + protected bool LostConnection { get; set; } + + private List GetTorrents() + { + if (MainData is null) + { + return []; + } + + var filterState = new FilterState(Category, Status, Tag, Tracker, MainData.ServerState.UseSubcategories, SearchText); + + return MainData.Torrents.Values.Filter(filterState).ToList(); + } + + protected override async Task OnInitializedAsync() + { + if (!await ApiClient.CheckAuthState()) + { + NavigationManager.NavigateTo("/login"); + return; + } + + await InvokeAsync(StateHasChanged); + + Version = await ApiClient.GetApplicationVersion(); + var data = await ApiClient.GetMainData(_requestId); + MainData = DataManager.CreateMainData(data); + + _requestId = data.ResponseId; + _refreshInterval = MainData.ServerState.RefreshInterval; + + IsAuthenticated = true; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!_refreshEnabled) + { + return; + } + + if (firstRender) + { + using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_refreshInterval))) + { + while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync()) + { + if (!IsAuthenticated) + { + return; + } + QBitTorrentClient.Models.MainData data; + try + { + data = await ApiClient.GetMainData(_requestId); + } + catch (HttpRequestException) + { + if (MainData is not null) + { + MainData.LostConnection = true; + } + _timerCancellationToken.CancelIfNotDisposed(); + await InvokeAsync(StateHasChanged); + return; + } + + if (MainData is null || data.FullUpdate) + { + MainData = DataManager.CreateMainData(data); + } + else + { + DataManager.MergeMainData(data, MainData); + } + + _refreshInterval = MainData.ServerState.RefreshInterval; + _requestId = data.ResponseId; + await InvokeAsync(StateHasChanged); + } + } + } + } + + protected EventCallback CategoryChanged => EventCallback.Factory.Create(this, category => Category = category); + + protected EventCallback StatusChanged => EventCallback.Factory.Create(this, status => Status = status); + + protected EventCallback TagChanged => EventCallback.Factory.Create(this, tag => Tag = tag); + + protected EventCallback TrackerChanged => EventCallback.Factory.Create(this, tracker => Tracker = tracker); + + protected EventCallback SearchTermChanged => EventCallback.Factory.Create(this, term => SearchText = term); + + protected static (string, Color) GetConnectionIcon(string? status) + { + if (status is null) + { + return (Icons.Material.Outlined.SignalWifiOff, Color.Warning); + } + + return (Icons.Material.Outlined.SignalWifi4Bar, Color.Success); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _timerCancellationToken.Cancel(); + _timerCancellationToken.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Layout/MainLayout.razor b/Lantean.QBTMudBlade/Layout/MainLayout.razor new file mode 100644 index 0000000..6437726 --- /dev/null +++ b/Lantean.QBTMudBlade/Layout/MainLayout.razor @@ -0,0 +1,22 @@ +@inherits LayoutComponentBase + + + + + +qBittorrent Web UI + + + + + qBittorrent Web UI + + @if (ShowMenu) + { + + } + + + @Body + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs b/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs new file mode 100644 index 0000000..0b03b9e --- /dev/null +++ b/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs @@ -0,0 +1,89 @@ +using Lantean.QBitTorrentClient; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using MudBlazor.Services; + +namespace Lantean.QBTMudBlade.Layout +{ + public partial class MainLayout : IBrowserViewportObserver, IAsyncDisposable + { + private bool _disposedValue; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [Inject] + private IBrowserViewportService BrowserViewportService { get; set; } = default!; + + [Inject] + private IApiClient ApiClient { get; set; } = default!; + + protected bool DrawerOpen { get; set; } = true; + + protected bool ShowMenu { get; set; } = false; + + public Guid Id => Guid.NewGuid(); + + ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new() + { + ReportRate = 50, + NotifyOnBreakpointOnly = true + }; + + protected void ToggleDrawer() + { + DrawerOpen = !DrawerOpen; + } + + protected override async Task OnParametersSetAsync() + { + if (!ShowMenu) + { + ShowMenu = await ApiClient.CheckAuthState(); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await BrowserViewportService.SubscribeAsync(this, fireImmediately: true); + } + + await base.OnAfterRenderAsync(firstRender); + } + + public async Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs) + { + if (browserViewportEventArgs.Breakpoint == Breakpoint.Sm && DrawerOpen) + { + DrawerOpen = false; + } + else if (browserViewportEventArgs.Breakpoint > Breakpoint.Sm && !DrawerOpen) + { + DrawerOpen = true; + } + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task DisposeAsync(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + await BrowserViewportService.UnsubscribeAsync(this); + } + + _disposedValue = true; + } + } + + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Lantean.QBTMudBlade/Models/AddTorrentFileOptions.cs b/Lantean.QBTMudBlade/Models/AddTorrentFileOptions.cs new file mode 100644 index 0000000..6dec44b --- /dev/null +++ b/Lantean.QBTMudBlade/Models/AddTorrentFileOptions.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Components.Forms; + +namespace Lantean.QBTMudBlade.Models +{ + public record AddTorrentFileOptions : TorrentOptions + { + public AddTorrentFileOptions(IReadOnlyList files, TorrentOptions options) : base(options) + { + Files = files; + } + + public IReadOnlyList Files { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/AddTorrentLinkOptions.cs b/Lantean.QBTMudBlade/Models/AddTorrentLinkOptions.cs new file mode 100644 index 0000000..16174dc --- /dev/null +++ b/Lantean.QBTMudBlade/Models/AddTorrentLinkOptions.cs @@ -0,0 +1,12 @@ +namespace Lantean.QBTMudBlade.Models +{ + public record AddTorrentLinkOptions : TorrentOptions + { + public AddTorrentLinkOptions(string urls, TorrentOptions options) : base(options) + { + Urls = urls.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + } + + public IReadOnlyList Urls { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/Category.cs b/Lantean.QBTMudBlade/Models/Category.cs new file mode 100644 index 0000000..98c5e88 --- /dev/null +++ b/Lantean.QBTMudBlade/Models/Category.cs @@ -0,0 +1,14 @@ +namespace Lantean.QBTMudBlade.Models +{ + public record Category + { + public Category(string name, string savePath) + { + Name = name; + SavePath = savePath; + } + + public string Name { get; set; } + public string SavePath { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/ContentItem.cs b/Lantean.QBTMudBlade/Models/ContentItem.cs new file mode 100644 index 0000000..b7c4cdc --- /dev/null +++ b/Lantean.QBTMudBlade/Models/ContentItem.cs @@ -0,0 +1,62 @@ +namespace Lantean.QBTMudBlade.Models +{ + public class ContentItem + { + public ContentItem( + string name, + string displayName, + int index, + Priority priority, + float progress, + long size, + float availability, + bool isFolder = false, + int level = 0) + { + Name = name; + DisplayName = displayName; + Index = index; + Priority = priority; + Progress = progress; + Size = size; + Availability = availability; + IsFolder = isFolder; + Level = level; + } + + public string Name { get; } + + public string Path => IsFolder ? Name : Name.GetDirectoryPath(); + + public string DisplayName { get; } + + public int Index { get; } + + public Priority Priority { get; set; } + + public float Progress { get; set; } + + public long Size { get; set; } + + public float Availability { get; set; } + + public long Downloaded => (long)Math.Round(Size * Progress, 0); + + public long Remaining => Size - Downloaded; + + public bool IsFolder { get; } + + public int Level { get; } + + public override bool Equals(object? obj) + { + if (obj is null) return false; + return ((ContentItem)obj).Name == Name; + } + + public override int GetHashCode() + { + return Name.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/ContentItemType.cs b/Lantean.QBTMudBlade/Models/ContentItemType.cs new file mode 100644 index 0000000..95178e4 --- /dev/null +++ b/Lantean.QBTMudBlade/Models/ContentItemType.cs @@ -0,0 +1,8 @@ +namespace Lantean.QBTMudBlade.Models +{ + public enum ContentItemType + { + File, + Folder + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/GlobalTransferInfo.cs b/Lantean.QBTMudBlade/Models/GlobalTransferInfo.cs new file mode 100644 index 0000000..4f014c5 --- /dev/null +++ b/Lantean.QBTMudBlade/Models/GlobalTransferInfo.cs @@ -0,0 +1,46 @@ +namespace Lantean.QBTMudBlade.Models +{ + public record GlobalTransferInfo + { + public GlobalTransferInfo( + string connectionStatus, + int dHTNodes, + long downloadInfoData, + long downloadInfoSpeed, + long downloadRateLimit, + long uploadInfoData, + long uploadInfoSpeed, + long uploadRateLimit) + { + ConnectionStatus = connectionStatus; + DHTNodes = dHTNodes; + DownloadInfoData = downloadInfoData; + DownloadInfoSpeed = downloadInfoSpeed; + DownloadRateLimit = downloadRateLimit; + UploadInfoData = uploadInfoData; + UploadInfoSpeed = uploadInfoSpeed; + UploadRateLimit = uploadRateLimit; + } + + public GlobalTransferInfo() + { + ConnectionStatus = "Unknown"; + } + + public string ConnectionStatus { get; set; } + + public int DHTNodes { get; set; } + + public long DownloadInfoData { get; set; } + + public long DownloadInfoSpeed { get; set; } + + public long DownloadRateLimit { get; set; } + + public long UploadInfoData { get; set; } + + public long UploadInfoSpeed { get; set; } + + public long UploadRateLimit { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/MainData.cs b/Lantean.QBTMudBlade/Models/MainData.cs new file mode 100644 index 0000000..ca51f7e --- /dev/null +++ b/Lantean.QBTMudBlade/Models/MainData.cs @@ -0,0 +1,40 @@ +namespace Lantean.QBTMudBlade.Models +{ + public record MainData + { + public MainData( + IDictionary torrents, + IEnumerable tags, + IDictionary categories, + IDictionary> trackers, + ServerState serverState, + Dictionary> tagState, + Dictionary> categoriesState, + Dictionary> statusState, + Dictionary> trackersState) + { + Torrents = torrents.ToDictionary(); + Tags = tags.ToHashSet(); + Categories = categories.ToDictionary(); + Trackers = trackers.ToDictionary(); + ServerState = serverState; + TagState = tagState; + CategoriesState = categoriesState; + StatusState = statusState; + TrackersState = trackersState; + } + + public Dictionary Torrents { get; } + public HashSet Tags { get; } + public Dictionary Categories { get; } + public Dictionary> Trackers { get; } + public ServerState ServerState { get; } + + public Dictionary> TagState { get; } + public Dictionary> CategoriesState { get; } + public Dictionary> StatusState { get; } + public Dictionary> TrackersState { get; } + public string? SelectedTorrentHash { get; set; } + public bool LostConnection { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/Peer.cs b/Lantean.QBTMudBlade/Models/Peer.cs new file mode 100644 index 0000000..f892d5e --- /dev/null +++ b/Lantean.QBTMudBlade/Models/Peer.cs @@ -0,0 +1,72 @@ +namespace Lantean.QBTMudBlade.Models +{ + public class Peer + { + public Peer( + string ip, + string client, + string clientId, + string connection, + string country, + string countryCode, + long downloaded, + long downloadSpeed, + string files, + string flags, + string flagsDescription, + string iPAddress, + int port, + float progress, + float relevance, + long uploaded, + long uploadSpeed) + { + IP = ip; + Client = client; + ClientId = clientId; + Connection = connection; + Country = country; + CountryCode = countryCode; + Downloaded = downloaded; + DownloadSpeed = downloadSpeed; + Files = files; + Flags = flags; + FlagsDescription = flagsDescription; + IPAddress = iPAddress; + Port = port; + Progress = progress; + Relevance = relevance; + Uploaded = uploaded; + UploadSpeed = uploadSpeed; + } + + public string IP { get; } + public string Client { get; set; } + public string ClientId { get; set; } + public string Connection { get; set; } + public string Country { get; set; } + public string CountryCode { get; set; } + public long Downloaded { get; set; } + public long DownloadSpeed { get; set; } + public string Files { get; set; } + public string Flags { get; set; } + public string FlagsDescription { get; set; } + public string IPAddress { get; set; } + public int Port { get; set; } + public float Progress { get; set; } + public float Relevance { get; set; } + public long Uploaded { get; set; } + public long UploadSpeed { get; set; } + + public override bool Equals(object? obj) + { + if (obj is null) return false; + return ((Peer)obj).IP == IP; + } + + public override int GetHashCode() + { + return IP.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/PeerList.cs b/Lantean.QBTMudBlade/Models/PeerList.cs new file mode 100644 index 0000000..c919844 --- /dev/null +++ b/Lantean.QBTMudBlade/Models/PeerList.cs @@ -0,0 +1,12 @@ +namespace Lantean.QBTMudBlade.Models +{ + public record PeerList + { + public PeerList(Dictionary peers) + { + Peers = peers; + } + + public Dictionary Peers { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/Priority.cs b/Lantean.QBTMudBlade/Models/Priority.cs new file mode 100644 index 0000000..e4b7bfb --- /dev/null +++ b/Lantean.QBTMudBlade/Models/Priority.cs @@ -0,0 +1,11 @@ +namespace Lantean.QBTMudBlade.Models +{ + public enum Priority + { + Mixed = -1, + DoNotDownload = 0, + Normal = 1, + High = 6, + Maximum = 7 + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/ServerState.cs b/Lantean.QBTMudBlade/Models/ServerState.cs new file mode 100644 index 0000000..98ed5fc --- /dev/null +++ b/Lantean.QBTMudBlade/Models/ServerState.cs @@ -0,0 +1,89 @@ +namespace Lantean.QBTMudBlade.Models +{ + public record ServerState : GlobalTransferInfo + { + public ServerState( + long allTimeDownloaded, + long allTimeUploaded, + int averageTimeQueue, + string connectionStatus, + int dHTNodes, + long downloadInfoData, + long downloadInfoSpeed, + long downloadRateLimit, + long freeSpaceOnDisk, + float globalRatio, + int queuedIOJobs, + bool queuing, + float readCacheHits, + float readCacheOverload, + int refreshInterval, + int totalBuffersSize, + int totalPeerConnections, + int totalQueuedSize, + long totalWastedSession, + long uploadInfoData, + long uploadInfoSpeed, + long uploadRateLimit, + bool useAltSpeedLimits, + bool useSubcategories, + float writeCacheOverload) : base(connectionStatus, dHTNodes, downloadInfoData, downloadInfoSpeed, downloadRateLimit, uploadInfoData, uploadInfoSpeed, uploadRateLimit) + { + AllTimeDownloaded = allTimeDownloaded; + AllTimeUploaded = allTimeUploaded; + AverageTimeQueue = averageTimeQueue; + FreeSpaceOnDisk = freeSpaceOnDisk; + GlobalRatio = globalRatio; + QueuedIOJobs = queuedIOJobs; + Queuing = queuing; + ReadCacheHits = readCacheHits; + ReadCacheOverload = readCacheOverload; + RefreshInterval = refreshInterval; + TotalBuffersSize = totalBuffersSize; + TotalPeerConnections = totalPeerConnections; + TotalQueuedSize = totalQueuedSize; + TotalWastedSession = totalWastedSession; + UseAltSpeedLimits = useAltSpeedLimits; + UseSubcategories = useSubcategories; + WriteCacheOverload = writeCacheOverload; + } + + public ServerState() + { + } + + public long AllTimeDownloaded { get; set; } + + public long AllTimeUploaded { get; set; } + + public int AverageTimeQueue { get; set; } + + public long FreeSpaceOnDisk { get; set; } + + public float GlobalRatio { get; set; } + + public int QueuedIOJobs { get; set; } + + public bool Queuing { get; set; } + + public float ReadCacheHits { get; set; } + + public float ReadCacheOverload { get; set; } + + public int RefreshInterval { get; set; } + + public int TotalBuffersSize { get; set; } + + public int TotalPeerConnections { get; set; } + + public int TotalQueuedSize { get; set; } + + public long TotalWastedSession { get; set; } + + public bool UseAltSpeedLimits { get; set; } + + public bool UseSubcategories { get; set; } + + public float WriteCacheOverload { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/Status.cs b/Lantean.QBTMudBlade/Models/Status.cs new file mode 100644 index 0000000..568f2e9 --- /dev/null +++ b/Lantean.QBTMudBlade/Models/Status.cs @@ -0,0 +1,19 @@ +namespace Lantean.QBTMudBlade.Models +{ + public enum Status + { + All, + Downloading, + Seeding, + Completed, + Resumed, + Paused, + Active, + Inactive, + Stalled, + StalledUploading, + StalledDownloading, + Checking, + Errored + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/Torrent.cs b/Lantean.QBTMudBlade/Models/Torrent.cs new file mode 100644 index 0000000..ab31fcd --- /dev/null +++ b/Lantean.QBTMudBlade/Models/Torrent.cs @@ -0,0 +1,232 @@ +namespace Lantean.QBTMudBlade.Models +{ + public class Torrent + { + public Torrent( + string hash, + long addedOn, + long amountLeft, + bool automaticTorrentManagement, + float aavailability, + string category, + long completed, + long completionOn, + string contentPath, + long downloadLimit, + long downloadSpeed, + long downloaded, + long downloadedSession, + long estimatedTimeOfArrival, + bool firstLastPiecePriority, + bool forceStart, + 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, + IEnumerable tags, + int timeActive, + long totalSize, + string tracker, + long uploadLimit, + long uploaded, + long uploadedSession, + long uploadSpeed, + long reannounce) + { + Hash = hash; + AddedOn = addedOn; + AmountLeft = amountLeft; + AutomaticTorrentManagement = automaticTorrentManagement; + Availability = aavailability; + Category = category; + Completed = completed; + CompletionOn = completionOn; + ContentPath = contentPath; + DownloadLimit = downloadLimit; + DownloadSpeed = downloadSpeed; + Downloaded = downloaded; + DownloadedSession = downloadedSession; + EstimatedTimeOfArrival = estimatedTimeOfArrival; + FirstLastPiecePriority = firstLastPiecePriority; + ForceStart = forceStart; + 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.ToList(); + TimeActive = timeActive; + TotalSize = totalSize; + Tracker = tracker; + UploadLimit = uploadLimit; + Uploaded = uploaded; + UploadedSession = uploadedSession; + UploadSpeed = uploadSpeed; + Reannounce = reannounce; + } + + protected Torrent() + { + Hash = ""; + Category = ""; + ContentPath = ""; + InfoHashV1 = ""; + InfoHashV2 = ""; + MagnetUri = ""; + Name = ""; + SavePath = ""; + State = ""; + Tags = []; + Tracker = ""; + } + + public string Hash { get; } + + public long AddedOn { get; set; } + + public long AmountLeft { get; set; } + + public bool AutomaticTorrentManagement { get; set; } + + public float Availability { get; set; } + + public string Category { get; set; } + + public long Completed { get; set; } + + public long CompletionOn { get; set; } + + public string ContentPath { get; set; } + + public long DownloadLimit { get; set; } + + public long DownloadSpeed { get; set; } + + public long Downloaded { get; set; } + + public long DownloadedSession { get; set; } + + public long EstimatedTimeOfArrival { get; set; } + + public bool FirstLastPiecePriority { get; set; } + + public bool ForceStart { get; set; } + + public string InfoHashV1 { get; set; } + + public string InfoHashV2 { get; set; } + + public long LastActivity { get; set; } + + public string MagnetUri { get; set; } + + public float MaxRatio { get; set; } + + public int MaxSeedingTime { get; set; } + + public string Name { get; set; } + + public int NumberComplete { get; set; } + + public int NumberIncomplete { get; set; } + + public int NumberLeeches { get; set; } + + public int NumberSeeds { get; set; } + + public int Priority { get; set; } + + public float Progress { get; set; } + + public float Ratio { get; set; } + + public float RatioLimit { get; set; } + + public string SavePath { get; set; } + + public long SeedingTime { get; set; } + + public int SeedingTimeLimit { get; set; } + + public long SeenComplete { get; set; } + + public bool SequentialDownload { get; set; } + + public long Size { get; set; } + + public string State { get; set; } + + public bool SuperSeeding { get; set; } + + public List Tags { get; set; } + + public int TimeActive { get; set; } + + public long TotalSize { get; set; } + + public string Tracker { get; set; } + + public long UploadLimit { get; set; } + + public long Uploaded { get; set; } + + public long UploadedSession { get; set; } + + public long UploadSpeed { get; set; } + + public long Reannounce { get; set; } + + public override bool Equals(object? obj) + { + if (obj is null) return false; + return ((Torrent)obj).Hash == Hash; + } + + public override int GetHashCode() + { + return Hash.GetHashCode(); + } + + public override string ToString() + { + return Hash; + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/TorrentOptions.cs b/Lantean.QBTMudBlade/Models/TorrentOptions.cs new file mode 100644 index 0000000..e51962e --- /dev/null +++ b/Lantean.QBTMudBlade/Models/TorrentOptions.cs @@ -0,0 +1,65 @@ +namespace Lantean.QBTMudBlade.Models +{ + public record TorrentOptions + { + public TorrentOptions( + bool torrentManagementMode, + string savePath, + string? cookie, + string? renameTorrent, + string? category, + bool startTorrent, + bool addToTopOfQueue, + string stopCondition, + bool skipHashCheck, + string contentLayout, + bool downloadInSequentialOrder, + bool downloadFirstAndLastPiecesFirst, + long downloadLimit, + long uploadLimit) + { + TorrentManagementMode = torrentManagementMode; + SavePath = savePath; + Cookie = cookie; + RenameTorrent = renameTorrent; + Category = category; + StartTorrent = startTorrent; + AddToTopOfQueue = addToTopOfQueue; + StopCondition = stopCondition; + SkipHashCheck = skipHashCheck; + ContentLayout = contentLayout; + DownloadInSequentialOrder = downloadInSequentialOrder; + DownloadFirstAndLastPiecesFirst = downloadFirstAndLastPiecesFirst; + DownloadLimit = downloadLimit; + UploadLimit = uploadLimit; + } + + public bool TorrentManagementMode { get; } + + public string SavePath { get; } + + public string? Cookie { get; } + + public string? RenameTorrent { get; } + + public string? Category { get; } + + public bool StartTorrent { get; } + + public bool AddToTopOfQueue { get; } + + public string StopCondition { get; } + + public bool SkipHashCheck { get; } + + public string ContentLayout { get; } + + public bool DownloadInSequentialOrder { get; } + + public bool DownloadFirstAndLastPiecesFirst { get; } + + public long DownloadLimit { get; } + + public long UploadLimit { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Details.razor b/Lantean.QBTMudBlade/Pages/Details.razor new file mode 100644 index 0000000..2a516c0 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Details.razor @@ -0,0 +1,36 @@ +@page "/details/{hash}" +@layout DetailsLayout + + + @if (!DrawerOpen) + { + + + } + @if (Hash is not null) + { + + } + + @Name + + + + + + + + + + + + + + + + + + + + + diff --git a/Lantean.QBTMudBlade/Pages/Details.razor.cs b/Lantean.QBTMudBlade/Pages/Details.razor.cs new file mode 100644 index 0000000..be97365 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Details.razor.cs @@ -0,0 +1,93 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Components.Dialogs; +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Pages +{ + public partial class Details + { + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDialogService DialogService { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [CascadingParameter] + public MainData? MainData { get; set; } + + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + [Parameter] + public string? Hash { get; set; } + + protected int ActiveTab { get; set; } = 0; + + protected int RefreshInterval => MainData?.ServerState.RefreshInterval ?? 1500; + + protected string Name => GetName(); + + private string GetName() + { + if (Hash is null || MainData is null) + { + return ""; + } + + if (!MainData.Torrents.TryGetValue(Hash, out var torrent)) + { + return ""; + } + + return torrent.Name; + } + + protected async Task PauseTorrent(MouseEventArgs eventArgs) + { + if (Hash is null) + { + return; + } + + await ApiClient.PauseTorrent(Hash); + } + + protected async Task ResumeTorrent(MouseEventArgs eventArgs) + { + if (Hash is null) + { + return; + } + + await ApiClient.ResumeTorrent(Hash); + } + + protected async Task RemoveTorrent(MouseEventArgs eventArgs) + { + if (Hash is null) + { + return; + } + + var reference = await DialogService.ShowAsync("Remove torrent(s)?"); + var result = await reference.Result; + if (result.Canceled) + { + return; + } + + await ApiClient.DeleteTorrent(Hash, (bool)result.Data); + } + + protected void NavigateBack() + { + NavigationManager.NavigateTo("/"); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Login.razor b/Lantean.QBTMudBlade/Pages/Login.razor new file mode 100644 index 0000000..8a52a5d --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Login.razor @@ -0,0 +1,38 @@ +@page "/login" +@layout MainLayout + +Login + + + + + + + + + Login + + + @if (ApiError is not null) + { + @ApiError + } + + + + + + + + + + + Login + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Login.razor.cs b/Lantean.QBTMudBlade/Pages/Login.razor.cs new file mode 100644 index 0000000..066a40d --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Login.razor.cs @@ -0,0 +1,67 @@ +using Lantean.QBitTorrentClient; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Net; + +namespace Lantean.QBTMudBlade.Pages +{ + public partial class Login + { + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + protected LoginModel Model { get; set; } = new LoginModel(); + + protected string? ApiError { get; set; } + + protected async Task LoginClick(EditContext context) + { + await DoLogin(Model.Username, Model.Password); + } + + private async Task DoLogin(string username, string password) + { + try + { + await ApiClient.Login(username, password); + + NavigationManager.NavigateTo("/"); + } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.BadRequest) + { + ApiError = "Invalid username or password."; + } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) + { + ApiError = "Requests from this client are currently unavailable."; + } + catch + { + ApiError = "Unable to communicate with the qBittorrent API."; + } + } + +#if DEBUG + protected override async Task OnInitializedAsync() + { + await DoLogin("admin", "kL76z3W36"); + } +#endif + } + + public class LoginModel + { + [Required] + [NotNull] + public string? Username { get; set; } + + [Required] + [NotNull] + public string? Password { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Main.razor b/Lantean.QBTMudBlade/Pages/Main.razor new file mode 100644 index 0000000..c1f8c02 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Main.razor @@ -0,0 +1,4 @@ +@page "/main" + +qBittorrent @Version Web UI + diff --git a/Lantean.QBTMudBlade/Pages/Main.razor.cs b/Lantean.QBTMudBlade/Pages/Main.razor.cs new file mode 100644 index 0000000..f4b4763 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Main.razor.cs @@ -0,0 +1,154 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBitTorrentClient.Models; +using Lantean.QBTMudBlade.Models; +using Lantean.QBTMudBlade.Services; +using Microsoft.AspNetCore.Components; +using System.Net; + +namespace Lantean.QBTMudBlade.Pages +{ + public partial class Main : IDisposable + { + private bool _refreshEnabled = true; + + protected bool DrawerOpen { get; set; } = true; + + protected int RefreshInterval { get; set; } = 1500; + + private int _requestId = 0; + private bool _disposedValue; + private readonly CancellationTokenSource _timerCancellationToken = new(); + + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDataManager DataManager { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + protected Models.MainData? TorrentList { get; set; } + + protected string Category { get; set; } = FilterHelper.CATEGORY_ALL; + + protected string Tag { get; set; } = FilterHelper.TAG_ALL; + + protected string Tracker { get; set; } = FilterHelper.TRACKER_ALL; + + protected Status Status { get; set; } = Status.All; + + protected string? SearchText { get; set; } + + protected FilterState FilterState => new FilterState(Category, Status, Tag, Tracker, TorrentList?.ServerState.UseSubcategories ?? false, SearchText); + + protected string? Version { get; set; } + + private async Task SearchTextChanged(string searchText) + { + SearchText = searchText == "" ? null : searchText; + + await InvokeAsync(StateHasChanged); + } + + protected async Task SelectedTorrentChanged(string hash) + { + if (TorrentList is not null) + { + TorrentList.SelectedTorrentHash = hash; + } + + await InvokeAsync(StateHasChanged); + } + + protected override async Task OnInitializedAsync() + { + if (!await ApiClient.CheckAuthState()) + { + NavigationManager.NavigateTo("/login"); + return; + } + try + { + Version = await ApiClient.GetApplicationVersion(); + var data = await ApiClient.GetMainData(_requestId); + TorrentList = DataManager.CreateMainData(data); + _requestId = data.ResponseId; + + RefreshInterval = TorrentList.ServerState.RefreshInterval; + + await InvokeAsync(StateHasChanged); + } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) + { + NavigationManager.NavigateTo("/login"); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!_refreshEnabled) + { + return; + } + if (firstRender) + { + using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval))) + { + while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync()) + { + QBitTorrentClient.Models.MainData data; + try + { + data = await ApiClient.GetMainData(_requestId); + } + catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden) + { + _timerCancellationToken.CancelIfNotDisposed(); + return; + } + + if (TorrentList is null || data.FullUpdate) + { + TorrentList = DataManager.CreateMainData(data); + } + else + { + DataManager.MergeMainData(data, TorrentList); + } + + RefreshInterval = TorrentList.ServerState.RefreshInterval; + _requestId = data.ResponseId; + await InvokeAsync(StateHasChanged); + } + } + } + } + + protected void DrawerToggle() + { + DrawerOpen = !DrawerOpen; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _timerCancellationToken.Cancel(); + _timerCancellationToken.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Options.razor b/Lantean.QBTMudBlade/Pages/Options.razor new file mode 100644 index 0000000..07be6c2 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Options.razor @@ -0,0 +1,55 @@ +@page "/options" +@layout LoggedInLayout + + + + Back + + Behaviour + Downloads + Connection + Speed + BitTorrent + RSS + Web UI + Advanced + + + + + @if (!DrawerOpen) + { + + + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Options.razor.cs b/Lantean.QBTMudBlade/Pages/Options.razor.cs new file mode 100644 index 0000000..be2cb41 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Options.razor.cs @@ -0,0 +1,134 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBitTorrentClient.Models; +using Lantean.QBTMudBlade.Components.Options; +using Lantean.QBTMudBlade.Services; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Pages +{ + public partial class Options + { + [Inject] + protected IDialogService DialogService { get; set; } = default!; + + [Inject] + protected ISnackbar Snackbar { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDataManager DataManager { get; set; } = default!; + + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + [CascadingParameter(Name = "LostConnection")] + public bool LostConnection { get; set; } + + protected int ActiveTab { get; set; } + + protected Preferences? Preferences { get; set; } + + protected BehaviourOptions? BehaviourOptions { get; set; } + + protected DownloadsOptions? DownloadsOptions { get; set; } + + protected ConnectionOptions? ConnectionOptions { get; set; } + + protected SpeedOptions? SpeedOptions { get; set; } + + protected BitTorrentOptions? BitTorrentOptions { get; set; } + + protected RSSOptions? RSSOptions { get; set; } + + protected WebUIOptions? WebUIOptions { get; set; } + + protected AdvancedOptions? AdvancedOptions { get; set; } + + private UpdatePreferences? UpdatePreferences { get; set; } + + protected override async Task OnInitializedAsync() + { + Preferences = await ApiClient.GetApplicationPreferences(); + } + + protected void PreferencesChanged(UpdatePreferences preferences) + { + UpdatePreferences = DataManager.MergePreferences(UpdatePreferences, preferences); + } + + protected async Task 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("/")); + } + + protected async Task Undo() + { + if (BehaviourOptions is not null) + { + await BehaviourOptions.ResetAsync(); + } + if (DownloadsOptions is not null) + { + await DownloadsOptions.ResetAsync(); + } + if (ConnectionOptions is not null) + { + await ConnectionOptions.ResetAsync(); + } + if (SpeedOptions is not null) + { + await SpeedOptions.ResetAsync(); + } + if (BitTorrentOptions is not null) + { + await BitTorrentOptions.ResetAsync(); + } + if (RSSOptions is not null) + { + await RSSOptions.ResetAsync(); + } + if (WebUIOptions is not null) + { + await WebUIOptions.ResetAsync(); + } + if (AdvancedOptions is not null) + { + await AdvancedOptions.ResetAsync(); + } + + UpdatePreferences = null; + + await InvokeAsync(StateHasChanged); + } + + protected async Task Save() + { + if (UpdatePreferences is null) + { + return; + } + await ApiClient.SetApplicationPreferences(UpdatePreferences); + Snackbar.Add("Options saved.", Severity.Success); + + Preferences = await ApiClient.GetApplicationPreferences(); + UpdatePreferences = null; + } + } +} + + diff --git a/Lantean.QBTMudBlade/Pages/TorrentList.razor b/Lantean.QBTMudBlade/Pages/TorrentList.razor new file mode 100644 index 0000000..ab64a13 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/TorrentList.razor @@ -0,0 +1,104 @@ +@page "/" +@layout ListLayout + + + + + + + + + + + + + + + + + + + + + + + @foreach (var column in GetColumns()) + { + var style = column.Width.HasValue ? $"width: {column.Width.Value}px" : null; + + } + + + @foreach (var column in GetColumns()) + { + + @if (column.SortSelector is not null) + { + @column.Header + } + else + { + @column.Header + } + + } + + + @foreach (var column in GetColumns()) + { + + @column.RowTemplate(column.GetRowContext(context)) + + } + + + +@code { + private static RenderFragment> ProgressBarColumn + { + get + { + return context => __builder => + { + var value = (float?)context.GetValue(); + var color = value < 1 ? Color.Success : Color.Info; + + @DisplayHelpers.Percentage(value) + ; + }; + } + } + + private static RenderFragment> IconColumn + { + get + { + return context => __builder => + { + var (icon, color) = DisplayHelpers.GetStateIcon((string?)context.GetValue()); + + }; + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs b/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs new file mode 100644 index 0000000..3e50774 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs @@ -0,0 +1,288 @@ +using Blazored.LocalStorage; +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Components.Dialogs; +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Pages +{ + public partial class TorrentList + { + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDialogService DialogService { get; set; } = default!; + + [Inject] + protected ILocalStorageService LocalStorage { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [CascadingParameter] + public IEnumerable? Torrents { get; set; } + + protected IEnumerable? OrderedTorrents => GetOrderedTorrents(); + + [CascadingParameter(Name = "SearchTermChanged")] + public EventCallback SearchTermChanged { get; set; } + + protected string? SearchText { get; set; } + + protected Torrent? SelectedTorrent { get; set; } + + protected HashSet SelectedItems { get; set; } = []; + + protected bool ToolbarButtonsEnabled => SelectedItems.Count > 0 || SelectedTorrent is not null; + + protected override void OnParametersSet() + { + if (SelectedColumns.Count == 0) + { + SelectedColumns = _columns.Where(c => c.Enabled).Select(c => c.Id).ToHashSet(); + } + _sortSelector ??= _columns.First(c => c.Enabled).SortSelector; + } + + private IEnumerable? GetOrderedTorrents() + { + if (Torrents is null) + { + return null; + } + + return Torrents.OrderByDirection(_sortDirection, _sortSelector ?? (t => t.Priority)); + } + + protected void SelectedItemsChanged(HashSet selectedItems) + { + SelectedItems = selectedItems; + if (selectedItems.Count == 1) + { + SelectedTorrent = selectedItems.First(); + } + } + + protected async Task SearchTextChanged(string text) + { + SearchText = text; + await SearchTermChanged.InvokeAsync(SearchText); + } + + protected async Task PauseTorrents(MouseEventArgs eventArgs) + { + await ApiClient.PauseTorrents(GetSelectedTorrents()); + + SelectedItems.Clear(); + await InvokeAsync(StateHasChanged); + } + + protected async Task ResumeTorrents(MouseEventArgs eventArgs) + { + await ApiClient.ResumeTorrents(GetSelectedTorrents()); + + SelectedItems.Clear(); + await InvokeAsync(StateHasChanged); + } + + protected async Task RemoveTorrents(MouseEventArgs eventArgs) + { + var reference = await DialogService.ShowAsync("Remove torrent(s)?"); + var result = await reference.Result; + if (result.Canceled) + { + return; + } + + await ApiClient.DeleteTorrents(GetSelectedTorrents(), (bool)result.Data); + + SelectedItems.Clear(); + await InvokeAsync(StateHasChanged); + } + + protected async Task AddTorrentFile(MouseEventArgs eventArgs) + { + await DialogService.InvokeAddTorrentFileDialog(ApiClient); + } + + protected async Task AddTorrentLink(MouseEventArgs eventArgs) + { + await DialogService.InvokeAddTorrentLinkDialog(ApiClient); + } + + protected void RowClick(TableRowClickEventArgs eventArgs) + { + if (eventArgs.MouseEventArgs.CtrlKey) + { + if (SelectedItems.Contains(eventArgs.Item)) + { + SelectedItems.Remove(eventArgs.Item); + } + else + { + SelectedItems.Add(eventArgs.Item); + } + + return; + } + + if (SelectedItems.Contains(eventArgs.Item)) + { + SelectedItems.Remove(eventArgs.Item); + } + else + { + SelectedItems.Clear(); + SelectedItems.Add(eventArgs.Item); + } + + SelectedTorrent = eventArgs.Item; + + if (eventArgs.MouseEventArgs.Detail > 1) + { + NavigationManager.NavigateTo("/details/" + SelectedTorrent); + } + } + + private IEnumerable GetSelectedTorrents() + { + if (SelectedItems.Count > 0) + { + return SelectedItems.Select(t => t.Hash); + } + + if (SelectedTorrent is not null) + { + return [SelectedTorrent.Hash]; + } + + return []; + } + + protected void Options(MouseEventArgs eventArgs) + { + NavigationManager.NavigateTo("/options"); + } + + protected async Task ColumnOptions() + { + DialogParameters parameters = new DialogParameters + { + { "Columns", _columns } + }; + + var reference = await DialogService.ShowAsync>("ColumnOptions", parameters, DialogHelper.FormDialogOptions); + + var result = await reference.Result; + if (result.Canceled) + { + return; + } + + SelectedColumns = (HashSet)result.Data; + } + + protected void ShowTorrent() + { + if (SelectedTorrent is null) + { + return; + } + NavigationManager.NavigateTo("/details/" + SelectedTorrent.Hash); + } + + protected string RowStyle(Torrent torrent, int index) + { + var style = "user-select: none; cursor: pointer;"; + if (torrent == SelectedTorrent) + { + style += " background: #D3D3D3"; + } + return style; + } + + protected HashSet SelectedColumns { get; set; } = new HashSet(); + + protected IEnumerable> GetColumns() + { + return _columns.Where(c => SelectedColumns.Contains(c.Id)); + } + + private void SetSort(Func sortSelector, SortDirection sortDirection) + { + _sortSelector = sortSelector; + _sortDirection = sortDirection; + } + + protected List> _columns = + [ + CreateColumnDefinition("#", t => t.Priority), + CreateColumnDefinition("State Icon", t => t.State, IconColumn), + CreateColumnDefinition("Name", t => t.Name, width: 200), + CreateColumnDefinition("Size", t => t.Size, t => DisplayHelpers.Size(t.Size)), + CreateColumnDefinition("Total Size", t => t.TotalSize, t => DisplayHelpers.Size(t.TotalSize), enabled: false), + CreateColumnDefinition("Done", t => t.Progress, ProgressBarColumn, tdClass: "table-progress pl-1 pr-1"), + CreateColumnDefinition("Status", t => t.State, t => DisplayHelpers.State(t.State)), + CreateColumnDefinition("Seeds", t => t.NumberSeeds), + CreateColumnDefinition("Peers", t => t.NumberLeeches), + CreateColumnDefinition("Down Speed", t => t.DownloadSpeed, t => DisplayHelpers.Speed(t.DownloadSpeed)), + CreateColumnDefinition("Up Speed", t => t.UploadSpeed, t => DisplayHelpers.Speed(t.UploadSpeed)), + CreateColumnDefinition("ETA", t => t.EstimatedTimeOfArrival, t => DisplayHelpers.Duration(t.EstimatedTimeOfArrival)), + CreateColumnDefinition("Ratio", t => t.Ratio, t => t.Ratio.ToString("0.00")), + CreateColumnDefinition("Category", t => t.Category), + CreateColumnDefinition("Tags", t => t.Tags, t => string.Join(", ", t.Tags)), + CreateColumnDefinition("Added On", t => t.AddedOn, t => DisplayHelpers.DateTime(t.AddedOn)), + CreateColumnDefinition("Completed On", t => t.CompletionOn, t => DisplayHelpers.DateTime(t.CompletionOn), enabled: false), + CreateColumnDefinition("Tracker", t => t.Tracker, enabled: false), + CreateColumnDefinition("Down Limit", t => t.DownloadLimit, t => DisplayHelpers.Size(t.DownloadLimit), enabled: false), + CreateColumnDefinition("Up Limit", t => t.UploadLimit, t => DisplayHelpers.Size(t.UploadLimit), enabled: false), + CreateColumnDefinition("Downloaded", t => t.Downloaded, t => DisplayHelpers.Size(t.Downloaded), enabled: false), + CreateColumnDefinition("Uploaded", t => t.Uploaded, t => DisplayHelpers.Size(t.Uploaded), enabled: false), + CreateColumnDefinition("Session Download", t => t.DownloadedSession, t => DisplayHelpers.Size(t.DownloadedSession), enabled: false), + CreateColumnDefinition("Session Upload", t => t.UploadedSession, t => DisplayHelpers.Size(t.UploadedSession), enabled: false), + CreateColumnDefinition("Remaining", t => t.AmountLeft, t => DisplayHelpers.Size(t.AmountLeft), enabled: false), + CreateColumnDefinition("Time Active", t => t.TimeActive, t => DisplayHelpers.Duration(t.TimeActive), enabled: false), + CreateColumnDefinition("Save path", t => t.SavePath, enabled: false), + CreateColumnDefinition("Completed", t => t.Completed, t => DisplayHelpers.DateTime(t.Completed), enabled: false), + CreateColumnDefinition("Ratio Limit", t => t.RatioLimit, t => t.Ratio.ToString("0.00"), enabled: false), + CreateColumnDefinition("Last Seen Complete", t => t.SeenComplete, t => DisplayHelpers.DateTime(t.SeenComplete), enabled: false), + CreateColumnDefinition("Last Activity", t => t.LastActivity, t => DisplayHelpers.DateTime(t.LastActivity), enabled: false), + CreateColumnDefinition("Availability", t => t.Availability, enabled: false), + //CreateColumnDefinition("Reannounce In", t => t.Reannounce, enabled: false), + ]; + + private Func? _sortSelector; + private SortDirection _sortDirection; + + private static ColumnDefinition CreateColumnDefinition(string name, Func selector, RenderFragment> rowTemplate, int? width = null, string? tdClass = null, bool enabled = true) + { + var cd = new ColumnDefinition(name, selector, rowTemplate); + cd.Class = "no-wrap"; + if (tdClass is not null) + { + cd.Class += " " + tdClass; + } + cd.Width = width; + cd.Enabled = enabled; + + return cd; + } + + private static ColumnDefinition CreateColumnDefinition(string name, Func selector, Func? formatter = null, int? width = null, string? tdClass = null, bool enabled = true) + { + var cd = new ColumnDefinition(name, selector, formatter); + cd.Class = "no-wrap"; + if (tdClass is not null) + { + cd.Class += " " + tdClass; + } + cd.Width = width; + cd.Enabled = enabled; + + return cd; + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Program.cs b/Lantean.QBTMudBlade/Program.cs new file mode 100644 index 0000000..d4856e9 --- /dev/null +++ b/Lantean.QBTMudBlade/Program.cs @@ -0,0 +1,45 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Services; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MudBlazor.Services; +using Blazored; +using Blazored.LocalStorage; + +namespace Lantean.QBTMudBlade +{ + public static class Program + { + public static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("#app"); + builder.RootComponents.Add("head::after"); + + builder.Services.AddMudServices(); + + Uri baseAddress; +#if DEBUG + baseAddress = new Uri("http://localhost:8080"); +#else + baseAddress = new Uri(builder.HostEnvironment.BaseAddress); +#endif + + builder.Services.AddTransient(); + builder.Services + .AddScoped(sp => sp + .GetRequiredService() + .CreateClient("API")) + .AddHttpClient("API", client => client.BaseAddress = new Uri(baseAddress, "/api/v2/")).AddHttpMessageHandler(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddSingleton(); + builder.Services.AddBlazoredLocalStorage(); + builder.Services.AddSingleton(); + + await builder.Build().RunAsync(); + } + } +} diff --git a/Lantean.QBTMudBlade/Properties/launchSettings.json b/Lantean.QBTMudBlade/Properties/launchSettings.json new file mode 100644 index 0000000..2c830b0 --- /dev/null +++ b/Lantean.QBTMudBlade/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28406", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5139", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Lantean.QBTMudBlade/Services/ClipboardService.cs b/Lantean.QBTMudBlade/Services/ClipboardService.cs new file mode 100644 index 0000000..327b580 --- /dev/null +++ b/Lantean.QBTMudBlade/Services/ClipboardService.cs @@ -0,0 +1,19 @@ +using Microsoft.JSInterop; + +namespace Lantean.QBTMudBlade.Services +{ + public class ClipboardService : IClipboardService + { + private readonly IJSRuntime _jSRuntime; + + public ClipboardService(IJSRuntime jSRuntime) + { + _jSRuntime = jSRuntime; + } + + public async Task WriteToClipboard(string text) + { + await _jSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text); + } + } +} diff --git a/Lantean.QBTMudBlade/Services/DataManager.cs b/Lantean.QBTMudBlade/Services/DataManager.cs new file mode 100644 index 0000000..5e6fe28 --- /dev/null +++ b/Lantean.QBTMudBlade/Services/DataManager.cs @@ -0,0 +1,1049 @@ +using Lantean.QBTMudBlade.Models; +using MudBlazor; +using System.Reflection; +using System.Threading.Channels; + +namespace Lantean.QBTMudBlade.Services +{ + public class DataManager : IDataManager + { + private static readonly Status[] _statuses = Enum.GetValues(); + + public PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers) + { + var peers = new Dictionary(); + if (torrentPeers.Peers is not null) + { + foreach (var (ip, peer) in torrentPeers.Peers) + { + var newPeer = CreatePeer(ip, peer); + + peers[ip] = newPeer; + } + } + + var peerList = new PeerList(peers); + + return peerList; + } + + public MainData CreateMainData(QBitTorrentClient.Models.MainData mainData) + { + var torrents = new Dictionary(mainData.Torrents?.Count ?? 0); + if (mainData.Torrents is not null) + { + foreach (var (hash, torrent) in mainData.Torrents) + { + var newTorrent = CreateTorrent(hash, torrent); + + torrents[hash] = newTorrent; + } + } + + var tags = new List(mainData.Tags?.Count ?? 0); + if (mainData.Tags is not null) + { + foreach (var tag in mainData.Tags) + { + tags.Add(tag); + } + } + + var categories = new Dictionary(mainData.Categories?.Count ?? 0); + if (mainData.Categories is not null) + { + foreach (var (name, category) in mainData.Categories) + { + var newCategory = CreateCategory(category); + + categories[name] = newCategory; + } + } + + var trackers = new Dictionary>(mainData.Trackers?.Count ?? 0); + if (mainData.Trackers is not null) + { + foreach (var (url, hashes) in mainData.Trackers) + { + trackers[url] = hashes; + } + } + + 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()); + foreach (var tag in tags) + { + tagState.Add(tag, torrents.Values.Where(t => FilterHelper.FilterTag(t, tag)).ToHashesHashSet()); + } + + var categoriesState = new Dictionary>(categories.Count + 2); + categoriesState.Add(FilterHelper.CATEGORY_ALL, torrents.Keys.ToHashSet()); + categoriesState.Add(FilterHelper.CATEGORY_UNCATEGORIZED, torrents.Values.Where(t => FilterHelper.FilterCategory(t, FilterHelper.CATEGORY_UNCATEGORIZED, serverState.UseSubcategories)).ToHashesHashSet()); + foreach (var category in categories.Keys) + { + categoriesState.Add(category, torrents.Values.Where(t => FilterHelper.FilterCategory(t, category, serverState.UseSubcategories)).ToHashesHashSet()); + } + + var statusState = new Dictionary>(_statuses.Length + 2); + foreach (var status in _statuses) + { + statusState.Add(status.ToString(), torrents.Values.Where(t => FilterHelper.FilterStatus(t, status)).ToHashesHashSet()); + } + + var trackersState = new Dictionary>(trackers.Count + 2); + trackersState.Add(FilterHelper.TRACKER_ALL, torrents.Keys.ToHashSet()); + trackersState.Add(FilterHelper.TRACKER_TRACKERLESS, torrents.Values.Where(t => FilterHelper.FilterTracker(t, FilterHelper.TRACKER_TRACKERLESS)).ToHashesHashSet()); + foreach (var tracker in trackers.Keys) + { + 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); + + return torrentList; + } + + private static ServerState CreateServerState(QBitTorrentClient.Models.ServerState? serverState) + { + if (serverState is null) + { + return new ServerState(); + } + return new ServerState( + serverState.AllTimeDownloaded!.Value, + serverState.AllTimeUploaded!.Value, + serverState.AverageTimeQueue!.Value, + serverState.ConnectionStatus!, + serverState.DHTNodes!.Value, + serverState.DownloadInfoData!.Value, + serverState.DownloadInfoSpeed!.Value, + serverState.DownloadRateLimit!.Value, + serverState.FreeSpaceOnDisk!.Value, + serverState.GlobalRatio!.Value, + serverState.QueuedIOJobs!.Value, + serverState.Queuing!.Value, + serverState.ReadCacheHits!.Value, + serverState.ReadCacheOverload!.Value, + serverState.RefreshInterval!.Value, + serverState.TotalBuffersSize!.Value, + serverState.TotalPeerConnections!.Value, + serverState.TotalQueuedSize!.Value, + serverState.TotalWastedSession!.Value, + serverState.UploadInfoData!.Value, + serverState.UploadInfoSpeed!.Value, + serverState.UploadRateLimit!.Value, + serverState.UseAltSpeedLimits!.Value, + serverState.UseSubcategories!.Value, + serverState.WriteCacheOverload!.Value); + } + + public void MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList) + { + if (mainData.CategoriesRemoved is not null) + { + foreach (var category in mainData.CategoriesRemoved) + { + torrentList.Categories.Remove(category); + torrentList.CategoriesState.Remove(category); + } + } + + if (mainData.TagsRemoved is not null) + { + foreach (var tag in mainData.TagsRemoved) + { + torrentList.Tags.Remove(tag); + torrentList.TagState.Remove(tag); + } + } + + if (mainData.TrackersRemoved is not null) + { + foreach (var tracker in mainData.TrackersRemoved) + { + torrentList.Trackers.Remove(tracker); + torrentList.TrackersState.Remove(tracker); + } + } + + if (mainData.TorrentsRemoved is not null) + { + foreach (var hash in mainData.TorrentsRemoved) + { + torrentList.Torrents.Remove(hash); + RemoveTorrentFromStates(torrentList, hash); + } + } + + if (mainData.Torrents is not null) + { + foreach (var (hash, torrent) in mainData.Torrents) + { + if (!torrentList.Torrents.TryGetValue(hash, out var existingTorrent)) + { + var newTorrent = CreateTorrent(hash, torrent); + torrentList.Torrents.Add(hash, newTorrent); + AddTorrentToStates(torrentList, hash); + } + else + { + UpdateTorrentStates(torrentList, hash); + UpdateTorrent(existingTorrent, torrent); + } + } + } + + if (mainData.Categories is not null) + { + foreach (var (name, category) in mainData.Categories) + { + if (!torrentList.Categories.TryGetValue(name, out var existingCategory)) + { + var newCategory = CreateCategory(category); + torrentList.Categories.Add(name, newCategory); + } + else + { + UpdateCategory(existingCategory, category); + } + } + } + + if (mainData.Tags is not null) + { + foreach (var tag in mainData.Tags) + { + torrentList.Tags.Add(tag); + } + } + + if (mainData.Trackers is not null) + { + foreach (var (url, hashes) in mainData.Trackers) + { + if (!torrentList.Trackers.TryGetValue(url, out var existingHashes)) + { + torrentList.Trackers.Add(url, hashes); + } + else + { + torrentList.Trackers[url] = hashes; + } + } + } + + if (mainData.ServerState is not null) + { + UpdateServerState(torrentList.ServerState, mainData.ServerState); + } + } + + private static void AddTorrentToStates(MainData torrentList, string hash) + { + var torrent = torrentList.Torrents[hash]; + + torrentList.TagState[FilterHelper.TAG_ALL].Add(hash); + torrentList.TagState[FilterHelper.TAG_UNTAGGED].AddIfTrue(hash, FilterHelper.FilterTag(torrent, FilterHelper.TAG_UNTAGGED)); + foreach (var tag in torrentList.Tags) + { + torrentList.TagState[tag].AddIfTrue(hash, FilterHelper.FilterTag(torrent, tag)); + } + + torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Add(hash); + torrentList.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].AddIfTrue(hash, FilterHelper.FilterCategory(torrent, FilterHelper.CATEGORY_UNCATEGORIZED, torrentList.ServerState.UseSubcategories)); + foreach (var category in torrentList.Categories.Keys) + { + torrentList.CategoriesState[category].AddIfTrue(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories)); + } + + foreach (var status in _statuses) + { + torrentList.StatusState[status.ToString()].AddIfTrue(hash, FilterHelper.FilterStatus(torrent, status)); + } + + torrentList.TrackersState[FilterHelper.TRACKER_ALL].Add(hash); + torrentList.TrackersState[FilterHelper.TRACKER_TRACKERLESS].AddIfTrue(hash, FilterHelper.FilterTracker(torrent, FilterHelper.TRACKER_TRACKERLESS)); + foreach (var tracker in torrentList.Trackers.Keys) + { + torrentList.TrackersState[tracker].AddIfTrue(hash, FilterHelper.FilterTracker(torrent, tracker)); + } + } + + private static void UpdateTorrentStates(MainData torrentList, string hash) + { + var torrent = torrentList.Torrents[hash]; + + torrentList.TagState[FilterHelper.TAG_UNTAGGED].AddIfTrueOrRemove(hash, FilterHelper.FilterTag(torrent, FilterHelper.TAG_UNTAGGED)); + foreach (var tag in torrentList.Tags) + { + torrentList.TagState[tag].AddIfTrueOrRemove(hash, FilterHelper.FilterTag(torrent, tag)); + } + + torrentList.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].AddIfTrueOrRemove(hash, FilterHelper.FilterCategory(torrent, FilterHelper.CATEGORY_UNCATEGORIZED, torrentList.ServerState.UseSubcategories)); + foreach (var category in torrentList.Categories.Keys) + { + torrentList.CategoriesState[category].AddIfTrueOrRemove(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories)); + } + + foreach (var status in _statuses) + { + torrentList.StatusState[status.ToString()].AddIfTrueOrRemove(hash, FilterHelper.FilterStatus(torrent, status)); + } + + torrentList.TrackersState[FilterHelper.TRACKER_TRACKERLESS].AddIfTrueOrRemove(hash, FilterHelper.FilterTracker(torrent, FilterHelper.TRACKER_TRACKERLESS)); + foreach (var tracker in torrentList.Trackers.Keys) + { + torrentList.TrackersState[tracker].AddIfTrueOrRemove(hash, FilterHelper.FilterTracker(torrent, tracker)); + } + } + + private static void RemoveTorrentFromStates(MainData torrentList, string hash) + { + var torrent = torrentList.Torrents[hash]; + + torrentList.TagState[FilterHelper.TAG_ALL].Remove(hash); + torrentList.TagState[FilterHelper.TAG_UNTAGGED].RemoveIfTrue(hash, FilterHelper.FilterTag(torrent, FilterHelper.TAG_UNTAGGED)); + foreach (var tag in torrentList.Tags) + { + torrentList.TagState[tag].RemoveIfTrue(hash, FilterHelper.FilterTag(torrent, tag)); + } + + torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Remove(hash); + torrentList.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].RemoveIfTrue(hash, FilterHelper.FilterCategory(torrent, FilterHelper.CATEGORY_UNCATEGORIZED, torrentList.ServerState.UseSubcategories)); + foreach (var category in torrentList.Categories.Keys) + { + torrentList.CategoriesState[category].RemoveIfTrue(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories)); + } + + foreach (var status in _statuses) + { + torrentList.StatusState[status.ToString()].RemoveIfTrue(hash, FilterHelper.FilterStatus(torrent, status)); + } + + torrentList.TrackersState[FilterHelper.TRACKER_ALL].Remove(hash); + torrentList.TrackersState[FilterHelper.TRACKER_TRACKERLESS].RemoveIfTrue(hash, FilterHelper.FilterTracker(torrent, FilterHelper.TRACKER_TRACKERLESS)); + foreach (var tracker in torrentList.Trackers.Keys) + { + torrentList.TrackersState[tracker].RemoveIfTrue(hash, FilterHelper.FilterTracker(torrent, tracker)); + } + } + + private static void UpdateServerState(ServerState existingServerState, QBitTorrentClient.Models.ServerState serverState) + { + existingServerState.AllTimeDownloaded = serverState.AllTimeDownloaded ?? existingServerState.AllTimeDownloaded; + existingServerState.AllTimeUploaded = serverState.AllTimeUploaded ?? existingServerState.AllTimeUploaded; + existingServerState.AverageTimeQueue = serverState.AverageTimeQueue ?? existingServerState.AverageTimeQueue; + existingServerState.ConnectionStatus = serverState.ConnectionStatus ?? existingServerState.ConnectionStatus; + existingServerState.DHTNodes = serverState.DHTNodes ?? existingServerState.DHTNodes; + existingServerState.DownloadInfoData = serverState.DownloadInfoData ?? existingServerState.DownloadInfoData; + existingServerState.DownloadInfoSpeed = serverState.DownloadInfoSpeed ?? existingServerState.DownloadInfoSpeed; + existingServerState.DownloadRateLimit = serverState.DownloadRateLimit ?? existingServerState.DownloadRateLimit; + existingServerState.FreeSpaceOnDisk = serverState.FreeSpaceOnDisk ?? existingServerState.FreeSpaceOnDisk; + existingServerState.GlobalRatio = serverState.GlobalRatio ?? existingServerState.GlobalRatio; + existingServerState.QueuedIOJobs = serverState.QueuedIOJobs ?? existingServerState.QueuedIOJobs; + existingServerState.Queuing = serverState.Queuing ?? existingServerState.Queuing; + existingServerState.ReadCacheHits = serverState.ReadCacheHits ?? existingServerState.ReadCacheHits; + existingServerState.ReadCacheOverload = serverState.ReadCacheOverload ?? existingServerState.ReadCacheOverload; + existingServerState.RefreshInterval = serverState.RefreshInterval ?? existingServerState.RefreshInterval; + existingServerState.TotalBuffersSize = serverState.TotalBuffersSize ?? existingServerState.TotalBuffersSize; + existingServerState.TotalPeerConnections = serverState.TotalPeerConnections ?? existingServerState.TotalPeerConnections; + existingServerState.TotalQueuedSize = serverState.TotalQueuedSize ?? existingServerState.TotalQueuedSize; + existingServerState.TotalWastedSession = serverState.TotalWastedSession ?? existingServerState.TotalWastedSession; + existingServerState.UploadInfoData = serverState.UploadInfoData ?? existingServerState.UploadInfoData; + existingServerState.UploadInfoSpeed = serverState.UploadInfoSpeed ?? existingServerState.UploadInfoSpeed; + existingServerState.UploadRateLimit = serverState.UploadRateLimit ?? existingServerState.UploadRateLimit; + existingServerState.UseAltSpeedLimits = serverState.UseAltSpeedLimits ?? existingServerState.UseAltSpeedLimits; + existingServerState.UseSubcategories = serverState.UseSubcategories ?? existingServerState.UseSubcategories; + existingServerState.WriteCacheOverload = serverState.WriteCacheOverload ?? existingServerState.WriteCacheOverload; + } + + public void MergeTorrentPeers(QBitTorrentClient.Models.TorrentPeers torrentPeers, PeerList peerList) + { + if (torrentPeers.PeersRemoved is not null) + { + foreach (var peer in torrentPeers.PeersRemoved) + { + peerList.Peers.Remove(peer); + } + } + + if (torrentPeers.Peers is not null) + { + foreach (var (ip, peer) in torrentPeers.Peers) + { + if (!peerList.Peers.TryGetValue(ip, out var existingPeer)) + { + var newPeer = CreatePeer(ip, peer); + peerList.Peers.Add(ip, newPeer); + } + else + { + UpdatePeer(existingPeer, peer); + } + } + } + } + + private static void UpdatePeer(Peer existingPeer, QBitTorrentClient.Models.Peer peer) + { + existingPeer.Client = peer.Client ?? existingPeer.Client; + existingPeer.ClientId = peer.ClientId ?? existingPeer.ClientId; + existingPeer.Connection = peer.Connection ?? existingPeer.Connection; + existingPeer.Country = peer.Country ?? existingPeer.Country; + existingPeer.CountryCode = peer.CountryCode ?? existingPeer.CountryCode; + existingPeer.Downloaded = peer.Downloaded ?? existingPeer.Downloaded; + existingPeer.DownloadSpeed = peer.DownloadSpeed ?? existingPeer.DownloadSpeed; + existingPeer.Files = peer.Files ?? existingPeer.Files; + existingPeer.Flags = peer.Flags ?? existingPeer.Flags; + existingPeer.FlagsDescription = peer.FlagsDescription ?? existingPeer.FlagsDescription; + existingPeer.IPAddress = peer.IPAddress ?? existingPeer.IPAddress; + existingPeer.Port = peer.Port ?? existingPeer.Port; + existingPeer.Progress = peer.Progress ?? existingPeer.Progress; + existingPeer.Relevance = peer.Relevance ?? existingPeer.Relevance; + existingPeer.Uploaded = peer.Uploaded ?? existingPeer.Uploaded; + existingPeer.UploadSpeed = peer.UploadSpeed ?? existingPeer.UploadSpeed; + } + + private static Category CreateCategory(QBitTorrentClient.Models.Category category) + { + return new Category(category.Name, category.SavePath!); + } + + private static Peer CreatePeer(string ip, QBitTorrentClient.Models.Peer peer) + { + return new Peer( + ip, + peer.Client!, + peer.ClientId!, + peer.Connection!, + peer.Country!, + peer.CountryCode!, + peer.Downloaded!.Value, + peer.DownloadSpeed!.Value, + peer.Files!, + peer.Flags!, + peer.FlagsDescription!, + peer.IPAddress!, + peer.Port!.Value, + peer.Progress!.Value, + peer.Relevance!.Value, + peer.Uploaded!.Value, + peer.UploadSpeed!.Value); + } + + public Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent) + { + return new Torrent( + hash, + torrent.AddedOn!.Value, + torrent.AmountLeft!.Value, + torrent.AutomaticTorrentManagement!.Value, + torrent.Availability!.Value, + torrent.Category!, + torrent.Completed!.Value, + torrent.CompletionOn!.Value, + torrent.ContentPath!, + torrent.DownloadLimit!.Value, + torrent.DownloadSpeed!.Value, + torrent.Downloaded!.Value, + torrent.DownloadedSession!.Value, + torrent.EstimatedTimeOfArrival!.Value, + torrent.FirstLastPiecePriority!.Value, + torrent.ForceStart!.Value, + torrent.InfoHashV1!, + torrent.InfoHashV2!, + torrent.LastActivity!.Value, + torrent.MagnetUri!, + torrent.MaxRatio!.Value, + torrent.MaxSeedingTime!.Value, + torrent.Name!, + torrent.NumberComplete!.Value, + torrent.NumberIncomplete!.Value, + torrent.NumberLeeches!.Value, + torrent.NumberSeeds!.Value, + torrent.Priority!.Value, + torrent.Progress!.Value, + torrent.Ratio!.Value, + torrent.RatioLimit!.Value, + torrent.SavePath!, + torrent.SeedingTime!.Value, + torrent.SeedingTimeLimit!.Value, + torrent.SeenComplete!.Value, + torrent.SequentialDownload!.Value, + torrent.Size!.Value, + torrent.State!, + torrent.SuperSeeding!.Value, + torrent.Tags!, + torrent.TimeActive!.Value, + torrent.TotalSize!.Value, + torrent.Tracker!, + torrent.UploadLimit!.Value, + torrent.Uploaded!.Value, + torrent.UploadedSession!.Value, + torrent.UploadSpeed!.Value, + torrent.Reannounce ?? 0); + } + + private static void UpdateCategory(Category existingCategory, QBitTorrentClient.Models.Category category) + { + existingCategory.SavePath = category.SavePath ?? existingCategory.SavePath; + } + + private static void UpdateTorrent(Torrent existingTorrent, QBitTorrentClient.Models.Torrent torrent) + { + existingTorrent.AddedOn = torrent.AddedOn ?? existingTorrent.AddedOn; + existingTorrent.AmountLeft = torrent.AmountLeft ?? existingTorrent.AmountLeft; + existingTorrent.AutomaticTorrentManagement = torrent.AutomaticTorrentManagement ?? existingTorrent.AutomaticTorrentManagement; + existingTorrent.Availability = torrent.Availability ?? existingTorrent.Availability; + existingTorrent.Category = torrent.Category ?? existingTorrent.Category; + existingTorrent.Completed = torrent.Completed ?? existingTorrent.Completed; + existingTorrent.CompletionOn = torrent.CompletionOn ?? existingTorrent.CompletionOn; + existingTorrent.ContentPath = torrent.ContentPath ?? existingTorrent.ContentPath; + existingTorrent.Downloaded = torrent.Downloaded ?? existingTorrent.Downloaded; + existingTorrent.DownloadedSession = torrent.DownloadedSession ?? existingTorrent.DownloadedSession; + existingTorrent.DownloadLimit = torrent.DownloadLimit ?? existingTorrent.DownloadLimit; + existingTorrent.DownloadSpeed = torrent.DownloadSpeed ?? existingTorrent.DownloadSpeed; + existingTorrent.EstimatedTimeOfArrival = torrent.EstimatedTimeOfArrival ?? existingTorrent.EstimatedTimeOfArrival; + existingTorrent.FirstLastPiecePriority = torrent.FirstLastPiecePriority ?? existingTorrent.FirstLastPiecePriority; + existingTorrent.ForceStart = torrent.ForceStart ?? existingTorrent.ForceStart; + existingTorrent.InfoHashV1 = torrent.InfoHashV1 ?? existingTorrent.InfoHashV1; + existingTorrent.InfoHashV2 = torrent.InfoHashV2 ?? existingTorrent.InfoHashV2; + existingTorrent.LastActivity = torrent.LastActivity ?? existingTorrent.LastActivity; + existingTorrent.MagnetUri = torrent.MagnetUri ?? existingTorrent.MagnetUri; + existingTorrent.MaxRatio = torrent.MaxRatio ?? existingTorrent.MaxRatio; + existingTorrent.MaxSeedingTime = torrent.MaxSeedingTime ?? existingTorrent.MaxSeedingTime; + existingTorrent.Name = torrent.Name ?? existingTorrent.Name; + existingTorrent.NumberComplete = torrent.NumberComplete ?? existingTorrent.NumberComplete; + existingTorrent.NumberIncomplete = torrent.NumberIncomplete ?? existingTorrent.NumberIncomplete; + existingTorrent.NumberLeeches = torrent.NumberLeeches ?? existingTorrent.NumberLeeches; + existingTorrent.NumberSeeds = torrent.NumberSeeds ?? existingTorrent.NumberSeeds; + existingTorrent.Priority = torrent.Priority ?? existingTorrent.Priority; + existingTorrent.Progress = torrent.Progress ?? existingTorrent.Progress; + existingTorrent.Ratio = torrent.Ratio ?? existingTorrent.Ratio; + existingTorrent.RatioLimit = torrent.RatioLimit ?? existingTorrent.RatioLimit; + existingTorrent.SavePath = torrent.SavePath ?? existingTorrent.SavePath; + existingTorrent.SeedingTime = torrent.SeedingTime ?? existingTorrent.SeedingTime; + existingTorrent.SeedingTimeLimit = torrent.SeedingTimeLimit ?? existingTorrent.SeedingTimeLimit; + existingTorrent.SeenComplete = torrent.SeenComplete ?? existingTorrent.SeenComplete; + existingTorrent.SequentialDownload = torrent.SequentialDownload ?? existingTorrent.SequentialDownload; + existingTorrent.Size = torrent.Size ?? existingTorrent.Size; + existingTorrent.State = torrent.State ?? existingTorrent.State; + existingTorrent.SuperSeeding = torrent.SuperSeeding ?? existingTorrent.SuperSeeding; + if (torrent.Tags is not null) + { + existingTorrent.Tags.Clear(); + existingTorrent.Tags.AddRange(torrent.Tags); + } + existingTorrent.TimeActive = torrent.TimeActive ?? existingTorrent.TimeActive; + existingTorrent.TotalSize = torrent.TotalSize ?? existingTorrent.TotalSize; + existingTorrent.Tracker = torrent.Tracker ?? existingTorrent.Tracker; + existingTorrent.UploadLimit = torrent.UploadLimit ?? existingTorrent.UploadLimit; + existingTorrent.Uploaded = torrent.Uploaded ?? existingTorrent.Uploaded; + existingTorrent.UploadedSession = torrent.UploadedSession ?? existingTorrent.UploadedSession; + existingTorrent.UploadSpeed = torrent.UploadSpeed ?? existingTorrent.UploadSpeed; + existingTorrent.Reannounce = torrent.Reannounce ?? existingTorrent.Reannounce; + } + + public Dictionary CreateContentsList(IReadOnlyList files) + { + var contents = new Dictionary(); + + var folderIndex = files.Max(f => f.Index) + 1; + + foreach (var file in files) + { + if (!file.Name.Contains(Extensions.DirectorySeparator)) + { + contents.Add(file.Name, new ContentItem(file.Name, file.Name, file.Index, (Priority)(int)file.Priority, file.Progress, file.Size, file.Availability)); + } + else + { + var nameAndPath = file.Name.Split(Extensions.DirectorySeparator); + var paths = nameAndPath[..^1]; + for (var i = 0; i < paths.Length; i++) + { + var directoryName = paths[i]; + var directoryPath = string.Join(Extensions.DirectorySeparator, paths[0..(i + 1)]); + if (!contents.ContainsKey(directoryPath)) + { + contents.Add(directoryPath, new ContentItem(directoryPath, directoryName, folderIndex++, Priority.Normal, 0, 0, 0, true, i)); + } + } + + var displayName = nameAndPath[^1]; + + contents.Add(file.Name, new ContentItem(file.Name, displayName, file.Index, (Priority)(int)file.Priority, file.Progress, file.Size, file.Availability, false, paths.Length)); + } + } + + var directories = contents.Where(c => c.Value.IsFolder).OrderByDescending(c => c.Value.Level); + + foreach (var key in directories.Select(d => d.Key)) + { + var directoryContents = contents.Where(c => c.Value.Name.StartsWith(key + Extensions.DirectorySeparator) && !c.Value.IsFolder); + var size = directoryContents.Sum(c => c.Value.Size); + var availability = directoryContents.Average(c => c.Value.Availability); + var downloaded = directoryContents.Sum(c => c.Value.Downloaded); + var progress = (float)downloaded / size; + + var content = contents[key]; + content.Availability = availability; + content.Size = size; + content.Progress = progress; + var priorities = directoryContents.Select(d => d.Value.Priority).Distinct(); + if (priorities.Count() == 1) + { + content.Priority = priorities.First(); + } + else + { + content.Priority = Priority.Mixed; + } + } + + return contents; + } + + public QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed) + { + if (original is null) + { + original = new QBitTorrentClient.Models.UpdatePreferences + { + AddToTopOfQueue = changed.AddToTopOfQueue, + AddTrackers = changed.AddTrackers, + AddTrackersEnabled = changed.AddTrackersEnabled, + AltDlLimit = changed.AltDlLimit, + AltUpLimit = changed.AltUpLimit, + AlternativeWebuiEnabled = changed.AlternativeWebuiEnabled, + AlternativeWebuiPath = changed.AlternativeWebuiPath, + AnnounceIp = changed.AnnounceIp, + AnnounceToAllTiers = changed.AnnounceToAllTiers, + AnnounceToAllTrackers = changed.AnnounceToAllTrackers, + AnonymousMode = changed.AnonymousMode, + AsyncIoThreads = changed.AsyncIoThreads, + AutoDeleteMode = changed.AutoDeleteMode, + AutoTmmEnabled = changed.AutoTmmEnabled, + AutorunEnabled = changed.AutorunEnabled, + AutorunOnTorrentAddedEnabled = changed.AutorunOnTorrentAddedEnabled, + AutorunOnTorrentAddedProgram = changed.AutorunOnTorrentAddedProgram, + AutorunProgram = changed.AutorunProgram, + BannedIPs = changed.BannedIPs, + BdecodeDepthLimit = changed.BdecodeDepthLimit, + BdecodeTokenLimit = changed.BdecodeTokenLimit, + BittorrentProtocol = changed.BittorrentProtocol, + BlockPeersOnPrivilegedPorts = changed.BlockPeersOnPrivilegedPorts, + BypassAuthSubnetWhitelist = changed.BypassAuthSubnetWhitelist, + BypassAuthSubnetWhitelistEnabled = changed.BypassAuthSubnetWhitelistEnabled, + BypassLocalAuth = changed.BypassLocalAuth, + CategoryChangedTmmEnabled = changed.CategoryChangedTmmEnabled, + CheckingMemoryUse = changed.CheckingMemoryUse, + ConnectionSpeed = changed.ConnectionSpeed, + CurrentInterfaceAddress = changed.CurrentInterfaceAddress, + CurrentInterfaceName = changed.CurrentInterfaceName, + CurrentNetworkInterface = changed.CurrentNetworkInterface, + Dht = changed.Dht, + DiskCache = changed.DiskCache, + DiskCacheTtl = changed.DiskCacheTtl, + DiskIoReadMode = changed.DiskIoReadMode, + DiskIoType = changed.DiskIoType, + DiskIoWriteMode = changed.DiskIoWriteMode, + DiskQueueSize = changed.DiskQueueSize, + DlLimit = changed.DlLimit, + DontCountSlowTorrents = changed.DontCountSlowTorrents, + DyndnsDomain = changed.DyndnsDomain, + DyndnsEnabled = changed.DyndnsEnabled, + DyndnsPassword = changed.DyndnsPassword, + DyndnsService = changed.DyndnsService, + DyndnsUsername = changed.DyndnsUsername, + EmbeddedTrackerPort = changed.EmbeddedTrackerPort, + EmbeddedTrackerPortForwarding = changed.EmbeddedTrackerPortForwarding, + EnableCoalesceReadWrite = changed.EnableCoalesceReadWrite, + EnableEmbeddedTracker = changed.EnableEmbeddedTracker, + EnableMultiConnectionsFromSameIp = changed.EnableMultiConnectionsFromSameIp, + EnablePieceExtentAffinity = changed.EnablePieceExtentAffinity, + EnableUploadSuggestions = changed.EnableUploadSuggestions, + Encryption = changed.Encryption, + ExcludedFileNames = changed.ExcludedFileNames, + ExcludedFileNamesEnabled = changed.ExcludedFileNamesEnabled, + ExportDir = changed.ExportDir, + ExportDirFin = changed.ExportDirFin, + FileLogAge = changed.FileLogAge, + FileLogAgeType = changed.FileLogAgeType, + FileLogBackupEnabled = changed.FileLogBackupEnabled, + FileLogDeleteOld = changed.FileLogDeleteOld, + FileLogEnabled = changed.FileLogEnabled, + FileLogMaxSize = changed.FileLogMaxSize, + FileLogPath = changed.FileLogPath, + FilePoolSize = changed.FilePoolSize, + HashingThreads = changed.HashingThreads, + I2pAddress = changed.I2pAddress, + I2pEnabled = changed.I2pEnabled, + I2pInboundLength = changed.I2pInboundLength, + I2pInboundQuantity = changed.I2pInboundQuantity, + I2pMixedMode = changed.I2pMixedMode, + I2pOutboundLength = changed.I2pOutboundLength, + I2pOutboundQuantity = changed.I2pOutboundQuantity, + I2pPort = changed.I2pPort, + IdnSupportEnabled = changed.IdnSupportEnabled, + IncompleteFilesExt = changed.IncompleteFilesExt, + IpFilterEnabled = changed.IpFilterEnabled, + IpFilterPath = changed.IpFilterPath, + IpFilterTrackers = changed.IpFilterTrackers, + LimitLanPeers = changed.LimitLanPeers, + LimitTcpOverhead = changed.LimitTcpOverhead, + LimitUtpRate = changed.LimitUtpRate, + ListenPort = changed.ListenPort, + Locale = changed.Locale, + Lsd = changed.Lsd, + MailNotificationAuthEnabled = changed.MailNotificationAuthEnabled, + MailNotificationEmail = changed.MailNotificationEmail, + MailNotificationEnabled = changed.MailNotificationEnabled, + MailNotificationPassword = changed.MailNotificationPassword, + MailNotificationSender = changed.MailNotificationSender, + MailNotificationSmtp = changed.MailNotificationSmtp, + MailNotificationSslEnabled = changed.MailNotificationSslEnabled, + MailNotificationUsername = changed.MailNotificationUsername, + MaxActiveCheckingTorrents = changed.MaxActiveCheckingTorrents, + MaxActiveDownloads = changed.MaxActiveDownloads, + MaxActiveTorrents = changed.MaxActiveTorrents, + MaxActiveUploads = changed.MaxActiveUploads, + MaxConcurrentHttpAnnounces = changed.MaxConcurrentHttpAnnounces, + MaxConnec = changed.MaxConnec, + MaxConnecPerTorrent = changed.MaxConnecPerTorrent, + MaxInactiveSeedingTime = changed.MaxInactiveSeedingTime, + MaxInactiveSeedingTimeEnabled = changed.MaxInactiveSeedingTimeEnabled, + MaxRatio = changed.MaxRatio, + MaxRatioAct = changed.MaxRatioAct, + MaxRatioEnabled = changed.MaxRatioEnabled, + MaxSeedingTime = changed.MaxSeedingTime, + MaxSeedingTimeEnabled = changed.MaxSeedingTimeEnabled, + MaxUploads = changed.MaxUploads, + MaxUploadsPerTorrent = changed.MaxUploadsPerTorrent, + MemoryWorkingSetLimit = changed.MemoryWorkingSetLimit, + MergeTrackers = changed.MergeTrackers, + OutgoingPortsMax = changed.OutgoingPortsMax, + OutgoingPortsMin = changed.OutgoingPortsMin, + PeerTos = changed.PeerTos, + PeerTurnover = changed.PeerTurnover, + PeerTurnoverCutoff = changed.PeerTurnoverCutoff, + PeerTurnoverInterval = changed.PeerTurnoverInterval, + PerformanceWarning = changed.PerformanceWarning, + Pex = changed.Pex, + PreallocateAll = changed.PreallocateAll, + ProxyAuthEnabled = changed.ProxyAuthEnabled, + ProxyBittorrent = changed.ProxyBittorrent, + ProxyHostnameLookup = changed.ProxyHostnameLookup, + ProxyIp = changed.ProxyIp, + ProxyMisc = changed.ProxyMisc, + ProxyPassword = changed.ProxyPassword, + ProxyPeerConnections = changed.ProxyPeerConnections, + ProxyPort = changed.ProxyPort, + ProxyRss = changed.ProxyRss, + ProxyType = changed.ProxyType, + ProxyUsername = changed.ProxyUsername, + QueueingEnabled = changed.QueueingEnabled, + RandomPort = changed.RandomPort, + ReannounceWhenAddressChanged = changed.ReannounceWhenAddressChanged, + RecheckCompletedTorrents = changed.RecheckCompletedTorrents, + RefreshInterval = changed.RefreshInterval, + RequestQueueSize = changed.RequestQueueSize, + ResolvePeerCountries = changed.ResolvePeerCountries, + ResumeDataStorageType = changed.ResumeDataStorageType, + RssAutoDownloadingEnabled = changed.RssAutoDownloadingEnabled, + RssDownloadRepackProperEpisodes = changed.RssDownloadRepackProperEpisodes, + RssMaxArticlesPerFeed = changed.RssMaxArticlesPerFeed, + RssProcessingEnabled = changed.RssProcessingEnabled, + RssRefreshInterval = changed.RssRefreshInterval, + RssSmartEpisodeFilters = changed.RssSmartEpisodeFilters, + SavePath = changed.SavePath, + SavePathChangedTmmEnabled = changed.SavePathChangedTmmEnabled, + SaveResumeDataInterval = changed.SaveResumeDataInterval, + ScanDirs = changed.ScanDirs, + ScheduleFromHour = changed.ScheduleFromHour, + ScheduleFromMin = changed.ScheduleFromMin, + ScheduleToHour = changed.ScheduleToHour, + ScheduleToMin = changed.ScheduleToMin, + SchedulerDays = changed.SchedulerDays, + SchedulerEnabled = changed.SchedulerEnabled, + SendBufferLowWatermark = changed.SendBufferLowWatermark, + SendBufferWatermark = changed.SendBufferWatermark, + SendBufferWatermarkFactor = changed.SendBufferWatermarkFactor, + SlowTorrentDlRateThreshold = changed.SlowTorrentDlRateThreshold, + SlowTorrentInactiveTimer = changed.SlowTorrentInactiveTimer, + SlowTorrentUlRateThreshold = changed.SlowTorrentUlRateThreshold, + SocketBacklogSize = changed.SocketBacklogSize, + SocketReceiveBufferSize = changed.SocketReceiveBufferSize, + SocketSendBufferSize = changed.SocketSendBufferSize, + SsrfMitigation = changed.SsrfMitigation, + StartPausedEnabled = changed.StartPausedEnabled, + StopTrackerTimeout = changed.StopTrackerTimeout, + TempPath = changed.TempPath, + TempPathEnabled = changed.TempPathEnabled, + TorrentChangedTmmEnabled = changed.TorrentChangedTmmEnabled, + TorrentContentLayout = changed.TorrentContentLayout, + TorrentFileSizeLimit = changed.TorrentFileSizeLimit, + TorrentStopCondition = changed.TorrentStopCondition, + UpLimit = changed.UpLimit, + UploadChokingAlgorithm = changed.UploadChokingAlgorithm, + UploadSlotsBehavior = changed.UploadSlotsBehavior, + Upnp = changed.Upnp, + UpnpLeaseDuration = changed.UpnpLeaseDuration, + UseCategoryPathsInManualMode = changed.UseCategoryPathsInManualMode, + UseHttps = changed.UseHttps, + UseSubcategories = changed.UseSubcategories, + UtpTcpMixedMode = changed.UtpTcpMixedMode, + ValidateHttpsTrackerCertificate = changed.ValidateHttpsTrackerCertificate, + WebUiAddress = changed.WebUiAddress, + WebUiBanDuration = changed.WebUiBanDuration, + WebUiClickjackingProtectionEnabled = changed.WebUiClickjackingProtectionEnabled, + WebUiCsrfProtectionEnabled = changed.WebUiCsrfProtectionEnabled, + WebUiCustomHttpHeaders = changed.WebUiCustomHttpHeaders, + WebUiDomainList = changed.WebUiDomainList, + WebUiHostHeaderValidationEnabled = changed.WebUiHostHeaderValidationEnabled, + WebUiHttpsCertPath = changed.WebUiHttpsCertPath, + WebUiHttpsKeyPath = changed.WebUiHttpsKeyPath, + WebUiMaxAuthFailCount = changed.WebUiMaxAuthFailCount, + WebUiPort = changed.WebUiPort, + WebUiReverseProxiesList = changed.WebUiReverseProxiesList, + WebUiReverseProxyEnabled = changed.WebUiReverseProxyEnabled, + WebUiSecureCookieEnabled = changed.WebUiSecureCookieEnabled, + WebUiSessionTimeout = changed.WebUiSessionTimeout, + WebUiUpnp = changed.WebUiUpnp, + WebUiUseCustomHttpHeadersEnabled = changed.WebUiUseCustomHttpHeadersEnabled, + WebUiUsername = changed.WebUiUsername + }; + } + else + { + original.AddToTopOfQueue = changed.AddToTopOfQueue ?? original.AddToTopOfQueue; + original.AddTrackers = changed.AddTrackers ?? original.AddTrackers; + original.AddTrackersEnabled = changed.AddTrackersEnabled ?? original.AddTrackersEnabled; + original.AltDlLimit = changed.AltDlLimit ?? original.AltDlLimit; + original.AltUpLimit = changed.AltUpLimit ?? original.AltUpLimit; + original.AlternativeWebuiEnabled = changed.AlternativeWebuiEnabled ?? original.AlternativeWebuiEnabled; + original.AlternativeWebuiPath = changed.AlternativeWebuiPath ?? original.AlternativeWebuiPath; + original.AnnounceIp = changed.AnnounceIp ?? original.AnnounceIp; + original.AnnounceToAllTiers = changed.AnnounceToAllTiers ?? original.AnnounceToAllTiers; + original.AnnounceToAllTrackers = changed.AnnounceToAllTrackers ?? original.AnnounceToAllTrackers; + original.AnonymousMode = changed.AnonymousMode ?? original.AnonymousMode; + original.AsyncIoThreads = changed.AsyncIoThreads ?? original.AsyncIoThreads; + original.AutoDeleteMode = changed.AutoDeleteMode ?? original.AutoDeleteMode; + original.AutoTmmEnabled = changed.AutoTmmEnabled ?? original.AutoTmmEnabled; + original.AutorunEnabled = changed.AutorunEnabled ?? original.AutorunEnabled; + original.AutorunOnTorrentAddedEnabled = changed.AutorunOnTorrentAddedEnabled ?? original.AutorunOnTorrentAddedEnabled; + original.AutorunOnTorrentAddedProgram = changed.AutorunOnTorrentAddedProgram ?? original.AutorunOnTorrentAddedProgram; + original.AutorunProgram = changed.AutorunProgram ?? original.AutorunProgram; + original.BannedIPs = changed.BannedIPs ?? original.BannedIPs; + original.BdecodeDepthLimit = changed.BdecodeDepthLimit ?? original.BdecodeDepthLimit; + original.BdecodeTokenLimit = changed.BdecodeTokenLimit ?? original.BdecodeTokenLimit; + original.BittorrentProtocol = changed.BittorrentProtocol ?? original.BittorrentProtocol; + original.BlockPeersOnPrivilegedPorts = changed.BlockPeersOnPrivilegedPorts ?? original.BlockPeersOnPrivilegedPorts; + original.BypassAuthSubnetWhitelist = changed.BypassAuthSubnetWhitelist ?? original.BypassAuthSubnetWhitelist; + original.BypassAuthSubnetWhitelistEnabled = changed.BypassAuthSubnetWhitelistEnabled ?? original.BypassAuthSubnetWhitelistEnabled; + original.BypassLocalAuth = changed.BypassLocalAuth ?? original.BypassLocalAuth; + original.CategoryChangedTmmEnabled = changed.CategoryChangedTmmEnabled ?? original.CategoryChangedTmmEnabled; + original.CheckingMemoryUse = changed.CheckingMemoryUse ?? original.CheckingMemoryUse; + original.ConnectionSpeed = changed.ConnectionSpeed ?? original.ConnectionSpeed; + original.CurrentInterfaceAddress = changed.CurrentInterfaceAddress ?? original.CurrentInterfaceAddress; + original.CurrentInterfaceName = changed.CurrentInterfaceName ?? original.CurrentInterfaceName; + original.CurrentNetworkInterface = changed.CurrentNetworkInterface ?? original.CurrentNetworkInterface; + original.Dht = changed.Dht ?? original.Dht; + original.DiskCache = changed.DiskCache ?? original.DiskCache; + original.DiskCacheTtl = changed.DiskCacheTtl ?? original.DiskCacheTtl; + original.DiskIoReadMode = changed.DiskIoReadMode ?? original.DiskIoReadMode; + original.DiskIoType = changed.DiskIoType ?? original.DiskIoType; + original.DiskIoWriteMode = changed.DiskIoWriteMode ?? original.DiskIoWriteMode; + original.DiskQueueSize = changed.DiskQueueSize ?? original.DiskQueueSize; + original.DlLimit = changed.DlLimit ?? original.DlLimit; + original.DontCountSlowTorrents = changed.DontCountSlowTorrents ?? original.DontCountSlowTorrents; + original.DyndnsDomain = changed.DyndnsDomain ?? original.DyndnsDomain; + original.DyndnsEnabled = changed.DyndnsEnabled ?? original.DyndnsEnabled; + original.DyndnsPassword = changed.DyndnsPassword ?? original.DyndnsPassword; + original.DyndnsService = changed.DyndnsService ?? original.DyndnsService; + original.DyndnsUsername = changed.DyndnsUsername ?? original.DyndnsUsername; + original.EmbeddedTrackerPort = changed.EmbeddedTrackerPort ?? original.EmbeddedTrackerPort; + original.EmbeddedTrackerPortForwarding = changed.EmbeddedTrackerPortForwarding ?? original.EmbeddedTrackerPortForwarding; + original.EnableCoalesceReadWrite = changed.EnableCoalesceReadWrite ?? original.EnableCoalesceReadWrite; + original.EnableEmbeddedTracker = changed.EnableEmbeddedTracker ?? original.EnableEmbeddedTracker; + original.EnableMultiConnectionsFromSameIp = changed.EnableMultiConnectionsFromSameIp ?? original.EnableMultiConnectionsFromSameIp; + original.EnablePieceExtentAffinity = changed.EnablePieceExtentAffinity ?? original.EnablePieceExtentAffinity; + original.EnableUploadSuggestions = changed.EnableUploadSuggestions ?? original.EnableUploadSuggestions; + original.Encryption = changed.Encryption ?? original.Encryption; + original.ExcludedFileNames = changed.ExcludedFileNames ?? original.ExcludedFileNames; + original.ExcludedFileNamesEnabled = changed.ExcludedFileNamesEnabled ?? original.ExcludedFileNamesEnabled; + original.ExportDir = changed.ExportDir ?? original.ExportDir; + original.ExportDirFin = changed.ExportDirFin ?? original.ExportDirFin; + original.FileLogAge = changed.FileLogAge ?? original.FileLogAge; + original.FileLogAgeType = changed.FileLogAgeType ?? original.FileLogAgeType; + original.FileLogBackupEnabled = changed.FileLogBackupEnabled ?? original.FileLogBackupEnabled; + original.FileLogDeleteOld = changed.FileLogDeleteOld ?? original.FileLogDeleteOld; + original.FileLogEnabled = changed.FileLogEnabled ?? original.FileLogEnabled; + original.FileLogMaxSize = changed.FileLogMaxSize ?? original.FileLogMaxSize; + original.FileLogPath = changed.FileLogPath ?? original.FileLogPath; + original.FilePoolSize = changed.FilePoolSize ?? original.FilePoolSize; + original.HashingThreads = changed.HashingThreads ?? original.HashingThreads; + original.I2pAddress = changed.I2pAddress ?? original.I2pAddress; + original.I2pEnabled = changed.I2pEnabled ?? original.I2pEnabled; + original.I2pInboundLength = changed.I2pInboundLength ?? original.I2pInboundLength; + original.I2pInboundQuantity = changed.I2pInboundQuantity ?? original.I2pInboundQuantity; + original.I2pMixedMode = changed.I2pMixedMode ?? original.I2pMixedMode; + original.I2pOutboundLength = changed.I2pOutboundLength ?? original.I2pOutboundLength; + original.I2pOutboundQuantity = changed.I2pOutboundQuantity ?? original.I2pOutboundQuantity; + original.I2pPort = changed.I2pPort ?? original.I2pPort; + original.IdnSupportEnabled = changed.IdnSupportEnabled ?? original.IdnSupportEnabled; + original.IncompleteFilesExt = changed.IncompleteFilesExt ?? original.IncompleteFilesExt; + original.IpFilterEnabled = changed.IpFilterEnabled ?? original.IpFilterEnabled; + original.IpFilterPath = changed.IpFilterPath ?? original.IpFilterPath; + original.IpFilterTrackers = changed.IpFilterTrackers ?? original.IpFilterTrackers; + original.LimitLanPeers = changed.LimitLanPeers ?? original.LimitLanPeers; + original.LimitTcpOverhead = changed.LimitTcpOverhead ?? original.LimitTcpOverhead; + original.LimitUtpRate = changed.LimitUtpRate ?? original.LimitUtpRate; + original.ListenPort = changed.ListenPort ?? original.ListenPort; + original.Locale = changed.Locale ?? original.Locale; + original.Lsd = changed.Lsd ?? original.Lsd; + original.MailNotificationAuthEnabled = changed.MailNotificationAuthEnabled ?? original.MailNotificationAuthEnabled; + original.MailNotificationEmail = changed.MailNotificationEmail ?? original.MailNotificationEmail; + original.MailNotificationEnabled = changed.MailNotificationEnabled ?? original.MailNotificationEnabled; + original.MailNotificationPassword = changed.MailNotificationPassword ?? original.MailNotificationPassword; + original.MailNotificationSender = changed.MailNotificationSender ?? original.MailNotificationSender; + original.MailNotificationSmtp = changed.MailNotificationSmtp ?? original.MailNotificationSmtp; + original.MailNotificationSslEnabled = changed.MailNotificationSslEnabled ?? original.MailNotificationSslEnabled; + original.MailNotificationUsername = changed.MailNotificationUsername ?? original.MailNotificationUsername; + original.MaxActiveCheckingTorrents = changed.MaxActiveCheckingTorrents ?? original.MaxActiveCheckingTorrents; + original.MaxActiveDownloads = changed.MaxActiveDownloads ?? original.MaxActiveDownloads; + original.MaxActiveTorrents = changed.MaxActiveTorrents ?? original.MaxActiveTorrents; + original.MaxActiveUploads = changed.MaxActiveUploads ?? original.MaxActiveUploads; + original.MaxConcurrentHttpAnnounces = changed.MaxConcurrentHttpAnnounces ?? original.MaxConcurrentHttpAnnounces; + original.MaxConnec = changed.MaxConnec ?? original.MaxConnec; + original.MaxConnecPerTorrent = changed.MaxConnecPerTorrent ?? original.MaxConnecPerTorrent; + original.MaxInactiveSeedingTime = changed.MaxInactiveSeedingTime ?? original.MaxInactiveSeedingTime; + original.MaxInactiveSeedingTimeEnabled = changed.MaxInactiveSeedingTimeEnabled ?? original.MaxInactiveSeedingTimeEnabled; + original.MaxRatio = changed.MaxRatio ?? original.MaxRatio; + original.MaxRatioAct = changed.MaxRatioAct ?? original.MaxRatioAct; + original.MaxRatioEnabled = changed.MaxRatioEnabled ?? original.MaxRatioEnabled; + original.MaxSeedingTime = changed.MaxSeedingTime ?? original.MaxSeedingTime; + original.MaxSeedingTimeEnabled = changed.MaxSeedingTimeEnabled ?? original.MaxSeedingTimeEnabled; + original.MaxUploads = changed.MaxUploads ?? original.MaxUploads; + original.MaxUploadsPerTorrent = changed.MaxUploadsPerTorrent ?? original.MaxUploadsPerTorrent; + original.MemoryWorkingSetLimit = changed.MemoryWorkingSetLimit ?? original.MemoryWorkingSetLimit; + original.MergeTrackers = changed.MergeTrackers ?? original.MergeTrackers; + original.OutgoingPortsMax = changed.OutgoingPortsMax ?? original.OutgoingPortsMax; + original.OutgoingPortsMin = changed.OutgoingPortsMin ?? original.OutgoingPortsMin; + original.PeerTos = changed.PeerTos ?? original.PeerTos; + original.PeerTurnover = changed.PeerTurnover ?? original.PeerTurnover; + original.PeerTurnoverCutoff = changed.PeerTurnoverCutoff ?? original.PeerTurnoverCutoff; + original.PeerTurnoverInterval = changed.PeerTurnoverInterval ?? original.PeerTurnoverInterval; + original.PerformanceWarning = changed.PerformanceWarning ?? original.PerformanceWarning; + original.Pex = changed.Pex ?? original.Pex; + original.PreallocateAll = changed.PreallocateAll ?? original.PreallocateAll; + original.ProxyAuthEnabled = changed.ProxyAuthEnabled ?? original.ProxyAuthEnabled; + original.ProxyBittorrent = changed.ProxyBittorrent ?? original.ProxyBittorrent; + original.ProxyHostnameLookup = changed.ProxyHostnameLookup ?? original.ProxyHostnameLookup; + original.ProxyIp = changed.ProxyIp ?? original.ProxyIp; + original.ProxyMisc = changed.ProxyMisc ?? original.ProxyMisc; + original.ProxyPassword = changed.ProxyPassword ?? original.ProxyPassword; + original.ProxyPeerConnections = changed.ProxyPeerConnections ?? original.ProxyPeerConnections; + original.ProxyPort = changed.ProxyPort ?? original.ProxyPort; + original.ProxyRss = changed.ProxyRss ?? original.ProxyRss; + original.ProxyType = changed.ProxyType ?? original.ProxyType; + original.ProxyUsername = changed.ProxyUsername ?? original.ProxyUsername; + original.QueueingEnabled = changed.QueueingEnabled ?? original.QueueingEnabled; + original.RandomPort = changed.RandomPort ?? original.RandomPort; + original.ReannounceWhenAddressChanged = changed.ReannounceWhenAddressChanged ?? original.ReannounceWhenAddressChanged; + original.RecheckCompletedTorrents = changed.RecheckCompletedTorrents ?? original.RecheckCompletedTorrents; + original.RefreshInterval = changed.RefreshInterval ?? original.RefreshInterval; + original.RequestQueueSize = changed.RequestQueueSize ?? original.RequestQueueSize; + original.ResolvePeerCountries = changed.ResolvePeerCountries ?? original.ResolvePeerCountries; + original.ResumeDataStorageType = changed.ResumeDataStorageType ?? original.ResumeDataStorageType; + original.RssAutoDownloadingEnabled = changed.RssAutoDownloadingEnabled ?? original.RssAutoDownloadingEnabled; + original.RssDownloadRepackProperEpisodes = changed.RssDownloadRepackProperEpisodes ?? original.RssDownloadRepackProperEpisodes; + original.RssMaxArticlesPerFeed = changed.RssMaxArticlesPerFeed ?? original.RssMaxArticlesPerFeed; + original.RssProcessingEnabled = changed.RssProcessingEnabled ?? original.RssProcessingEnabled; + original.RssRefreshInterval = changed.RssRefreshInterval ?? original.RssRefreshInterval; + original.RssSmartEpisodeFilters = changed.RssSmartEpisodeFilters ?? original.RssSmartEpisodeFilters; + original.SavePath = changed.SavePath ?? original.SavePath; + original.SavePathChangedTmmEnabled = changed.SavePathChangedTmmEnabled ?? original.SavePathChangedTmmEnabled; + original.SaveResumeDataInterval = changed.SaveResumeDataInterval ?? original.SaveResumeDataInterval; + original.ScanDirs = changed.ScanDirs ?? original.ScanDirs; + original.ScheduleFromHour = changed.ScheduleFromHour ?? original.ScheduleFromHour; + original.ScheduleFromMin = changed.ScheduleFromMin ?? original.ScheduleFromMin; + original.ScheduleToHour = changed.ScheduleToHour ?? original.ScheduleToHour; + original.ScheduleToMin = changed.ScheduleToMin ?? original.ScheduleToMin; + original.SchedulerDays = changed.SchedulerDays ?? original.SchedulerDays; + original.SchedulerEnabled = changed.SchedulerEnabled ?? original.SchedulerEnabled; + original.SendBufferLowWatermark = changed.SendBufferLowWatermark ?? original.SendBufferLowWatermark; + original.SendBufferWatermark = changed.SendBufferWatermark ?? original.SendBufferWatermark; + original.SendBufferWatermarkFactor = changed.SendBufferWatermarkFactor ?? original.SendBufferWatermarkFactor; + original.SlowTorrentDlRateThreshold = changed.SlowTorrentDlRateThreshold ?? original.SlowTorrentDlRateThreshold; + original.SlowTorrentInactiveTimer = changed.SlowTorrentInactiveTimer ?? original.SlowTorrentInactiveTimer; + original.SlowTorrentUlRateThreshold = changed.SlowTorrentUlRateThreshold ?? original.SlowTorrentUlRateThreshold; + original.SocketBacklogSize = changed.SocketBacklogSize ?? original.SocketBacklogSize; + 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.StopTrackerTimeout = changed.StopTrackerTimeout ?? original.StopTrackerTimeout; + original.TempPath = changed.TempPath ?? original.TempPath; + original.TempPathEnabled = changed.TempPathEnabled ?? original.TempPathEnabled; + original.TorrentChangedTmmEnabled = changed.TorrentChangedTmmEnabled ?? original.TorrentChangedTmmEnabled; + original.TorrentContentLayout = changed.TorrentContentLayout ?? original.TorrentContentLayout; + original.TorrentFileSizeLimit = changed.TorrentFileSizeLimit ?? original.TorrentFileSizeLimit; + original.TorrentStopCondition = changed.TorrentStopCondition ?? original.TorrentStopCondition; + original.UpLimit = changed.UpLimit ?? original.UpLimit; + original.UploadChokingAlgorithm = changed.UploadChokingAlgorithm ?? original.UploadChokingAlgorithm; + original.UploadSlotsBehavior = changed.UploadSlotsBehavior ?? original.UploadSlotsBehavior; + original.Upnp = changed.Upnp ?? original.Upnp; + original.UpnpLeaseDuration = changed.UpnpLeaseDuration ?? original.UpnpLeaseDuration; + original.UseCategoryPathsInManualMode = changed.UseCategoryPathsInManualMode ?? original.UseCategoryPathsInManualMode; + original.UseHttps = changed.UseHttps ?? original.UseHttps; + original.UseSubcategories = changed.UseSubcategories ?? original.UseSubcategories; + original.UtpTcpMixedMode = changed.UtpTcpMixedMode ?? original.UtpTcpMixedMode; + original.ValidateHttpsTrackerCertificate = changed.ValidateHttpsTrackerCertificate ?? original.ValidateHttpsTrackerCertificate; + original.WebUiAddress = changed.WebUiAddress ?? original.WebUiAddress; + original.WebUiBanDuration = changed.WebUiBanDuration ?? original.WebUiBanDuration; + original.WebUiClickjackingProtectionEnabled = changed.WebUiClickjackingProtectionEnabled ?? original.WebUiClickjackingProtectionEnabled; + original.WebUiCsrfProtectionEnabled = changed.WebUiCsrfProtectionEnabled ?? original.WebUiCsrfProtectionEnabled; + original.WebUiCustomHttpHeaders = changed.WebUiCustomHttpHeaders ?? original.WebUiCustomHttpHeaders; + original.WebUiDomainList = changed.WebUiDomainList ?? original.WebUiDomainList; + original.WebUiHostHeaderValidationEnabled = changed.WebUiHostHeaderValidationEnabled ?? original.WebUiHostHeaderValidationEnabled; + original.WebUiHttpsCertPath = changed.WebUiHttpsCertPath ?? original.WebUiHttpsCertPath; + original.WebUiHttpsKeyPath = changed.WebUiHttpsKeyPath ?? original.WebUiHttpsKeyPath; + original.WebUiMaxAuthFailCount = changed.WebUiMaxAuthFailCount ?? original.WebUiMaxAuthFailCount; + original.WebUiPort = changed.WebUiPort ?? original.WebUiPort; + original.WebUiReverseProxiesList = changed.WebUiReverseProxiesList ?? original.WebUiReverseProxiesList; + original.WebUiReverseProxyEnabled = changed.WebUiReverseProxyEnabled ?? original.WebUiReverseProxyEnabled; + original.WebUiSecureCookieEnabled = changed.WebUiSecureCookieEnabled ?? original.WebUiSecureCookieEnabled; + original.WebUiSessionTimeout = changed.WebUiSessionTimeout ?? original.WebUiSessionTimeout; + original.WebUiUpnp = changed.WebUiUpnp ?? original.WebUiUpnp; + original.WebUiUseCustomHttpHeadersEnabled = changed.WebUiUseCustomHttpHeadersEnabled ?? original.WebUiUseCustomHttpHeadersEnabled; + original.WebUiUsername = changed.WebUiUsername ?? original.WebUiUsername; + } + + return original; + } + + public void MergeContentsList(IReadOnlyList files, Dictionary contents) + { + var contentsList = CreateContentsList(files); + + foreach (var (key, value) in contentsList) + { + if (contents.TryGetValue(key, out var content)) + { + content.Availability = value.Availability; + content.Priority = value.Priority; + content.Progress = value.Progress; + content.Size = value.Size; + } + else + { + contents[key] = value; + } + } + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Services/IClipboardService.cs b/Lantean.QBTMudBlade/Services/IClipboardService.cs new file mode 100644 index 0000000..96da59b --- /dev/null +++ b/Lantean.QBTMudBlade/Services/IClipboardService.cs @@ -0,0 +1,7 @@ +namespace Lantean.QBTMudBlade.Services +{ + public interface IClipboardService + { + Task WriteToClipboard(string text); + } +} diff --git a/Lantean.QBTMudBlade/Services/IDataManager.cs b/Lantean.QBTMudBlade/Services/IDataManager.cs new file mode 100644 index 0000000..7bd8f15 --- /dev/null +++ b/Lantean.QBTMudBlade/Services/IDataManager.cs @@ -0,0 +1,23 @@ +using Lantean.QBTMudBlade.Models; + +namespace Lantean.QBTMudBlade.Services +{ + public interface IDataManager + { + MainData CreateMainData(QBitTorrentClient.Models.MainData mainData); + + Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent); + + void MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList); + + PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers); + + void MergeTorrentPeers(QBitTorrentClient.Models.TorrentPeers torrentPeers, PeerList peerList); + + Dictionary CreateContentsList(IReadOnlyList files); + + void MergeContentsList(IReadOnlyList files, Dictionary contents); + + QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed); + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/TableHelper.cs b/Lantean.QBTMudBlade/TableHelper.cs new file mode 100644 index 0000000..99a3932 --- /dev/null +++ b/Lantean.QBTMudBlade/TableHelper.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade +{ + 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) + { + Header = header; + SortSelector = sortSelector; + Formatter = formatter; + Class = tdClass; + Width = width; + + RowTemplate = (context) => (builder) => builder.AddContent(1, context.GetValue()); + } + + public ColumnDefinition(string header, Func sortSelector, RenderFragment> rowTemplate, Func? formatter = null, string? tdClass = null, int? width = null) + { + Header = header; + SortSelector = sortSelector; + RowTemplate = rowTemplate; + Formatter = formatter; + Class = tdClass; + Width = width; + } + + public string Id => Header.ToLowerInvariant().Replace(' ', '_'); + + public string Header { get; set; } + + public Func SortSelector { get; set; } + + public RenderFragment> RowTemplate { get; set; } + + public int? Width { get; set; } + + public Func? Formatter { get; set; } + + public string? Class { get; set; } + + public bool Enabled { get; set; } = true; + + public SortDirection InitialDirection { get; set; } = SortDirection.None; + + public RowContext GetRowContext(T data) + { + 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/_Imports.razor b/Lantean.QBTMudBlade/_Imports.razor new file mode 100644 index 0000000..6d890a1 --- /dev/null +++ b/Lantean.QBTMudBlade/_Imports.razor @@ -0,0 +1,15 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using MudBlazor +@using Lantean.QBTMudBlade +@using Lantean.QBTMudBlade.Layout +@using Lantean.QBTMudBlade.Models +@using Lantean.QBTMudBlade.Components +@using Lantean.QBTMudBlade.Components.Dialogs +@using Lantean.QBTMudBlade.Components.Options \ No newline at end of file diff --git a/Lantean.QBTMudBlade/wwwroot/css/app.css b/Lantean.QBTMudBlade/wwwroot/css/app.css new file mode 100644 index 0000000..762bdfe --- /dev/null +++ b/Lantean.QBTMudBlade/wwwroot/css/app.css @@ -0,0 +1,112 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +code { + color: #c02d76; +} + +.mud-appbar.mud-appbar-fixed-bottom { + height: 35px; +} + +.mud-main-content { + padding-bottom: 35px; +} + +.mud-drawer-fixed.mud-drawer-mini.mud-drawer-clipped-always, .mud-drawer-fixed.mud-drawer-persistent:not(.mud-drawer-clipped-never), .mud-drawer-fixed.mud-drawer-responsive.mud-drawer-clipped-always, .mud-drawer-fixed.mud-drawer-temporary.mud-drawer-clipped-always { + height: calc(100% - var(--mud-appbar-height) - 35px); +} + +.w-100 { + width: 100%; +} + +.table-progress { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.table-progress .progress-expand { + height: 28px !important; +} + +.torrent-list { + height: calc(100vh - 100px); +} + +td.no-wrap { + white-space: nowrap; /* Prevent text from wrapping to the next line */ + overflow: hidden; /* Hide any overflowing content */ + text-overflow: ellipsis; /* Display an ellipsis when the text overflows */ +} + +.rotate-180 { + transform: rotate(180deg); +} + +.rotate-90 { + transform: rotate(90deg); +} + +.background-blur { + backdrop-filter: blur(10px); +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/wwwroot/favicon.png b/Lantean.QBTMudBlade/wwwroot/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8422b59695935d180d11d5dbe99653e711097819 GIT binary patch literal 1148 zcmV-?1cUpDP)9h26h2-Cs%i*@Moc3?#6qJID|D#|3|2Hn7gTIYEkr|%Xjp);YgvFmB&0#2E2b=| zkVr)lMv9=KqwN&%obTp-$<51T%rx*NCwceh-E+=&e(oLO`@Z~7gybJ#U|^tB2Pai} zRN@5%1qsZ1e@R(XC8n~)nU1S0QdzEYlWPdUpH{wJ2Pd4V8kI3BM=)sG^IkUXF2-j{ zrPTYA6sxpQ`Q1c6mtar~gG~#;lt=s^6_OccmRd>o{*=>)KS=lM zZ!)iG|8G0-9s3VLm`bsa6e ze*TlRxAjXtm^F8V`M1%s5d@tYS>&+_ga#xKGb|!oUBx3uc@mj1%=MaH4GR0tPBG_& z9OZE;->dO@`Q)nr<%dHAsEZRKl zedN6+3+uGHejJp;Q==pskSAcRcyh@6mjm2z-uG;s%dM-u0*u##7OxI7wwyCGpS?4U zBFAr(%GBv5j$jS@@t@iI8?ZqE36I^4t+P^J9D^ELbS5KMtZ z{Qn#JnSd$15nJ$ggkF%I4yUQC+BjDF^}AtB7w348EL>7#sAsLWs}ndp8^DsAcOIL9 zTOO!!0!k2`9BLk25)NeZp7ev>I1Mn={cWI3Yhx2Q#DnAo4IphoV~R^c0x&nw*MoIV zPthX?{6{u}sMS(MxD*dmd5rU(YazQE59b|TsB5Tm)I4a!VaN@HYOR)DwH1U5y(E)z zQqQU*B%MwtRQ$%x&;1p%ANmc|PkoFJZ%<-uq%PX&C!c-7ypis=eP+FCeuv+B@h#{4 zGx1m0PjS~FJt}3mdt4c!lel`1;4W|03kcZRG+DzkTy|7-F~eDsV2Tx!73dM0H0CTh zl)F-YUkE1zEzEW(;JXc|KR5{ox%YTh{$%F$a36JP6Nb<0%#NbSh$dMYF-{ z1_x(Vx)}fs?5_|!5xBTWiiIQHG<%)*e=45Fhjw_tlnmlixq;mUdC$R8v#j( zhQ$9YR-o%i5Uc`S?6EC51!bTRK=Xkyb<18FkCKnS2;o*qlij1YA@-nRpq#OMTX&RbL<^2q@0qja!uIvI;j$6>~k@IMwD42=8$$!+R^@5o6HX(*n~v0A9xRwxP|bki~~&uFk>U z#P+PQh zyZ;-jwXKqnKbb6)@RaxQz@vm={%t~VbaZrdbaZrdbaeEeXj>~BG?&`J0XrqR#sSlO zg~N5iUk*15JibvlR1f^^1czzNKWvoJtc!Sj*G37QXbZ8LeD{Fzxgdv#Q{x}ytfZ5q z+^k#NaEp>zX_8~aSaZ`O%B9C&YLHb(mNtgGD&Kezd5S@&C=n~Uy1NWHM`t07VQP^MopUXki{2^#ryd94>UJMYW|(#4qV`kb7eD)Q=~NN zaVIRi@|TJ!Rni8J=5DOutQ#bEyMVr8*;HU|)MEKmVC+IOiDi9y)vz=rdtAUHW$yjt zrj3B7v(>exU=IrzC<+?AE=2vI;%fafM}#ShGDZx=0Nus5QHKdyb9pw&4>4XCpa-o?P(Gnco1CGX|U> z$f+_tA3+V~<{MU^A%eP!8R*-sD9y<>Jc7A(;aC5hVbs;kX9&Sa$JMG!W_BLFQa*hM zri__C@0i0U1X#?)Y=)>JpvTnY6^s;fu#I}K9u>OldV}m!Ch`d1Vs@v9 zb}w(!TvOmSzmMBa9gYvD4xocL2r0ds6%Hs>Z& z#7#o9PGHDmfG%JQq`O5~dt|MAQN@2wyJw_@``7Giyy(yyk(m8U*kk5$X1^;3$a3}N^Lp6hE5!#8l z#~NYHmKAs6IAe&A;bvM8OochRmXN>`D`{N$%#dZCRxp4-dJ?*3P}}T`tYa3?zz5BA zTu7uE#GsDpZ$~j9q=Zq!LYjLbZPXFILZK4?S)C-zE1(dC2d<7nO4-nSCbV#9E|E1MM|V<9>i4h?WX*r*ul1 z5#k6;po8z=fdMiVVz*h+iaTlz#WOYmU^SX5#97H~B32s-#4wk<1NTN#g?LrYieCu> zF7pbOLR;q2D#Q`^t%QcY06*X-jM+ei7%ZuanUTH#9Y%FBi*Z#22({_}3^=BboIsbg zR0#jJ>9QR8SnmtSS6x($?$}6$x+q)697#m${Z@G6Ujf=6iO^S}7P`q8DkH!IHd4lB zDzwxt3BHsPAcXFFY^Fj}(073>NL_$A%v2sUW(CRutd%{G`5ow?L`XYSO*Qu?x+Gzv zBtR}Y6`XF4xX7)Z04D+fH;TMapdQFFameUuHL34NN)r@aF4RO%x&NApeWGtr#mG~M z6sEIZS;Uj1HB1*0hh=O@0q1=Ia@L>-tETu-3n(op+97E z#&~2xggrl(LA|giII;RwBlX2^Q`B{_t}gxNL;iB11gEPC>v` zb4SJ;;BFOB!{chn>?cCeGDKuqI0+!skyWTn*k!WiPNBf=8rn;@y%( znhq%8fj2eAe?`A5mP;TE&iLEmQ^xV%-kmC-8mWao&EUK_^=GW-Y3z ksi~={si~={skwfB0gq6itke#r1ONa407*qoM6N<$g11Kq@c;k- literal 0 HcmV?d00001 diff --git a/Lantean.QBTMudBlade/wwwroot/index.html b/Lantean.QBTMudBlade/wwwroot/index.html new file mode 100644 index 0000000..0ecafe6 --- /dev/null +++ b/Lantean.QBTMudBlade/wwwroot/index.html @@ -0,0 +1,35 @@ + + + + + + + qBittorrent Web UI + + + + + + + + + + +
+ + + + +
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + diff --git a/Lantean.QBitTorrentClient/ApiClient.cs b/Lantean.QBitTorrentClient/ApiClient.cs new file mode 100644 index 0000000..3ec5b34 --- /dev/null +++ b/Lantean.QBitTorrentClient/ApiClient.cs @@ -0,0 +1,951 @@ +using Lantean.QBitTorrentClient.Models; +using System; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Lantean.QBitTorrentClient +{ + public class ApiClient : IApiClient + { + private readonly HttpClient _httpClient; + + private readonly JsonSerializerOptions _options = SerializerOptions.Options; + + public ApiClient(HttpClient httpClient) + { + _httpClient = httpClient; + } + + #region Authentication + + public async Task CheckAuthState() + { + try + { + var response = await _httpClient.GetAsync("app/version"); + return response.StatusCode == HttpStatusCode.OK; + } + catch + { + return false; + } + } + + public async Task Login(string username, string password) + { + var content = new FormUrlEncodedBuilder() + .Add("username", username) + .Add("password", password) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("auth/login", content); + + response.EnsureSuccessStatusCode(); + + var responseContent = await response.Content.ReadAsStringAsync(); + if (responseContent == "Fails.") + { + throw new HttpRequestException(null, null, HttpStatusCode.BadRequest); + } + } + + public async Task Logout() + { + var response = await _httpClient.PostAsync("auth/logout", null); + + response.EnsureSuccessStatusCode(); + } + + #endregion Authentication + + #region Application + + public async Task GetApplicationVersion() + { + var response = await _httpClient.GetAsync("app/version"); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStringAsync(); + } + + public async Task GetAPIVersion() + { + var response = await _httpClient.GetAsync("app/webapiVersion"); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStringAsync(); + } + + public async Task GetBuildInfo() + { + var response = await _httpClient.GetAsync("app/buildInfo"); + + response.EnsureSuccessStatusCode(); + + return await GetJson(response.Content); + } + + public async Task Shutdown() + { + var response = await _httpClient.PostAsync("app/shutdown", null); + + response.EnsureSuccessStatusCode(); + } + + public async Task GetApplicationPreferences() + { + var response = await _httpClient.GetAsync("app/preferences"); + + response.EnsureSuccessStatusCode(); + + return await GetJson(response.Content); + } + + public async Task SetApplicationPreferences(UpdatePreferences preferences) + { + var json = JsonSerializer.Serialize(preferences, _options); + + var content = new FormUrlEncodedBuilder() + .Add("json", json) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("app/setPreferences", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task GetDefaultSavePath() + { + var response = await _httpClient.GetAsync("app/defaultSavePath"); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStringAsync(); + } + + #endregion Application + + #region Log + + public async Task> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null) + { + var query = new QueryBuilder(); + if (normal is not null) + { + query.Add("normal", normal.Value); + } + if (info is not null) + { + query.Add("info", info.Value); + } + if (warning is not null) + { + query.Add("warning", warning.Value); + } + if (critical is not null) + { + query.Add("critical", critical.Value); + } + if (lastKnownId is not null) + { + query.Add("last_known_id", lastKnownId.Value); + } + + var response = await _httpClient.GetAsync($"log/main", query); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(response.Content); + } + + public async Task> GetPeerLog(int? lastKnownId = null) + { + var query = new QueryBuilder(); + if (lastKnownId is not null) + { + query.Add("last_known_id", lastKnownId.Value); + } + + var response = await _httpClient.GetAsync($"log/peers", query); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(response.Content); + } + + #endregion Log + + #region Sync + + public async Task GetMainData(int requestId) + { + var response = await _httpClient.GetAsync($"sync/maindata?rid={requestId}"); + + response.EnsureSuccessStatusCode(); + + return await GetJson(response.Content); + } + + public async Task GetTorrentPeersData(string hash, int requestId) + { + var response = await _httpClient.GetAsync($"sync/torrentPeers?hash={hash}&rid={requestId}"); + + response.EnsureSuccessStatusCode(); + + return await GetJson(response.Content); + } + + #endregion Sync + + #region Transfer info + + public async Task GetGlobalTransferInfo() + { + var response = await _httpClient.GetAsync("transfer/info"); + + response.EnsureSuccessStatusCode(); + + return await GetJson(response.Content); + } + + public async Task GetAlternativeSpeedLimitsState() + { + var response = await _httpClient.GetAsync("transfer/speedLimitsMode"); + + response.EnsureSuccessStatusCode(); + + var value = await response.Content.ReadAsStringAsync(); + + return value == "1"; + } + + public async Task ToggleAlternativeSpeedLimits() + { + var response = await _httpClient.PostAsync("transfer/toggleSpeedLimitsMode", null); + + response.EnsureSuccessStatusCode(); + } + + public async Task GetGlobalDownloadLimit() + { + var response = await _httpClient.GetAsync("transfer/downloadLimit"); + + response.EnsureSuccessStatusCode(); + + var value = await response.Content.ReadAsStringAsync(); + + return long.Parse(value); + } + + public async Task SetGlobalDownloadLimit(long limit) + { + var content = new FormUrlEncodedBuilder() + .Add("limit", limit) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("transfer/setDownloadLimit", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task GetGlobalUploadLimit() + { + var response = await _httpClient.GetAsync("transfer/uploadLimit"); + + response.EnsureSuccessStatusCode(); + + var value = await response.Content.ReadAsStringAsync(); + + return long.Parse(value); + } + + public async Task SetGlobalUploadLimit(long limit) + { + var content = new FormUrlEncodedBuilder() + .Add("limit", limit) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("transfer/setUploadLimit", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task BanPeers(IEnumerable peers) + { + var content = new FormUrlEncodedBuilder() + .AddPipeSeparated("peers", peers) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("transfer/banPeers", content); + + response.EnsureSuccessStatusCode(); + } + + #endregion Transfer info + + #region Torrent management + + public async Task> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, params string[] hashes) + { + var query = new QueryBuilder(); + if (filter is not null) + { + query.Add("filter", filter); + } + if (category is not null) + { + query.Add("category", category); + } + if (tag is not null) + { + query.Add("tag", tag); + } + if (sort is not null) + { + query.Add("sort", sort); + } + if (reverse is not null) + { + query.Add("reverse", reverse.Value); + } + if (limit is not null) + { + query.Add("limit", limit.Value); + } + if (offset is not null) + { + query.Add("offset", offset.Value); + } + if (hashes.Length > 0) + { + query.Add("hashes", string.Join('|', hashes)); + } + + var response = await _httpClient.GetAsync("torrents/info", query); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(response.Content); + } + + public async Task GetTorrentProperties(string hash) + { + var response = await _httpClient.GetAsync($"torrents/properties?hash={hash}"); + + response.EnsureSuccessStatusCode(); + + return await GetJson(response.Content); + } + + public async Task> GetTorrentTrackers(string hash) + { + var response = await _httpClient.GetAsync($"torrents/trackers?hash={hash}"); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(response.Content); + } + + public async Task> GetTorrentWebSeeds(string hash) + { + var response = await _httpClient.GetAsync($"torrents/webseeds?hash={hash}"); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(response.Content); + } + + public async Task> GetTorrentContents(string hash, params int[] indexes) + { + var query = new QueryBuilder(); + query.Add("hash", hash); + if (indexes.Length > 0) + { + query.Add("indexes", string.Join('|', indexes)); + } + var response = await _httpClient.GetAsync("torrents/files", query); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(response.Content); + } + + public async Task> GetTorrentPieceStates(string hash) + { + var response = await _httpClient.GetAsync($"torrents/pieceStates?hash={hash}"); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(response.Content); + } + + public async Task> GetTorrentPieceHashes(string hash) + { + var response = await _httpClient.GetAsync($"torrents/pieceHashes?hash={hash}"); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(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); + + response.EnsureSuccessStatusCode(); + } + + 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); + + response.EnsureSuccessStatusCode(); + } + + public async Task DeleteTorrents(bool? all = null, bool deleteFiles = false, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .Add("deleteFiles", deleteFiles) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/delete", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task RecheckTorrents(bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/recheck", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task ReannounceTorrents(bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/reannounce", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task AddTorrent(IEnumerable? urls = null, Dictionary? torrents = null, string? savePath = null, string? cookie = null, string? category = null, IEnumerable? tags = null, bool? skipChecking = null, bool? paused = null, string? contentLayout = null, string? renameTorrent = null, long? uploadLimit = null, long? downloadLimit = null, float? ratioLimit = null, int? seedingTimeLimit = null, bool? autoTorrentManagement = null, bool? sequentialDownload = null, bool? firstLastPiecePriority = null) + { + var content = new MultipartFormDataContent(); + if (urls is not null) + { + content.AddString("urls", string.Join('\n', urls)); + } + if (torrents is not null) + { + foreach (var (name, stream) in torrents) + { + content.Add(new StreamContent(stream), name); + } + } + if (savePath is not null) + { + content.AddString("savepath", savePath); + } + if (cookie is not null) + { + content.AddString("cookie", cookie); + } + if (category is not null) + { + content.AddString("category", category); + } + if (tags is not null) + { + content.AddString("tags", string.Join(',', tags)); + } + if (skipChecking is not null) + { + content.AddString("skip_checking", skipChecking.Value); + } + if (paused is not null) + { + content.AddString("paused", paused.Value); + } + if (contentLayout is not null) + { + content.AddString("contentLayout", contentLayout); + } + if (renameTorrent is not null) + { + content.AddString("rename", renameTorrent); + } + if (uploadLimit is not null) + { + content.AddString("upLimit", uploadLimit.Value); + } + if (downloadLimit is not null) + { + content.AddString("dlLimit", downloadLimit.Value); + } + if (ratioLimit is not null) + { + content.AddString("ratioLimit", ratioLimit.Value); + } + if (seedingTimeLimit is not null) + { + content.AddString("seedingTimeLimit", seedingTimeLimit.Value); + } + if (autoTorrentManagement is not null) + { + content.AddString("autoTMM", autoTorrentManagement.Value); + } + if (sequentialDownload is not null) + { + content.AddString("sequentialDownload", sequentialDownload.Value); + } + if (firstLastPiecePriority is not null) + { + content.AddString("firstLastPiecePrio", firstLastPiecePriority.Value); + } + + var response = await _httpClient.PostAsync("torrents/add", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task AddTrackersToTorrent(string hash, IEnumerable urls) + { + var content = new FormUrlEncodedBuilder() + .Add("hash", hash) + .Add("urls", string.Join('\n', urls)) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/addTrackers", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task EditTracker(string hash, string originalUrl, string newUrl) + { + var content = new FormUrlEncodedBuilder() + .Add("hash", hash) + .Add("originalUrl", originalUrl) + .Add("newUrl", newUrl) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/editTracker", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task RemoveTrackers(string hash, IEnumerable urls) + { + var content = new FormUrlEncodedBuilder() + .Add("hash", hash) + .AddPipeSeparated("urls", urls) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/removeTrackers", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task AddPeers(IEnumerable hashes, IEnumerable peers) + { + var content = new FormUrlEncodedBuilder() + .AddPipeSeparated("hash", hashes) + .AddPipeSeparated("urls", peers) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/addPeers", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task IncreaseTorrentPriority(bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hash", all, hashes) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/increasePrio", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task DecreaseTorrentPriority(bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hash", all, hashes) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/decreasePrio", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task MaximalTorrentPriority(bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hash", all, hashes) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/topPrio", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task MinimalTorrentPriority(bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hash", all, hashes) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/bottomPrio", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task SetFilePriority(string hash, IEnumerable id, Priority priority) + { + var content = new FormUrlEncodedBuilder() + .Add("hash", hash) + .AddPipeSeparated("id", id) + .Add("priority", priority) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/filePrio", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task> GetTorrentDownloadLimit(bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/downloadLimit", content); + + response.EnsureSuccessStatusCode(); + + return await GetJsonDictionary(response.Content); + } + + public async Task SetTorrentDownloadLimit(long limit, bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .Add("limit", limit) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/setDownloadLimit", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .Add("ratioLimit", ratioLimit) + .Add("seedingTimeLimit", seedingTimeLimit) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/setShareLimits", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task> GetTorrentUploadLimit(bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/uploadLimit", content); + + response.EnsureSuccessStatusCode(); + + return await GetJsonDictionary(response.Content); + } + + public async Task SetTorrentUploadLimit(long limit, bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .Add("limit", limit) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/setUploadLimit", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task SetTorrentLocation(string location, bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .Add("location", location) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/setLocation", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task SetTorrentName(string name, string hash) + { + var content = new FormUrlEncodedBuilder() + .Add("hash", hash) + .Add("name", name) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/rename", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task SetTorrentCategory(string category, bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .Add("category", category) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/setCategory", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task> GetAllCategories() + { + var response = await _httpClient.GetAsync("torrents/categories"); + + response.EnsureSuccessStatusCode(); + + return await GetJsonDictionary(response.Content); + } + + public async Task AddCategory(string category, string savePath) + { + var content = new FormUrlEncodedBuilder() + .Add("category", category) + .Add("savePath", savePath) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/createCategory", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task EditCategory(string category, string savePath) + { + var content = new FormUrlEncodedBuilder() + .Add("category", category) + .Add("savePath", savePath) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/editCategory", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task RemoveCategories(params string[] categories) + { + var content = new FormUrlEncodedBuilder() + .Add("categories", string.Join('\n', categories)) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/removeCategories", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task AddTorrentTags(IEnumerable tags, bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .AddPipeSeparated("tags", tags) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/addTags", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task RemoveTorrentTags(IEnumerable tags, bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .AddPipeSeparated("tags", tags) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/removeTags", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task> GetAllTags() + { + var response = await _httpClient.GetAsync("torrents/tags"); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(response.Content); + } + + public async Task CreateTags(IEnumerable tags) + { + var content = new FormUrlEncodedBuilder() + .AddPipeSeparated("tags", tags) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/createTags", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task DeleteTags(IEnumerable tags) + { + var content = new FormUrlEncodedBuilder() + .AddPipeSeparated("tags", tags) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/deleteTags", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task SetAutomaticTorrentManagement(bool enable, bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .Add("enable", enable) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/setAutoManagement", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task ToggleSequentialDownload(bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/toggleSequentialDownload", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task SetFirstLastPiecePriority(bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/toggleFirstLastPiecePrio", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task SetForceStart(bool value, bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .Add("enable", value) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/setFOrceStart", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task SetSuperSeeding(bool value, bool? all = null, params string[] hashes) + { + var content = new FormUrlEncodedBuilder() + .AddAllOrPipeSeparated("hashes", all, hashes) + .Add("enable", value) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/setSuperSeeding", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task RenameFile(string hash, string oldPath, string newPath) + { + var content = new FormUrlEncodedBuilder() + .Add("hash", hash) + .Add("oldPath", oldPath) + .Add("newPath", newPath) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/renameFile", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task RenameFolder(string hash, string oldPath, string newPath) + { + var content = new FormUrlEncodedBuilder() + .Add("hash", hash) + .Add("oldPath", oldPath) + .Add("newPath", newPath) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("torrents/renameFolder", content); + + response.EnsureSuccessStatusCode(); + } + + #endregion Torrent management + + #region RSS + + // not implementing RSS right now + + #endregion RSS + + #region Search + + // not implementing Search right now + + #endregion Search + + private async Task GetJson(HttpContent content) + { + return await content.ReadFromJsonAsync(_options) ?? throw new InvalidOperationException($"Unable to deserialize response as {typeof(T).Name}"); + } + + private async Task> GetJsonList(HttpContent content) + { + var items = await GetJson>(content); + + return items.ToList().AsReadOnly(); + } + + private async Task> GetJsonDictionary(HttpContent content) where TKey : notnull + { + var items = await GetJson>(content); + + return items.AsReadOnly(); + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/ApiClientExtensions.cs b/Lantean.QBitTorrentClient/ApiClientExtensions.cs new file mode 100644 index 0000000..f4c78a0 --- /dev/null +++ b/Lantean.QBitTorrentClient/ApiClientExtensions.cs @@ -0,0 +1,109 @@ +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 hashes) + { + return apiClient.PauseTorrents(null, hashes.ToArray()); + } + + public static Task PauseAllTorrents(this IApiClient apiClient) + { + return apiClient.PauseTorrents(true); + } + + public static Task ResumeTorrent(this IApiClient apiClient, string hash) + { + return apiClient.ResumeTorrents(null, hash); + } + + public static Task ResumeTorrents(this IApiClient apiClient, IEnumerable hashes) + { + return apiClient.ResumeTorrents(null, hashes.ToArray()); + } + + public static Task ResumeAllTorrents(this IApiClient apiClient) + { + return apiClient.ResumeTorrents(true); + } + + public static Task DeleteTorrent(this IApiClient apiClient, string hash, bool deleteFiles) + { + return apiClient.DeleteTorrents(null, deleteFiles, hash); + } + + public static Task DeleteTorrents(this IApiClient apiClient, IEnumerable hashes, bool deleteFiles) + { + return apiClient.DeleteTorrents(null, deleteFiles, hashes.ToArray()); + } + + public static Task DeleteAllTorrents(this IApiClient apiClient, bool deleteFiles) + { + return apiClient.DeleteTorrents(true, deleteFiles); + } + + public static async Task GetTorrent(this IApiClient apiClient, string hash) + { + var torrents = await apiClient.GetTorrentList(hashes: hash); + + if (torrents.Count == 0) + { + return null; + } + + return torrents[0]; + } + + public static Task SetTorrentCategory(this IApiClient apiClient, string category, string hash) + { + return apiClient.SetTorrentCategory(category, null, hash); + } + + public static Task RemoveTorrentTags(this IApiClient apiClient, IEnumerable tags, string hash) + { + return apiClient.RemoveTorrentTags(tags, null, hash); + } + + public static Task RemoveTorrentTag(this IApiClient apiClient, string tag, string hash) + { + return apiClient.RemoveTorrentTags([tag], hash); + } + + public static Task RemoveTorrentTag(this IApiClient apiClient, string tag, IEnumerable hashes) + { + return apiClient.RemoveTorrentTags([tag], null, hashes.ToArray()); + } + + public static Task AddTorrentTags(this IApiClient apiClient, IEnumerable tags, string hash) + { + return apiClient.AddTorrentTags(tags, null, hash); + } + + public static Task AddTorrentTag(this IApiClient apiClient, string tag, string hash) + { + return apiClient.AddTorrentTags([tag], hash); + } + + public static Task AddTorrentTag(this IApiClient apiClient, string tag, IEnumerable hashes) + { + return apiClient.AddTorrentTags([tag], null, hashes.ToArray()); + } + + public static Task RecheckTorrent(this IApiClient apiClient, string hash) + { + return apiClient.RecheckTorrents(null, hash); + } + + public static Task ReannounceTorrent(this IApiClient apiClient, string hash) + { + return apiClient.ReannounceTorrents(null, hash); + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Converters/CommaSeparatedJsonConverter.cs b/Lantean.QBitTorrentClient/Converters/CommaSeparatedJsonConverter.cs new file mode 100644 index 0000000..d7ee090 --- /dev/null +++ b/Lantean.QBitTorrentClient/Converters/CommaSeparatedJsonConverter.cs @@ -0,0 +1,37 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Converters +{ + internal class CommaSeparatedJsonConverter : JsonConverter> + { + public override IReadOnlyList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException("Must be of type string."); + } + + List list; + var value = reader.GetString(); + if (value is null) + { + list = []; + } + else + { + var values = value.Split(','); + list = [.. values]; + } + + return list.AsReadOnly(); + } + + public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options) + { + var output = string.Join(',', value); + + writer.WriteStringValue(output); + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Converters/SaveLocationJsonConverter.cs b/Lantean.QBitTorrentClient/Converters/SaveLocationJsonConverter.cs new file mode 100644 index 0000000..a49c9e8 --- /dev/null +++ b/Lantean.QBitTorrentClient/Converters/SaveLocationJsonConverter.cs @@ -0,0 +1,40 @@ +using Lantean.QBitTorrentClient.Models; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Converters +{ + public class SaveLocationJsonConverter : JsonConverter + { + public override SaveLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + return SaveLocation.Create(reader.GetString()); + } + + if (reader.TokenType == JsonTokenType.Number) + { + return SaveLocation.Create(reader.GetInt32()); + } + + throw new JsonException($"Unsupported token type {reader.TokenType}"); + } + + public override void Write(Utf8JsonWriter writer, SaveLocation value, JsonSerializerOptions options) + { + if (value.IsWatchedFolder) + { + writer.WriteNumberValue(0); + } + else if (value.IsDefaltFolder) + { + writer.WriteNumberValue(1); + } + else if (value.SavePath is not null) + { + writer.WriteStringValue(value.SavePath); + } + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Converters/StringFloatJsonConverter.cs b/Lantean.QBitTorrentClient/Converters/StringFloatJsonConverter.cs new file mode 100644 index 0000000..3435339 --- /dev/null +++ b/Lantean.QBitTorrentClient/Converters/StringFloatJsonConverter.cs @@ -0,0 +1,38 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Converters +{ + internal class StringFloatJsonConverter : JsonConverter + { + public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + if (float.TryParse(reader.GetString(), out var value)) + { + return value; + } + + return 0; + } + + if (reader.TokenType == JsonTokenType.Number) + { + if (reader.TryGetSingle(out var value)) + { + return value; + } + + return 0; + } + + return 0; + } + + public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/FormUrlEncodedBuilder.cs b/Lantean.QBitTorrentClient/FormUrlEncodedBuilder.cs new file mode 100644 index 0000000..5097bc9 --- /dev/null +++ b/Lantean.QBitTorrentClient/FormUrlEncodedBuilder.cs @@ -0,0 +1,38 @@ +namespace Lantean.QBitTorrentClient +{ + public class FormUrlEncodedBuilder + { + private readonly IList> _parameters; + + public FormUrlEncodedBuilder() + { + _parameters = []; + } + + public FormUrlEncodedBuilder(IList> parameters) + { + _parameters = parameters; + } + + public FormUrlEncodedBuilder Add(string key, string value) + { + _parameters.Add(new KeyValuePair(key, value)); + return this; + } + + public FormUrlEncodedBuilder AddIfNotNullOrEmpty(string key, string value) + { + if (!string.IsNullOrEmpty(value)) + { + _parameters.Add(new KeyValuePair(key, value)); + } + + return this; + } + + public FormUrlEncodedContent ToFormUrlEncodedContent() + { + return new FormUrlEncodedContent(_parameters); + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/FormUrlEncodedBuilderExtensions.cs b/Lantean.QBitTorrentClient/FormUrlEncodedBuilderExtensions.cs new file mode 100644 index 0000000..70a5a8e --- /dev/null +++ b/Lantean.QBitTorrentClient/FormUrlEncodedBuilderExtensions.cs @@ -0,0 +1,52 @@ +using System.Globalization; + +namespace Lantean.QBitTorrentClient +{ + public static class FormUrlEncodedBuilderExtensions + { + public static FormUrlEncodedBuilder Add(this FormUrlEncodedBuilder builder, string key, bool value) + { + return builder.Add(key, value ? "true" : "false"); + } + + public static FormUrlEncodedBuilder Add(this FormUrlEncodedBuilder builder, string key, int value) + { + return builder.Add(key, value.ToString()); + } + + public static FormUrlEncodedBuilder Add(this FormUrlEncodedBuilder builder, string key, long value) + { + return builder.Add(key, value.ToString()); + } + + public static FormUrlEncodedBuilder Add(this FormUrlEncodedBuilder builder, string key, DateTimeOffset value, bool useSeconds = true) + { + return builder.Add(key, useSeconds ? value.ToUnixTimeSeconds() : value.ToUnixTimeMilliseconds()); + } + + public static FormUrlEncodedBuilder Add(this FormUrlEncodedBuilder builder, string key, float value) + { + return builder.Add(key, value.ToString()); + } + + public static FormUrlEncodedBuilder Add(this FormUrlEncodedBuilder builder, string key, T value) where T : struct, IConvertible + { + return builder.Add(key, value.ToInt32(CultureInfo.InvariantCulture).ToString()); + } + + public static FormUrlEncodedBuilder AddAllOrPipeSeparated(this FormUrlEncodedBuilder builder, string key, bool? all = null, params string[] values) + { + return builder.Add(key, all.GetValueOrDefault() ? "all" : string.Join('|', values)); + } + + public static FormUrlEncodedBuilder AddPipeSeparated(this FormUrlEncodedBuilder builder, string key, IEnumerable values) + { + return builder.Add(key, string.Join('|', values)); + } + + public static FormUrlEncodedBuilder AddCommaSeparated(this FormUrlEncodedBuilder builder, string key, IEnumerable values) + { + return builder.Add(key, string.Join(',', values)); + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/HttpClientExtensions.cs b/Lantean.QBitTorrentClient/HttpClientExtensions.cs new file mode 100644 index 0000000..5045b19 --- /dev/null +++ b/Lantean.QBitTorrentClient/HttpClientExtensions.cs @@ -0,0 +1,15 @@ +namespace Lantean.QBitTorrentClient +{ + internal static class HttpClientExtensions + { + public static Task PostAsync(this HttpClient httpClient, string requestUrl, FormUrlEncodedBuilder builder) + { + return httpClient.PostAsync(requestUrl, builder.ToFormUrlEncodedContent()); + } + + public static Task GetAsync(this HttpClient httpClient, string requestUrl, QueryBuilder builder) + { + return httpClient.GetAsync($"{requestUrl}{builder.ToQueryString()}"); + } + } +} diff --git a/Lantean.QBitTorrentClient/IApiClient.cs b/Lantean.QBitTorrentClient/IApiClient.cs new file mode 100644 index 0000000..2ea6d35 --- /dev/null +++ b/Lantean.QBitTorrentClient/IApiClient.cs @@ -0,0 +1,179 @@ +using Lantean.QBitTorrentClient.Models; + +namespace Lantean.QBitTorrentClient +{ + public interface IApiClient + { + #region Authentication + + Task CheckAuthState(); + + Task Login(string username, string password); + + Task Logout(); + + #endregion Authentication + + #region Application + + Task GetApplicationVersion(); + + Task GetAPIVersion(); + + Task GetBuildInfo(); + + Task Shutdown(); + + Task GetApplicationPreferences(); + + Task SetApplicationPreferences(UpdatePreferences preferences); + + Task GetDefaultSavePath(); + + #endregion Application + + #region Log + + Task> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null); + + Task> GetPeerLog(int? lastKnownId = null); + + #endregion Log + + #region Sync + + Task GetMainData(int requestId); + + Task GetTorrentPeersData(string hash, int requestId); + + #endregion Sync + + #region Transfer info + + Task GetGlobalTransferInfo(); + + Task GetAlternativeSpeedLimitsState(); + + Task ToggleAlternativeSpeedLimits(); + + Task GetGlobalDownloadLimit(); + + Task SetGlobalDownloadLimit(long limit); + + Task GetGlobalUploadLimit(); + + Task SetGlobalUploadLimit(long limit); + + Task BanPeers(IEnumerable peers); + + #endregion Transfer info + + #region Torrent management + + Task> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, params string[] hashes); + + Task GetTorrentProperties(string hash); + + Task> GetTorrentTrackers(string hash); + + Task> GetTorrentWebSeeds(string hash); + + Task> GetTorrentContents(string hash, params int[] indexes); + + Task> GetTorrentPieceStates(string hash); + + Task> GetTorrentPieceHashes(string hash); + + Task PauseTorrents(bool? all = null, params string[] hashes); + + Task ResumeTorrents(bool? all = null, params string[] hashes); + + Task DeleteTorrents(bool? all = null, bool deleteFiles = false, params string[] hashes); + + Task RecheckTorrents(bool? all = null, params string[] hashes); + + Task ReannounceTorrents(bool? all = null, params string[] hashes); + + Task AddTorrent(IEnumerable? urls = null, Dictionary? torrents = null, string? savePath = null, string? cookie = null, string? category = null, IEnumerable? tags = null, bool? skipChecking = null, bool? paused = null, string? contentLayout = null, string? renameTorrent = null, long? uploadLimit = null, long? downloadLimit = null, float? ratioLimit = null, int? seedingTimeLimit = null, bool? autoTorrentManagement = null, bool? sequentialDownload = null, bool? firstLastPiecePriority = null); + + Task AddTrackersToTorrent(string hash, IEnumerable urls); + + Task EditTracker(string hash, string originalUrl, string newUrl); + + Task RemoveTrackers(string hash, IEnumerable urls); + + Task AddPeers(IEnumerable hashes, IEnumerable peers); + + Task IncreaseTorrentPriority(bool? all = null, params string[] hashes); + + Task DecreaseTorrentPriority(bool? all = null, params string[] hashes); + + Task MaximalTorrentPriority(bool? all = null, params string[] hashes); + + Task MinimalTorrentPriority(bool? all = null, params string[] hashes); + + Task SetFilePriority(string hash, IEnumerable id, Priority priority); + + Task> GetTorrentDownloadLimit(bool? all = null, params string[] hashes); + + Task SetTorrentDownloadLimit(long limit, bool? all = null, params string[] hashes); + + Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, bool? all = null, params string[] hashes); + + Task> GetTorrentUploadLimit(bool? all = null, params string[] hashes); + + Task SetTorrentUploadLimit(long limit, bool? all = null, params string[] hashes); + + Task SetTorrentLocation(string location, bool? all = null, params string[] hashes); + + Task SetTorrentName(string name, string hash); + + Task SetTorrentCategory(string category, bool? all = null, params string[] hashes); + + Task> GetAllCategories(); + + Task AddCategory(string category, string savePath); + + Task EditCategory(string category, string savePath); + + Task RemoveCategories(params string[] categories); + + Task AddTorrentTags(IEnumerable tags, bool? all = null, params string[] hashes); + + Task RemoveTorrentTags(IEnumerable tags, bool? all = null, params string[] hashes); + + Task> GetAllTags(); + + Task CreateTags(IEnumerable tags); + + Task DeleteTags(IEnumerable tags); + + Task SetAutomaticTorrentManagement(bool enable, bool? all = null, params string[] hashes); + + Task ToggleSequentialDownload(bool? all = null, params string[] hashes); + + Task SetFirstLastPiecePriority(bool? all = null, params string[] hashes); + + Task SetForceStart(bool value, bool? all = null, params string[] hashes); + + Task SetSuperSeeding(bool value, bool? all = null, params string[] hashes); + + Task RenameFile(string hash, string oldPath, string newPath); + + Task RenameFolder(string hash, string oldPath, string newPath); + + #endregion Torrent management + + #region RSS + + // not implementing RSS right now + + #endregion RSS + + #region Search + + // not implementing Search right now + + #endregion Search + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Lantean.QBitTorrentClient.csproj b/Lantean.QBitTorrentClient/Lantean.QBitTorrentClient.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/Lantean.QBitTorrentClient/Lantean.QBitTorrentClient.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Lantean.QBitTorrentClient/Limits.cs b/Lantean.QBitTorrentClient/Limits.cs new file mode 100644 index 0000000..391c94a --- /dev/null +++ b/Lantean.QBitTorrentClient/Limits.cs @@ -0,0 +1,9 @@ +namespace Lantean.QBitTorrentClient +{ + public static class Limits + { + public const long GlobalLimit = -2; + + public const long NoLimit = -1; + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/MockApiClient.cs b/Lantean.QBitTorrentClient/MockApiClient.cs new file mode 100644 index 0000000..5b7cc66 --- /dev/null +++ b/Lantean.QBitTorrentClient/MockApiClient.cs @@ -0,0 +1,367 @@ +using Lantean.QBitTorrentClient.Models; + +namespace Lantean.QBitTorrentClient +{ + public class MockApiClient : IApiClient + { + private readonly ApiClient _apiClient; + + public MockApiClient(ApiClient apiClient) + { + _apiClient = apiClient; + } + + public Task AddCategory(string category, string savePath) + { + return _apiClient.AddCategory(category, savePath); + } + + public Task AddPeers(IEnumerable hashes, IEnumerable peers) + { + return _apiClient.AddPeers(hashes, peers); + } + + public Task AddTorrent(IEnumerable? urls = null, Dictionary? torrents = null, string? savePath = null, string? cookie = null, string? category = null, IEnumerable? tags = null, bool? skipChecking = null, bool? paused = null, string? contentLayout = null, string? renameTorrent = null, long? uploadLimit = null, long? downloadLimit = null, float? ratioLimit = null, int? seedingTimeLimit = null, bool? autoTorrentManagement = null, bool? sequentialDownload = null, bool? firstLastPiecePriority = null) + { + return _apiClient.AddTorrent(urls, torrents, savePath, cookie, category, tags, skipChecking, paused, contentLayout, renameTorrent, uploadLimit, downloadLimit, ratioLimit, seedingTimeLimit, autoTorrentManagement, sequentialDownload, firstLastPiecePriority); + } + + public Task AddTorrentTags(IEnumerable tags, bool? all = null, params string[] hashes) + { + return _apiClient.AddTorrentTags(tags, all, hashes); + } + + public Task AddTrackersToTorrent(string hash, IEnumerable urls) + { + return _apiClient.AddTrackersToTorrent(hash, urls); + } + + public Task BanPeers(IEnumerable peers) + { + return _apiClient.BanPeers(peers); + } + + public Task CheckAuthState() + { + return _apiClient.CheckAuthState(); + } + + public Task CreateTags(IEnumerable tags) + { + return _apiClient.CreateTags(tags); + } + + public Task DecreaseTorrentPriority(bool? all = null, params string[] hashes) + { + return _apiClient.DecreaseTorrentPriority(all, hashes); + } + + public Task DeleteTags(IEnumerable tags) + { + return _apiClient.DeleteTags(tags); + } + + public Task DeleteTorrents(bool? all = null, bool deleteFiles = false, params string[] hashes) + { + return _apiClient.DeleteTorrents(all, deleteFiles, hashes); + } + + public Task EditCategory(string category, string savePath) + { + return _apiClient.EditCategory(category, savePath); + } + + public Task EditTracker(string hash, string originalUrl, string newUrl) + { + return _apiClient.EditTracker(hash, originalUrl, newUrl); + } + + public Task> GetAllCategories() + { + return _apiClient.GetAllCategories(); + } + + public Task> GetAllTags() + { + return _apiClient.GetAllTags(); + } + + public Task GetAlternativeSpeedLimitsState() + { + return _apiClient.GetAlternativeSpeedLimitsState(); + } + + public Task GetAPIVersion() + { + return _apiClient.GetAPIVersion(); + } + + public Task GetApplicationPreferences() + { + return _apiClient.GetApplicationPreferences(); + } + + public Task GetApplicationVersion() + { + return _apiClient.GetApplicationVersion(); + } + + public Task GetBuildInfo() + { + return _apiClient.GetBuildInfo(); + } + + public Task GetDefaultSavePath() + { + return _apiClient.GetDefaultSavePath(); + } + + public Task GetGlobalDownloadLimit() + { + return _apiClient.GetGlobalDownloadLimit(); + } + + public Task GetGlobalTransferInfo() + { + return _apiClient.GetGlobalTransferInfo(); + } + + public Task GetGlobalUploadLimit() + { + return _apiClient.GetGlobalUploadLimit(); + } + + public Task> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null) + { + return _apiClient.GetLog(normal, info, warning, critical, lastKnownId); + } + + public Task GetMainData(int requestId) + { + return _apiClient.GetMainData(requestId); + } + + public Task> GetPeerLog(int? lastKnownId = null) + { + return _apiClient.GetPeerLog(lastKnownId); + } + + public Task> GetTorrentContents(string hash, params int[] indexes) + { + var list = new List(); + list.Add(new FileData(2, "slackware-14.2-iso/slackware-14.2-source-d6.iso", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(3, "slackware-14.2-iso/slackware-14.2-source-d6.iso.asc", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(4, "slackware-14.2-iso/slackware-14.2-source-d6.iso.md5", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(5, "slackware-14.2-iso/slackware-14.2-source-d6.iso.txt", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(6, "slackware-14.2-iso/temp/slackware-14.2-source-d6.iso.md5", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(7, "slackware-14.2-iso/temp/slackware-14.2-source-d6.iso.txt", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(8, "slackware-14.2-iso2/slackware-14.2-source-d6.iso2", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(9, "slackware-14.2-iso2/slackware-14.2-source-d6.iso2.asc", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(10, "slackware-14.2-iso2/slackware-14.2-source-d6.iso2.md5", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(11, "slackware-14.2-iso2/slackware-14.2-source-d6.iso2.txt", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(12, "really/long/directory/path/is/here/file.txt", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + list.Add(new FileData(13, "other.txt", 500, 0f, Priority.Normal, false, [1, 2], 0f)); + return Task.FromResult>(list); + } + + public Task> GetTorrentDownloadLimit(bool? all = null, params string[] hashes) + { + return _apiClient.GetTorrentDownloadLimit(all, hashes); + } + + public Task> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, params string[] hashes) + { + return _apiClient.GetTorrentList(filter, category, tag, sort, reverse, limit, offset, hashes); + } + + public Task GetTorrentPeersData(string hash, int requestId) + { + return _apiClient.GetTorrentPeersData(hash, requestId); + } + + public Task> GetTorrentPieceHashes(string hash) + { + return _apiClient.GetTorrentPieceHashes(hash); + } + + public Task> GetTorrentPieceStates(string hash) + { + return _apiClient.GetTorrentPieceStates(hash); + } + + public Task GetTorrentProperties(string hash) + { + return _apiClient.GetTorrentProperties(hash); + } + + public Task> GetTorrentTrackers(string hash) + { + return _apiClient.GetTorrentTrackers(hash); + } + + public Task> GetTorrentUploadLimit(bool? all = null, params string[] hashes) + { + return _apiClient.GetTorrentUploadLimit(all, hashes); + } + + public Task> GetTorrentWebSeeds(string hash) + { + return _apiClient.GetTorrentWebSeeds(hash); + } + + public Task IncreaseTorrentPriority(bool? all = null, params string[] hashes) + { + return _apiClient.IncreaseTorrentPriority(all, hashes); + } + + public Task Login(string username, string password) + { + return _apiClient.Login(username, password); + } + + public Task Logout() + { + return _apiClient.Logout(); + } + + public Task MaximalTorrentPriority(bool? all = null, params string[] hashes) + { + return _apiClient.MaximalTorrentPriority(all, hashes); + } + + public Task MinimalTorrentPriority(bool? all = null, params string[] hashes) + { + return _apiClient.MinimalTorrentPriority(all, hashes); + } + + public Task PauseTorrents(bool? all = null, params string[] hashes) + { + return _apiClient.PauseTorrents(all, hashes); + } + + public Task ReannounceTorrents(bool? all = null, params string[] hashes) + { + return _apiClient.ReannounceTorrents(all, hashes); + } + + public Task RecheckTorrents(bool? all = null, params string[] hashes) + { + return _apiClient.ReannounceTorrents(all, hashes); + } + + public Task RemoveCategories(params string[] categories) + { + return _apiClient.RemoveCategories(categories); + } + + public Task RemoveTorrentTags(IEnumerable tags, bool? all = null, params string[] hashes) + { + return _apiClient.RemoveTorrentTags(tags, all, hashes); + } + + public Task RemoveTrackers(string hash, IEnumerable urls) + { + return _apiClient.RemoveTrackers(hash, urls); + } + + public Task RenameFile(string hash, string oldPath, string newPath) + { + return _apiClient.RenameFile(hash, oldPath, newPath); + } + + public Task RenameFolder(string hash, string oldPath, string newPath) + { + return _apiClient.RenameFolder(hash, oldPath, newPath); + } + + public Task ResumeTorrents(bool? all = null, params string[] hashes) + { + return _apiClient.ResumeTorrents(all, hashes); + } + + public Task SetApplicationPreferences(UpdatePreferences preferences) + { + return _apiClient.SetApplicationPreferences(preferences); + } + + public Task SetAutomaticTorrentManagement(bool enable, bool? all = null, params string[] hashes) + { + return _apiClient.SetAutomaticTorrentManagement(enable, all, hashes); + } + + public Task SetFilePriority(string hash, IEnumerable id, Priority priority) + { + return _apiClient.SetFilePriority(hash, id, priority); + } + + public Task SetFirstLastPiecePriority(bool? all = null, params string[] hashes) + { + return _apiClient.SetFirstLastPiecePriority(all, hashes); + } + + public Task SetForceStart(bool value, bool? all = null, params string[] hashes) + { + return _apiClient.SetForceStart(value, all, hashes); + } + + public Task SetGlobalDownloadLimit(long limit) + { + return _apiClient.SetGlobalDownloadLimit(limit); + } + + public Task SetGlobalUploadLimit(long limit) + { + return _apiClient.SetGlobalDownloadLimit(limit); + } + + public Task SetSuperSeeding(bool value, bool? all = null, params string[] hashes) + { + return _apiClient.SetSuperSeeding(value, all, hashes); + } + + public Task SetTorrentCategory(string category, bool? all = null, params string[] hashes) + { + return _apiClient.SetTorrentCategory(category, all, hashes); + } + + public Task SetTorrentDownloadLimit(long limit, bool? all = null, params string[] hashes) + { + return _apiClient.SetTorrentDownloadLimit(limit, all, hashes); + } + + public Task SetTorrentLocation(string location, bool? all = null, params string[] hashes) + { + return _apiClient.SetTorrentLocation(location, all, hashes); + } + + public Task SetTorrentName(string name, string hash) + { + return _apiClient.SetTorrentName(name, hash); + } + + public Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, bool? all = null, params string[] hashes) + { + return _apiClient.SetTorrentShareLimit(ratioLimit, seedingTimeLimit, all, hashes); + } + + public Task SetTorrentUploadLimit(long limit, bool? all = null, params string[] hashes) + { + return _apiClient.SetTorrentUploadLimit(limit, all, hashes); + } + + public Task Shutdown() + { + return _apiClient.Shutdown(); + } + + public Task ToggleAlternativeSpeedLimits() + { + return _apiClient.ToggleAlternativeSpeedLimits(); + } + + public Task ToggleSequentialDownload(bool? all = null, params string[] hashes) + { + return _apiClient.ToggleSequentialDownload(all, hashes); + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/BuildInfo.cs b/Lantean.QBitTorrentClient/Models/BuildInfo.cs new file mode 100644 index 0000000..e1daa13 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/BuildInfo.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record BuildInfo + { + [JsonConstructor] + public BuildInfo( + string qTVersion, + string libTorrentVersion, + string boostVersion, + string openSSLVersion, + int bitness) + { + QTVersion = qTVersion; + LibTorrentVersion = libTorrentVersion; + BoostVersion = boostVersion; + OpenSSLVersion = openSSLVersion; + Bitness = bitness; + } + + [JsonPropertyName("qt")] + public string QTVersion { get; } + + [JsonPropertyName("libtorrent")] + public string LibTorrentVersion { get; } + + [JsonPropertyName("boost")] + public string BoostVersion { get; } + + [JsonPropertyName("openssl")] + public string OpenSSLVersion { get; } + + [JsonPropertyName("bitness")] + public int Bitness { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/Category.cs b/Lantean.QBitTorrentClient/Models/Category.cs new file mode 100644 index 0000000..943cc1c --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/Category.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record Category + { + [JsonConstructor] + public Category( + string name, + string? savePath) + { + Name = name; + SavePath = savePath; + } + + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("savePath")] + public string? SavePath { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/FileData.cs b/Lantean.QBitTorrentClient/Models/FileData.cs new file mode 100644 index 0000000..0044d5b --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/FileData.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record FileData + { + [JsonConstructor] + public FileData( + int index, + string name, + long size, + float progress, + Priority priority, + bool isSeed, + IReadOnlyList pieceRange, + float availability) + { + Index = index; + Name = name; + Size = size; + Progress = progress; + Priority = priority; + IsSeed = isSeed; + PieceRange = pieceRange ?? []; + Availability = availability; + } + + [JsonPropertyName("index")] + public int Index { get; } + + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("size")] + public long Size { get; } + + [JsonPropertyName("progress")] + public float Progress { get; } + + [JsonPropertyName("priority")] + public Priority Priority { get; } + + [JsonPropertyName("is_seed")] + public bool IsSeed { get; } + + [JsonPropertyName("piece_range")] + public IReadOnlyList PieceRange { get; } + + [JsonPropertyName("availability")] + public float Availability { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/GlobalTransferInfo.cs b/Lantean.QBitTorrentClient/Models/GlobalTransferInfo.cs new file mode 100644 index 0000000..576a504 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/GlobalTransferInfo.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record GlobalTransferInfo + { + [JsonConstructor] + public GlobalTransferInfo( + string? connectionStatus, + int? dHTNodes, + long? downloadInfoData, + long? downloadInfoSpeed, + long? downloadRateLimit, + long? uploadInfoData, + long? uploadInfoSpeed, + long? uploadRateLimit) + { + ConnectionStatus = connectionStatus; + DHTNodes = dHTNodes; + DownloadInfoData = downloadInfoData; + DownloadInfoSpeed = downloadInfoSpeed; + DownloadRateLimit = downloadRateLimit; + UploadInfoData = uploadInfoData; + UploadInfoSpeed = uploadInfoSpeed; + UploadRateLimit = uploadRateLimit; + } + + [JsonPropertyName("connection_status")] + public string? ConnectionStatus { get; } + + [JsonPropertyName("dht_nodes")] + public int? DHTNodes { get; } + + [JsonPropertyName("dl_info_data")] + public long? DownloadInfoData { get; } + + [JsonPropertyName("dl_info_speed")] + public long? DownloadInfoSpeed { get; } + + [JsonPropertyName("dl_rate_limit")] + public long? DownloadRateLimit { get; } + + [JsonPropertyName("up_info_data")] + public long? UploadInfoData { get; } + + [JsonPropertyName("up_info_speed")] + public long? UploadInfoSpeed { get; } + + [JsonPropertyName("up_rate_limit")] + public long? UploadRateLimit { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/Log.cs b/Lantean.QBitTorrentClient/Models/Log.cs new file mode 100644 index 0000000..341452a --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/Log.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record Log + { + [JsonConstructor] + public Log( + int id, + string message, + long timestamp, + LogType type) + { + Id = id; + Message = message; + Timestamp = timestamp; + Type = type; + } + + [JsonPropertyName("id")] + public int Id { get; } + + [JsonPropertyName("message")] + public string Message { get; } + + [JsonPropertyName("timestamp")] + public long Timestamp { get; } + + [JsonPropertyName("type")] + public LogType Type { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/LogType.cs b/Lantean.QBitTorrentClient/Models/LogType.cs new file mode 100644 index 0000000..e38798e --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/LogType.cs @@ -0,0 +1,10 @@ +namespace Lantean.QBitTorrentClient.Models +{ + public enum LogType + { + Normal = 1, + Info = 2, + Warning = 4, + Critical = 8 + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/MainData.cs b/Lantean.QBitTorrentClient/Models/MainData.cs new file mode 100644 index 0000000..280b52a --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/MainData.cs @@ -0,0 +1,65 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record MainData + { + [JsonConstructor] + public MainData( + int responseId, + bool fullUpdate, + IReadOnlyDictionary? torrents, + IReadOnlyList? torrentsRemoved, + IReadOnlyDictionary? categories, + IReadOnlyList? categoriesRemoved, + IReadOnlyList? tags, + IReadOnlyList? tagsRemoved, + IReadOnlyDictionary> trackers, + ServerState? serverState) + { + ResponseId = responseId; + FullUpdate = fullUpdate; + Torrents = torrents; + TorrentsRemoved = torrentsRemoved; + Categories = categories; + CategoriesRemoved = categoriesRemoved; + Tags = tags; + TagsRemoved = tagsRemoved; + Trackers = trackers; + ServerState = serverState; + } + + [JsonPropertyName("rid")] + public int ResponseId { get; } + + [JsonPropertyName("full_update")] + public bool FullUpdate { get; } + + [JsonPropertyName("torrents")] + public IReadOnlyDictionary? Torrents { get; } + + [JsonPropertyName("torrents_removed")] + public IReadOnlyList? TorrentsRemoved { get; } + + [JsonPropertyName("categories")] + public IReadOnlyDictionary? Categories { get; } + + [JsonPropertyName("categories_removed")] + public IReadOnlyList? CategoriesRemoved { get; } + + [JsonPropertyName("tags")] + public IReadOnlyList? Tags { get; } + + [JsonPropertyName("tags_removed")] + public IReadOnlyList? TagsRemoved { get; } + + [JsonPropertyName("trackers")] + public IReadOnlyDictionary>? Trackers { get; } + + [JsonPropertyName("trackers_removed")] + public IReadOnlyList? TrackersRemoved { get; } + + [JsonPropertyName("server_state")] + public ServerState? ServerState { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/Peer.cs b/Lantean.QBitTorrentClient/Models/Peer.cs new file mode 100644 index 0000000..4c9c2ae --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/Peer.cs @@ -0,0 +1,92 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record Peer + { + [JsonConstructor] + public Peer( + string? client, + string? connection, + string? country, + string? countryCode, + long? downloadSpeed, + long? downloaded, + string? files, + string? flags, + string? flagsDescription, + string? iPAddress, + string? clientId, + int? port, + float? progress, + float? relevance, + long? uploadSpeed, + long? uploaded) + { + Client = client; + Connection = connection; + Country = country; + CountryCode = countryCode; + DownloadSpeed = downloadSpeed; + Downloaded = downloaded; + Files = files; + Flags = flags; + FlagsDescription = flagsDescription; + IPAddress = iPAddress; + ClientId = clientId; + Port = port; + Progress = progress; + Relevance = relevance; + UploadSpeed = uploadSpeed; + Uploaded = uploaded; + } + + [JsonPropertyName("client")] + public string? Client { get; } + + [JsonPropertyName("connection")] + public string? Connection { get; } + + [JsonPropertyName("country")] + public string? Country { get; } + + [JsonPropertyName("country_code")] + public string? CountryCode { get; } + + [JsonPropertyName("dl_speed")] + public long? DownloadSpeed { get; } + + [JsonPropertyName("downloaded")] + public long? Downloaded { get; } + + [JsonPropertyName("files")] + public string? Files { get; } + + [JsonPropertyName("flags")] + public string? Flags { get; } + + [JsonPropertyName("flags_desc")] + public string? FlagsDescription { get; } + + [JsonPropertyName("ip")] + public string? IPAddress { get; } + + [JsonPropertyName("peer_id_client")] + public string? ClientId { get; } + + [JsonPropertyName("port")] + public int? Port { get; } + + [JsonPropertyName("progress")] + public float? Progress { get; } + + [JsonPropertyName("relevance")] + public float? Relevance { get; } + + [JsonPropertyName("up_speed")] + public long? UploadSpeed { get; } + + [JsonPropertyName("uploaded")] + public long? Uploaded { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/PeerId.cs b/Lantean.QBitTorrentClient/Models/PeerId.cs new file mode 100644 index 0000000..436faee --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/PeerId.cs @@ -0,0 +1,14 @@ +namespace Lantean.QBitTorrentClient.Models +{ + public readonly struct PeerId(string host, int port) + { + public string Host { get; } = host; + + public int Port { get; } = port; + + public override string ToString() + { + return $"{Host}:{Port}"; + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/PeerLog.cs b/Lantean.QBitTorrentClient/Models/PeerLog.cs new file mode 100644 index 0000000..4e45224 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/PeerLog.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record PeerLog + { + [JsonConstructor] + public PeerLog( + int id, + string iPAddress, + long timestamp, + bool blocked, + string reason) + { + Id = id; + IPAddress = iPAddress; + Timestamp = timestamp; + Blocked = blocked; + Reason = reason; + } + + [JsonPropertyName("id")] + public int Id { get; } + + [JsonPropertyName("ip")] + public string IPAddress { get; } + + [JsonPropertyName("timestamp")] + public long Timestamp { get; } + + [JsonPropertyName("blocked")] + public bool Blocked { get; } + + [JsonPropertyName("reason")] + public string Reason { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/PieceState.cs b/Lantean.QBitTorrentClient/Models/PieceState.cs new file mode 100644 index 0000000..4629553 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/PieceState.cs @@ -0,0 +1,9 @@ +namespace Lantean.QBitTorrentClient.Models +{ + public enum PieceState + { + NotDownloaded = 0, + Downloading = 1, + Downloaded = 2, + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/Preferences.cs b/Lantean.QBitTorrentClient/Models/Preferences.cs new file mode 100644 index 0000000..f4baecf --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/Preferences.cs @@ -0,0 +1,1023 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record Preferences + { + [JsonConstructor] + public Preferences( + bool addToTopOfQueue, + string addTrackers, + bool addTrackersEnabled, + int altDlLimit, + int altUpLimit, + bool alternativeWebuiEnabled, + string alternativeWebuiPath, + string announceIp, + bool announceToAllTiers, + bool announceToAllTrackers, + bool anonymousMode, + int asyncIoThreads, + int autoDeleteMode, + bool autoTmmEnabled, + bool autorunEnabled, + bool autorunOnTorrentAddedEnabled, + string autorunOnTorrentAddedProgram, + string autorunProgram, + string bannedIPs, + int bdecodeDepthLimit, + int bdecodeTokenLimit, + int bittorrentProtocol, + bool blockPeersOnPrivilegedPorts, + string bypassAuthSubnetWhitelist, + bool bypassAuthSubnetWhitelistEnabled, + bool bypassLocalAuth, + bool categoryChangedTmmEnabled, + int checkingMemoryUse, + int connectionSpeed, + string currentInterfaceAddress, + string currentInterfaceName, + string currentNetworkInterface, + bool dht, + int diskCache, + int diskCacheTtl, + int diskIoReadMode, + int diskIoType, + int diskIoWriteMode, + int diskQueueSize, + int dlLimit, + bool dontCountSlowTorrents, + string dyndnsDomain, + bool dyndnsEnabled, + string dyndnsPassword, + int dyndnsService, + string dyndnsUsername, + int embeddedTrackerPort, + bool embeddedTrackerPortForwarding, + bool enableCoalesceReadWrite, + bool enableEmbeddedTracker, + bool enableMultiConnectionsFromSameIp, + bool enablePieceExtentAffinity, + bool enableUploadSuggestions, + int encryption, + string excludedFileNames, + bool excludedFileNamesEnabled, + string exportDir, + string exportDirFin, + int fileLogAge, + int fileLogAgeType, + bool fileLogBackupEnabled, + bool fileLogDeleteOld, + bool fileLogEnabled, + int fileLogMaxSize, + string fileLogPath, + int filePoolSize, + int hashingThreads, + string i2pAddress, + bool i2pEnabled, + int i2pInboundLength, + int i2pInboundQuantity, + bool i2pMixedMode, + int i2pOutboundLength, + int i2pOutboundQuantity, + int i2pPort, + bool idnSupportEnabled, + bool incompleteFilesExt, + bool ipFilterEnabled, + string ipFilterPath, + bool ipFilterTrackers, + bool limitLanPeers, + bool limitTcpOverhead, + bool limitUtpRate, + int listenPort, + string locale, + bool lsd, + bool mailNotificationAuthEnabled, + string mailNotificationEmail, + bool mailNotificationEnabled, + string mailNotificationPassword, + string mailNotificationSender, + string mailNotificationSmtp, + bool mailNotificationSslEnabled, + string mailNotificationUsername, + int maxActiveCheckingTorrents, + int maxActiveDownloads, + int maxActiveTorrents, + int maxActiveUploads, + int maxConcurrentHttpAnnounces, + int maxConnec, + int maxConnecPerTorrent, + int maxInactiveSeedingTime, + bool maxInactiveSeedingTimeEnabled, + int maxRatio, + int maxRatioAct, + bool maxRatioEnabled, + int maxSeedingTime, + bool maxSeedingTimeEnabled, + int maxUploads, + int maxUploadsPerTorrent, + int memoryWorkingSetLimit, + bool mergeTrackers, + int outgoingPortsMax, + int outgoingPortsMin, + int peerTos, + int peerTurnover, + int peerTurnoverCutoff, + int peerTurnoverInterval, + bool performanceWarning, + bool pex, + bool preallocateAll, + bool proxyAuthEnabled, + bool proxyBittorrent, + bool proxyHostnameLookup, + string proxyIp, + bool proxyMisc, + string proxyPassword, + bool proxyPeerConnections, + int proxyPort, + bool proxyRss, + string proxyType, + string proxyUsername, + bool queueingEnabled, + bool randomPort, + bool reannounceWhenAddressChanged, + bool recheckCompletedTorrents, + int refreshInterval, + int requestQueueSize, + bool resolvePeerCountries, + string resumeDataStorageType, + bool rssAutoDownloadingEnabled, + bool rssDownloadRepackProperEpisodes, + int rssMaxArticlesPerFeed, + bool rssProcessingEnabled, + int rssRefreshInterval, + string rssSmartEpisodeFilters, + string savePath, + bool savePathChangedTmmEnabled, + int saveResumeDataInterval, + Dictionary scanDirs, + int scheduleFromHour, + int scheduleFromMin, + int scheduleToHour, + int scheduleToMin, + int schedulerDays, + bool schedulerEnabled, + int sendBufferLowWatermark, + int sendBufferWatermark, + int sendBufferWatermarkFactor, + int slowTorrentDlRateThreshold, + int slowTorrentInactiveTimer, + int slowTorrentUlRateThreshold, + int socketBacklogSize, + int socketReceiveBufferSize, + int socketSendBufferSize, + bool ssrfMitigation, + bool startPausedEnabled, + int stopTrackerTimeout, + string tempPath, + bool tempPathEnabled, + bool torrentChangedTmmEnabled, + string torrentContentLayout, + int torrentFileSizeLimit, + string torrentStopCondition, + int upLimit, + int uploadChokingAlgorithm, + int uploadSlotsBehavior, + bool upnp, + int upnpLeaseDuration, + bool useCategoryPathsInManualMode, + bool useHttps, + bool useSubcategories, + int utpTcpMixedMode, + bool validateHttpsTrackerCertificate, + string webUiAddress, + int webUiBanDuration, + bool webUiClickjackingProtectionEnabled, + bool webUiCsrfProtectionEnabled, + string webUiCustomHttpHeaders, + string webUiDomainList, + bool webUiHostHeaderValidationEnabled, + string webUiHttpsCertPath, + string webUiHttpsKeyPath, + int webUiMaxAuthFailCount, + int webUiPort, + string webUiReverseProxiesList, + bool webUiReverseProxyEnabled, + bool webUiSecureCookieEnabled, + int webUiSessionTimeout, + bool webUiUpnp, + bool webUiUseCustomHttpHeadersEnabled, + string webUiUsername + ) + { + AddToTopOfQueue = addToTopOfQueue; + AddTrackers = addTrackers; + AddTrackersEnabled = addTrackersEnabled; + AltDlLimit = altDlLimit; + AltUpLimit = altUpLimit; + AlternativeWebuiEnabled = alternativeWebuiEnabled; + AlternativeWebuiPath = alternativeWebuiPath; + AnnounceIp = announceIp; + AnnounceToAllTiers = announceToAllTiers; + AnnounceToAllTrackers = announceToAllTrackers; + AnonymousMode = anonymousMode; + AsyncIoThreads = asyncIoThreads; + AutoDeleteMode = autoDeleteMode; + AutoTmmEnabled = autoTmmEnabled; + AutorunEnabled = autorunEnabled; + AutorunOnTorrentAddedEnabled = autorunOnTorrentAddedEnabled; + AutorunOnTorrentAddedProgram = autorunOnTorrentAddedProgram; + AutorunProgram = autorunProgram; + BannedIPs = bannedIPs; + BdecodeDepthLimit = bdecodeDepthLimit; + BdecodeTokenLimit = bdecodeTokenLimit; + BittorrentProtocol = bittorrentProtocol; + BlockPeersOnPrivilegedPorts = blockPeersOnPrivilegedPorts; + BypassAuthSubnetWhitelist = bypassAuthSubnetWhitelist; + BypassAuthSubnetWhitelistEnabled = bypassAuthSubnetWhitelistEnabled; + BypassLocalAuth = bypassLocalAuth; + CategoryChangedTmmEnabled = categoryChangedTmmEnabled; + CheckingMemoryUse = checkingMemoryUse; + ConnectionSpeed = connectionSpeed; + CurrentInterfaceAddress = currentInterfaceAddress; + CurrentInterfaceName = currentInterfaceName; + CurrentNetworkInterface = currentNetworkInterface; + Dht = dht; + DiskCache = diskCache; + DiskCacheTtl = diskCacheTtl; + DiskIoReadMode = diskIoReadMode; + DiskIoType = diskIoType; + DiskIoWriteMode = diskIoWriteMode; + DiskQueueSize = diskQueueSize; + DlLimit = dlLimit; + DontCountSlowTorrents = dontCountSlowTorrents; + DyndnsDomain = dyndnsDomain; + DyndnsEnabled = dyndnsEnabled; + DyndnsPassword = dyndnsPassword; + DyndnsService = dyndnsService; + DyndnsUsername = dyndnsUsername; + EmbeddedTrackerPort = embeddedTrackerPort; + EmbeddedTrackerPortForwarding = embeddedTrackerPortForwarding; + EnableCoalesceReadWrite = enableCoalesceReadWrite; + EnableEmbeddedTracker = enableEmbeddedTracker; + EnableMultiConnectionsFromSameIp = enableMultiConnectionsFromSameIp; + EnablePieceExtentAffinity = enablePieceExtentAffinity; + EnableUploadSuggestions = enableUploadSuggestions; + Encryption = encryption; + ExcludedFileNames = excludedFileNames; + ExcludedFileNamesEnabled = excludedFileNamesEnabled; + ExportDir = exportDir; + ExportDirFin = exportDirFin; + FileLogAge = fileLogAge; + FileLogAgeType = fileLogAgeType; + FileLogBackupEnabled = fileLogBackupEnabled; + FileLogDeleteOld = fileLogDeleteOld; + FileLogEnabled = fileLogEnabled; + FileLogMaxSize = fileLogMaxSize; + FileLogPath = fileLogPath; + FilePoolSize = filePoolSize; + HashingThreads = hashingThreads; + I2pAddress = i2pAddress; + I2pEnabled = i2pEnabled; + I2pInboundLength = i2pInboundLength; + I2pInboundQuantity = i2pInboundQuantity; + I2pMixedMode = i2pMixedMode; + I2pOutboundLength = i2pOutboundLength; + I2pOutboundQuantity = i2pOutboundQuantity; + I2pPort = i2pPort; + IdnSupportEnabled = idnSupportEnabled; + IncompleteFilesExt = incompleteFilesExt; + IpFilterEnabled = ipFilterEnabled; + IpFilterPath = ipFilterPath; + IpFilterTrackers = ipFilterTrackers; + LimitLanPeers = limitLanPeers; + LimitTcpOverhead = limitTcpOverhead; + LimitUtpRate = limitUtpRate; + ListenPort = listenPort; + Locale = locale; + Lsd = lsd; + MailNotificationAuthEnabled = mailNotificationAuthEnabled; + MailNotificationEmail = mailNotificationEmail; + MailNotificationEnabled = mailNotificationEnabled; + MailNotificationPassword = mailNotificationPassword; + MailNotificationSender = mailNotificationSender; + MailNotificationSmtp = mailNotificationSmtp; + MailNotificationSslEnabled = mailNotificationSslEnabled; + MailNotificationUsername = mailNotificationUsername; + MaxActiveCheckingTorrents = maxActiveCheckingTorrents; + MaxActiveDownloads = maxActiveDownloads; + MaxActiveTorrents = maxActiveTorrents; + MaxActiveUploads = maxActiveUploads; + MaxConcurrentHttpAnnounces = maxConcurrentHttpAnnounces; + MaxConnec = maxConnec; + MaxConnecPerTorrent = maxConnecPerTorrent; + MaxInactiveSeedingTime = maxInactiveSeedingTime; + MaxInactiveSeedingTimeEnabled = maxInactiveSeedingTimeEnabled; + MaxRatio = maxRatio; + MaxRatioAct = maxRatioAct; + MaxRatioEnabled = maxRatioEnabled; + MaxSeedingTime = maxSeedingTime; + MaxSeedingTimeEnabled = maxSeedingTimeEnabled; + MaxUploads = maxUploads; + MaxUploadsPerTorrent = maxUploadsPerTorrent; + MemoryWorkingSetLimit = memoryWorkingSetLimit; + MergeTrackers = mergeTrackers; + OutgoingPortsMax = outgoingPortsMax; + OutgoingPortsMin = outgoingPortsMin; + PeerTos = peerTos; + PeerTurnover = peerTurnover; + PeerTurnoverCutoff = peerTurnoverCutoff; + PeerTurnoverInterval = peerTurnoverInterval; + PerformanceWarning = performanceWarning; + Pex = pex; + PreallocateAll = preallocateAll; + ProxyAuthEnabled = proxyAuthEnabled; + ProxyBittorrent = proxyBittorrent; + ProxyHostnameLookup = proxyHostnameLookup; + ProxyIp = proxyIp; + ProxyMisc = proxyMisc; + ProxyPassword = proxyPassword; + ProxyPeerConnections = proxyPeerConnections; + ProxyPort = proxyPort; + ProxyRss = proxyRss; + ProxyType = proxyType; + ProxyUsername = proxyUsername; + QueueingEnabled = queueingEnabled; + RandomPort = randomPort; + ReannounceWhenAddressChanged = reannounceWhenAddressChanged; + RecheckCompletedTorrents = recheckCompletedTorrents; + RefreshInterval = refreshInterval; + RequestQueueSize = requestQueueSize; + ResolvePeerCountries = resolvePeerCountries; + ResumeDataStorageType = resumeDataStorageType; + RssAutoDownloadingEnabled = rssAutoDownloadingEnabled; + RssDownloadRepackProperEpisodes = rssDownloadRepackProperEpisodes; + RssMaxArticlesPerFeed = rssMaxArticlesPerFeed; + RssProcessingEnabled = rssProcessingEnabled; + RssRefreshInterval = rssRefreshInterval; + RssSmartEpisodeFilters = rssSmartEpisodeFilters; + SavePath = savePath; + SavePathChangedTmmEnabled = savePathChangedTmmEnabled; + SaveResumeDataInterval = saveResumeDataInterval; + ScanDirs = scanDirs; + ScheduleFromHour = scheduleFromHour; + ScheduleFromMin = scheduleFromMin; + ScheduleToHour = scheduleToHour; + ScheduleToMin = scheduleToMin; + SchedulerDays = schedulerDays; + SchedulerEnabled = schedulerEnabled; + SendBufferLowWatermark = sendBufferLowWatermark; + SendBufferWatermark = sendBufferWatermark; + SendBufferWatermarkFactor = sendBufferWatermarkFactor; + SlowTorrentDlRateThreshold = slowTorrentDlRateThreshold; + SlowTorrentInactiveTimer = slowTorrentInactiveTimer; + SlowTorrentUlRateThreshold = slowTorrentUlRateThreshold; + SocketBacklogSize = socketBacklogSize; + SocketReceiveBufferSize = socketReceiveBufferSize; + SocketSendBufferSize = socketSendBufferSize; + SsrfMitigation = ssrfMitigation; + StartPausedEnabled = startPausedEnabled; + StopTrackerTimeout = stopTrackerTimeout; + TempPath = tempPath; + TempPathEnabled = tempPathEnabled; + TorrentChangedTmmEnabled = torrentChangedTmmEnabled; + TorrentContentLayout = torrentContentLayout; + TorrentFileSizeLimit = torrentFileSizeLimit; + TorrentStopCondition = torrentStopCondition; + UpLimit = upLimit; + UploadChokingAlgorithm = uploadChokingAlgorithm; + UploadSlotsBehavior = uploadSlotsBehavior; + Upnp = upnp; + UpnpLeaseDuration = upnpLeaseDuration; + UseCategoryPathsInManualMode = useCategoryPathsInManualMode; + UseHttps = useHttps; + UseSubcategories = useSubcategories; + UtpTcpMixedMode = utpTcpMixedMode; + ValidateHttpsTrackerCertificate = validateHttpsTrackerCertificate; + WebUiAddress = webUiAddress; + WebUiBanDuration = webUiBanDuration; + WebUiClickjackingProtectionEnabled = webUiClickjackingProtectionEnabled; + WebUiCsrfProtectionEnabled = webUiCsrfProtectionEnabled; + WebUiCustomHttpHeaders = webUiCustomHttpHeaders; + WebUiDomainList = webUiDomainList; + WebUiHostHeaderValidationEnabled = webUiHostHeaderValidationEnabled; + WebUiHttpsCertPath = webUiHttpsCertPath; + WebUiHttpsKeyPath = webUiHttpsKeyPath; + WebUiMaxAuthFailCount = webUiMaxAuthFailCount; + WebUiPort = webUiPort; + WebUiReverseProxiesList = webUiReverseProxiesList; + WebUiReverseProxyEnabled = webUiReverseProxyEnabled; + WebUiSecureCookieEnabled = webUiSecureCookieEnabled; + WebUiSessionTimeout = webUiSessionTimeout; + WebUiUpnp = webUiUpnp; + WebUiUseCustomHttpHeadersEnabled = webUiUseCustomHttpHeadersEnabled; + WebUiUsername = webUiUsername; + } + + [JsonPropertyName("add_to_top_of_queue")] + public bool AddToTopOfQueue { get; } + + [JsonPropertyName("add_trackers")] + public string AddTrackers { get; } + + [JsonPropertyName("add_trackers_enabled")] + public bool AddTrackersEnabled { get; } + + [JsonPropertyName("alt_dl_limit")] + public int AltDlLimit { get; } + + [JsonPropertyName("alt_up_limit")] + public int AltUpLimit { get; } + + [JsonPropertyName("alternative_webui_enabled")] + public bool AlternativeWebuiEnabled { get; } + + [JsonPropertyName("alternative_webui_path")] + public string AlternativeWebuiPath { get; } + + [JsonPropertyName("announce_ip")] + public string AnnounceIp { get; } + + [JsonPropertyName("announce_to_all_tiers")] + public bool AnnounceToAllTiers { get; } + + [JsonPropertyName("announce_to_all_trackers")] + public bool AnnounceToAllTrackers { get; } + + [JsonPropertyName("anonymous_mode")] + public bool AnonymousMode { get; } + + [JsonPropertyName("async_io_threads")] + public int AsyncIoThreads { get; } + + [JsonPropertyName("auto_delete_mode")] + public int AutoDeleteMode { get; } + + [JsonPropertyName("auto_tmm_enabled")] + public bool AutoTmmEnabled { get; } + + [JsonPropertyName("autorun_enabled")] + public bool AutorunEnabled { get; } + + [JsonPropertyName("autorun_on_torrent_added_enabled")] + public bool AutorunOnTorrentAddedEnabled { get; } + + [JsonPropertyName("autorun_on_torrent_added_program")] + public string AutorunOnTorrentAddedProgram { get; } + + [JsonPropertyName("autorun_program")] + public string AutorunProgram { get; } + + [JsonPropertyName("banned_IPs")] + public string BannedIPs { get; } + + [JsonPropertyName("bdecode_depth_limit")] + public int BdecodeDepthLimit { get; } + + [JsonPropertyName("bdecode_token_limit")] + public int BdecodeTokenLimit { get; } + + [JsonPropertyName("bittorrent_protocol")] + public int BittorrentProtocol { get; } + + [JsonPropertyName("block_peers_on_privileged_ports")] + public bool BlockPeersOnPrivilegedPorts { get; } + + [JsonPropertyName("bypass_auth_subnet_whitelist")] + public string BypassAuthSubnetWhitelist { get; } + + [JsonPropertyName("bypass_auth_subnet_whitelist_enabled")] + public bool BypassAuthSubnetWhitelistEnabled { get; } + + [JsonPropertyName("bypass_local_auth")] + public bool BypassLocalAuth { get; } + + [JsonPropertyName("category_changed_tmm_enabled")] + public bool CategoryChangedTmmEnabled { get; } + + [JsonPropertyName("checking_memory_use")] + public int CheckingMemoryUse { get; } + + [JsonPropertyName("connection_speed")] + public int ConnectionSpeed { get; } + + [JsonPropertyName("current_interface_address")] + public string CurrentInterfaceAddress { get; } + + [JsonPropertyName("current_interface_name")] + public string CurrentInterfaceName { get; } + + [JsonPropertyName("current_network_interface")] + public string CurrentNetworkInterface { get; } + + [JsonPropertyName("dht")] + public bool Dht { get; } + + [JsonPropertyName("disk_cache")] + public int DiskCache { get; } + + [JsonPropertyName("disk_cache_ttl")] + public int DiskCacheTtl { get; } + + [JsonPropertyName("disk_io_read_mode")] + public int DiskIoReadMode { get; } + + [JsonPropertyName("disk_io_type")] + public int DiskIoType { get; } + + [JsonPropertyName("disk_io_write_mode")] + public int DiskIoWriteMode { get; } + + [JsonPropertyName("disk_queue_size")] + public int DiskQueueSize { get; } + + [JsonPropertyName("dl_limit")] + public int DlLimit { get; } + + [JsonPropertyName("dont_count_slow_torrents")] + public bool DontCountSlowTorrents { get; } + + [JsonPropertyName("dyndns_domain")] + public string DyndnsDomain { get; } + + [JsonPropertyName("dyndns_enabled")] + public bool DyndnsEnabled { get; } + + [JsonPropertyName("dyndns_password")] + public string DyndnsPassword { get; } + + [JsonPropertyName("dyndns_service")] + public int DyndnsService { get; } + + [JsonPropertyName("dyndns_username")] + public string DyndnsUsername { get; } + + [JsonPropertyName("embedded_tracker_port")] + public int EmbeddedTrackerPort { get; } + + [JsonPropertyName("embedded_tracker_port_forwarding")] + public bool EmbeddedTrackerPortForwarding { get; } + + [JsonPropertyName("enable_coalesce_read_write")] + public bool EnableCoalesceReadWrite { get; } + + [JsonPropertyName("enable_embedded_tracker")] + public bool EnableEmbeddedTracker { get; } + + [JsonPropertyName("enable_multi_connections_from_same_ip")] + public bool EnableMultiConnectionsFromSameIp { get; } + + [JsonPropertyName("enable_piece_extent_affinity")] + public bool EnablePieceExtentAffinity { get; } + + [JsonPropertyName("enable_upload_suggestions")] + public bool EnableUploadSuggestions { get; } + + [JsonPropertyName("encryption")] + public int Encryption { get; } + + [JsonPropertyName("excluded_file_names")] + public string ExcludedFileNames { get; } + + [JsonPropertyName("excluded_file_names_enabled")] + public bool ExcludedFileNamesEnabled { get; } + + [JsonPropertyName("export_dir")] + public string ExportDir { get; } + + [JsonPropertyName("export_dir_fin")] + public string ExportDirFin { get; } + + [JsonPropertyName("file_log_age")] + public int FileLogAge { get; } + + [JsonPropertyName("file_log_age_type")] + public int FileLogAgeType { get; } + + [JsonPropertyName("file_log_backup_enabled")] + public bool FileLogBackupEnabled { get; } + + [JsonPropertyName("file_log_delete_old")] + public bool FileLogDeleteOld { get; } + + [JsonPropertyName("file_log_enabled")] + public bool FileLogEnabled { get; } + + [JsonPropertyName("file_log_max_size")] + public int FileLogMaxSize { get; } + + [JsonPropertyName("file_log_path")] + public string FileLogPath { get; } + + [JsonPropertyName("file_pool_size")] + public int FilePoolSize { get; } + + [JsonPropertyName("hashing_threads")] + public int HashingThreads { get; } + + [JsonPropertyName("i2p_address")] + public string I2pAddress { get; } + + [JsonPropertyName("i2p_enabled")] + public bool I2pEnabled { get; } + + [JsonPropertyName("i2p_inbound_length")] + public int I2pInboundLength { get; } + + [JsonPropertyName("i2p_inbound_quantity")] + public int I2pInboundQuantity { get; } + + [JsonPropertyName("i2p_mixed_mode")] + public bool I2pMixedMode { get; } + + [JsonPropertyName("i2p_outbound_length")] + public int I2pOutboundLength { get; } + + [JsonPropertyName("i2p_outbound_quantity")] + public int I2pOutboundQuantity { get; } + + [JsonPropertyName("i2p_port")] + public int I2pPort { get; } + + [JsonPropertyName("idn_support_enabled")] + public bool IdnSupportEnabled { get; } + + [JsonPropertyName("incomplete_files_ext")] + public bool IncompleteFilesExt { get; } + + [JsonPropertyName("ip_filter_enabled")] + public bool IpFilterEnabled { get; } + + [JsonPropertyName("ip_filter_path")] + public string IpFilterPath { get; } + + [JsonPropertyName("ip_filter_trackers")] + public bool IpFilterTrackers { get; } + + [JsonPropertyName("limit_lan_peers")] + public bool LimitLanPeers { get; } + + [JsonPropertyName("limit_tcp_overhead")] + public bool LimitTcpOverhead { get; } + + [JsonPropertyName("limit_utp_rate")] + public bool LimitUtpRate { get; } + + [JsonPropertyName("listen_port")] + public int ListenPort { get; } + + [JsonPropertyName("locale")] + public string Locale { get; } + + [JsonPropertyName("lsd")] + public bool Lsd { get; } + + [JsonPropertyName("mail_notification_auth_enabled")] + public bool MailNotificationAuthEnabled { get; } + + [JsonPropertyName("mail_notification_email")] + public string MailNotificationEmail { get; } + + [JsonPropertyName("mail_notification_enabled")] + public bool MailNotificationEnabled { get; } + + [JsonPropertyName("mail_notification_password")] + public string MailNotificationPassword { get; } + + [JsonPropertyName("mail_notification_sender")] + public string MailNotificationSender { get; } + + [JsonPropertyName("mail_notification_smtp")] + public string MailNotificationSmtp { get; } + + [JsonPropertyName("mail_notification_ssl_enabled")] + public bool MailNotificationSslEnabled { get; } + + [JsonPropertyName("mail_notification_username")] + public string MailNotificationUsername { get; } + + [JsonPropertyName("max_active_checking_torrents")] + public int MaxActiveCheckingTorrents { get; } + + [JsonPropertyName("max_active_downloads")] + public int MaxActiveDownloads { get; } + + [JsonPropertyName("max_active_torrents")] + public int MaxActiveTorrents { get; } + + [JsonPropertyName("max_active_uploads")] + public int MaxActiveUploads { get; } + + [JsonPropertyName("max_concurrent_http_announces")] + public int MaxConcurrentHttpAnnounces { get; } + + [JsonPropertyName("max_connec")] + public int MaxConnec { get; } + + [JsonPropertyName("max_connec_per_torrent")] + public int MaxConnecPerTorrent { get; } + + [JsonPropertyName("max_inactive_seeding_time")] + public int MaxInactiveSeedingTime { get; } + + [JsonPropertyName("max_inactive_seeding_time_enabled")] + public bool MaxInactiveSeedingTimeEnabled { get; } + + [JsonPropertyName("max_ratio")] + public int MaxRatio { get; } + + [JsonPropertyName("max_ratio_act")] + public int MaxRatioAct { get; } + + [JsonPropertyName("max_ratio_enabled")] + public bool MaxRatioEnabled { get; } + + [JsonPropertyName("max_seeding_time")] + public int MaxSeedingTime { get; } + + [JsonPropertyName("max_seeding_time_enabled")] + public bool MaxSeedingTimeEnabled { get; } + + [JsonPropertyName("max_uploads")] + public int MaxUploads { get; } + + [JsonPropertyName("max_uploads_per_torrent")] + public int MaxUploadsPerTorrent { get; } + + [JsonPropertyName("memory_working_set_limit")] + public int MemoryWorkingSetLimit { get; } + + [JsonPropertyName("merge_trackers")] + public bool MergeTrackers { get; } + + [JsonPropertyName("outgoing_ports_max")] + public int OutgoingPortsMax { get; } + + [JsonPropertyName("outgoing_ports_min")] + public int OutgoingPortsMin { get; } + + [JsonPropertyName("peer_tos")] + public int PeerTos { get; } + + [JsonPropertyName("peer_turnover")] + public int PeerTurnover { get; } + + [JsonPropertyName("peer_turnover_cutoff")] + public int PeerTurnoverCutoff { get; } + + [JsonPropertyName("peer_turnover_interval")] + public int PeerTurnoverInterval { get; } + + [JsonPropertyName("performance_warning")] + public bool PerformanceWarning { get; } + + [JsonPropertyName("pex")] + public bool Pex { get; } + + [JsonPropertyName("preallocate_all")] + public bool PreallocateAll { get; } + + [JsonPropertyName("proxy_auth_enabled")] + public bool ProxyAuthEnabled { get; } + + [JsonPropertyName("proxy_bittorrent")] + public bool ProxyBittorrent { get; } + + [JsonPropertyName("proxy_hostname_lookup")] + public bool ProxyHostnameLookup { get; } + + [JsonPropertyName("proxy_ip")] + public string ProxyIp { get; } + + [JsonPropertyName("proxy_misc")] + public bool ProxyMisc { get; } + + [JsonPropertyName("proxy_password")] + public string ProxyPassword { get; } + + [JsonPropertyName("proxy_peer_connections")] + public bool ProxyPeerConnections { get; } + + [JsonPropertyName("proxy_port")] + public int ProxyPort { get; } + + [JsonPropertyName("proxy_rss")] + public bool ProxyRss { get; } + + [JsonPropertyName("proxy_type")] + public string ProxyType { get; } + + [JsonPropertyName("proxy_username")] + public string ProxyUsername { get; } + + [JsonPropertyName("queueing_enabled")] + public bool QueueingEnabled { get; } + + [JsonPropertyName("random_port")] + public bool RandomPort { get; } + + [JsonPropertyName("reannounce_when_address_changed")] + public bool ReannounceWhenAddressChanged { get; } + + [JsonPropertyName("recheck_completed_torrents")] + public bool RecheckCompletedTorrents { get; } + + [JsonPropertyName("refresh_interval")] + public int RefreshInterval { get; } + + [JsonPropertyName("request_queue_size")] + public int RequestQueueSize { get; } + + [JsonPropertyName("resolve_peer_countries")] + public bool ResolvePeerCountries { get; } + + [JsonPropertyName("resume_data_storage_type")] + public string ResumeDataStorageType { get; } + + [JsonPropertyName("rss_auto_downloading_enabled")] + public bool RssAutoDownloadingEnabled { get; } + + [JsonPropertyName("rss_download_repack_proper_episodes")] + public bool RssDownloadRepackProperEpisodes { get; } + + [JsonPropertyName("rss_max_articles_per_feed")] + public int RssMaxArticlesPerFeed { get; } + + [JsonPropertyName("rss_processing_enabled")] + public bool RssProcessingEnabled { get; } + + [JsonPropertyName("rss_refresh_interval")] + public int RssRefreshInterval { get; } + + [JsonPropertyName("rss_smart_episode_filters")] + public string RssSmartEpisodeFilters { get; } + + [JsonPropertyName("save_path")] + public string SavePath { get; } + + [JsonPropertyName("save_path_changed_tmm_enabled")] + public bool SavePathChangedTmmEnabled { get; } + + [JsonPropertyName("save_resume_data_interval")] + public int SaveResumeDataInterval { get; } + + [JsonPropertyName("scan_dirs")] + public Dictionary ScanDirs { get; } + + [JsonPropertyName("schedule_from_hour")] + public int ScheduleFromHour { get; } + + [JsonPropertyName("schedule_from_min")] + public int ScheduleFromMin { get; } + + [JsonPropertyName("schedule_to_hour")] + public int ScheduleToHour { get; } + + [JsonPropertyName("schedule_to_min")] + public int ScheduleToMin { get; } + + [JsonPropertyName("scheduler_days")] + public int SchedulerDays { get; } + + [JsonPropertyName("scheduler_enabled")] + public bool SchedulerEnabled { get; } + + [JsonPropertyName("send_buffer_low_watermark")] + public int SendBufferLowWatermark { get; } + + [JsonPropertyName("send_buffer_watermark")] + public int SendBufferWatermark { get; } + + [JsonPropertyName("send_buffer_watermark_factor")] + public int SendBufferWatermarkFactor { get; } + + [JsonPropertyName("slow_torrent_dl_rate_threshold")] + public int SlowTorrentDlRateThreshold { get; } + + [JsonPropertyName("slow_torrent_inactive_timer")] + public int SlowTorrentInactiveTimer { get; } + + [JsonPropertyName("slow_torrent_ul_rate_threshold")] + public int SlowTorrentUlRateThreshold { get; } + + [JsonPropertyName("socket_backlog_size")] + public int SocketBacklogSize { get; } + + [JsonPropertyName("socket_receive_buffer_size")] + public int SocketReceiveBufferSize { get; } + + [JsonPropertyName("socket_send_buffer_size")] + public int SocketSendBufferSize { get; } + + [JsonPropertyName("ssrf_mitigation")] + public bool SsrfMitigation { get; } + + [JsonPropertyName("start_paused_enabled")] + public bool StartPausedEnabled { get; } + + [JsonPropertyName("stop_tracker_timeout")] + public int StopTrackerTimeout { get; } + + [JsonPropertyName("temp_path")] + public string TempPath { get; } + + [JsonPropertyName("temp_path_enabled")] + public bool TempPathEnabled { get; } + + [JsonPropertyName("torrent_changed_tmm_enabled")] + public bool TorrentChangedTmmEnabled { get; } + + [JsonPropertyName("torrent_content_layout")] + public string TorrentContentLayout { get; } + + [JsonPropertyName("torrent_file_size_limit")] + public int TorrentFileSizeLimit { get; } + + [JsonPropertyName("torrent_stop_condition")] + public string TorrentStopCondition { get; } + + [JsonPropertyName("up_limit")] + public int UpLimit { get; } + + [JsonPropertyName("upload_choking_algorithm")] + public int UploadChokingAlgorithm { get; } + + [JsonPropertyName("upload_slots_behavior")] + public int UploadSlotsBehavior { get; } + + [JsonPropertyName("upnp")] + public bool Upnp { get; } + + [JsonPropertyName("upnp_lease_duration")] + public int UpnpLeaseDuration { get; } + + [JsonPropertyName("use_category_paths_in_manual_mode")] + public bool UseCategoryPathsInManualMode { get; } + + [JsonPropertyName("use_https")] + public bool UseHttps { get; } + + [JsonPropertyName("use_subcategories")] + public bool UseSubcategories { get; } + + [JsonPropertyName("utp_tcp_mixed_mode")] + public int UtpTcpMixedMode { get; } + + [JsonPropertyName("validate_https_tracker_certificate")] + public bool ValidateHttpsTrackerCertificate { get; } + + [JsonPropertyName("web_ui_address")] + public string WebUiAddress { get; } + + [JsonPropertyName("web_ui_ban_duration")] + public int WebUiBanDuration { get; } + + [JsonPropertyName("web_ui_clickjacking_protection_enabled")] + public bool WebUiClickjackingProtectionEnabled { get; } + + [JsonPropertyName("web_ui_csrf_protection_enabled")] + public bool WebUiCsrfProtectionEnabled { get; } + + [JsonPropertyName("web_ui_custom_http_headers")] + public string WebUiCustomHttpHeaders { get; } + + [JsonPropertyName("web_ui_domain_list")] + public string WebUiDomainList { get; } + + [JsonPropertyName("web_ui_host_header_validation_enabled")] + public bool WebUiHostHeaderValidationEnabled { get; } + + [JsonPropertyName("web_ui_https_cert_path")] + public string WebUiHttpsCertPath { get; } + + [JsonPropertyName("web_ui_https_key_path")] + public string WebUiHttpsKeyPath { get; } + + [JsonPropertyName("web_ui_max_auth_fail_count")] + public int WebUiMaxAuthFailCount { get; } + + [JsonPropertyName("web_ui_port")] + public int WebUiPort { get; } + + [JsonPropertyName("web_ui_reverse_proxies_list")] + public string WebUiReverseProxiesList { get; } + + [JsonPropertyName("web_ui_reverse_proxy_enabled")] + public bool WebUiReverseProxyEnabled { get; } + + [JsonPropertyName("web_ui_secure_cookie_enabled")] + public bool WebUiSecureCookieEnabled { get; } + + [JsonPropertyName("web_ui_session_timeout")] + public int WebUiSessionTimeout { get; } + + [JsonPropertyName("web_ui_upnp")] + public bool WebUiUpnp { get; } + + [JsonPropertyName("web_ui_use_custom_http_headers_enabled")] + public bool WebUiUseCustomHttpHeadersEnabled { get; } + + [JsonPropertyName("web_ui_username")] + public string WebUiUsername { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/Priority.cs b/Lantean.QBitTorrentClient/Models/Priority.cs new file mode 100644 index 0000000..78da911 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/Priority.cs @@ -0,0 +1,10 @@ +namespace Lantean.QBitTorrentClient.Models +{ + public enum Priority + { + DoNotDownload = 0, + Normal = 1, + High = 6, + Maximum = 7 + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/SaveLocation.cs b/Lantean.QBitTorrentClient/Models/SaveLocation.cs new file mode 100644 index 0000000..62855a1 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/SaveLocation.cs @@ -0,0 +1,82 @@ + +namespace Lantean.QBitTorrentClient.Models +{ + public class SaveLocation + { + public bool IsWatchedFolder { get; set; } + + public bool IsDefaltFolder { get; set; } + + public string? SavePath { get; set; } + + public static SaveLocation Create(object? value) + { + if (value is int intValue) + { + if (intValue == 0) + { + return new SaveLocation + { + IsWatchedFolder = true + }; + } + else if (intValue == 1) + { + return new SaveLocation + { + IsDefaltFolder = true + }; + } + } + else if (value is string stringValue) + { + if (stringValue == "0") + { + return new SaveLocation + { + IsWatchedFolder = true + }; + } + else if (stringValue == "1") + { + return new SaveLocation + { + IsDefaltFolder = true + }; + } + else + { + return new SaveLocation + { + SavePath = stringValue + }; + } + } + + throw new ArgumentOutOfRangeException(nameof(value)); + } + + public object ToValue() + { + if (IsWatchedFolder) + { + return 0; + } + else if (IsDefaltFolder) + { + return 1; + } + else if (SavePath is not null) + { + return SavePath; + } + + throw new InvalidOperationException("Invalid value."); + } + + public override string? ToString() + { + return ToValue().ToString(); + } + } +} diff --git a/Lantean.QBitTorrentClient/Models/ServerState.cs b/Lantean.QBitTorrentClient/Models/ServerState.cs new file mode 100644 index 0000000..920c40c --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/ServerState.cs @@ -0,0 +1,105 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record ServerState : GlobalTransferInfo + { + [JsonConstructor] + public ServerState( + long? allTimeDownloaded, + long? allTimeUploaded, + int? averageTimeQueue, + string? connectionStatus, + int? dHTNodes, + long? downloadInfoData, + long? downloadInfoSpeed, + long? downloadRateLimit, + long? freeSpaceOnDisk, + float? globalRatio, + int? queuedIOJobs, + bool? queuing, + float? readCacheHits, + float? readCacheOverload, + int? refreshInterval, + int? totalBuffersSize, + int? totalPeerConnections, + int? totalQueuedSize, + long? totalWastedSession, + long? uploadInfoData, + long? uploadInfoSpeed, + long? uploadRateLimit, + bool? useAltSpeedLimits, + bool? useSubcategories, + float? writeCacheOverload) : base(connectionStatus, dHTNodes, downloadInfoData, downloadInfoSpeed, downloadRateLimit, uploadInfoData, uploadInfoSpeed, uploadRateLimit) + { + AllTimeDownloaded = allTimeDownloaded; + AllTimeUploaded = allTimeUploaded; + AverageTimeQueue = averageTimeQueue; + FreeSpaceOnDisk = freeSpaceOnDisk; + GlobalRatio = globalRatio; + QueuedIOJobs = queuedIOJobs; + Queuing = queuing; + ReadCacheHits = readCacheHits; + ReadCacheOverload = readCacheOverload; + RefreshInterval = refreshInterval; + TotalBuffersSize = totalBuffersSize; + TotalPeerConnections = totalPeerConnections; + TotalQueuedSize = totalQueuedSize; + TotalWastedSession = totalWastedSession; + UseAltSpeedLimits = useAltSpeedLimits; + UseSubcategories = useSubcategories; + WriteCacheOverload = writeCacheOverload; + } + + [JsonPropertyName("alltime_dl")] + public long? AllTimeDownloaded { get; } + + [JsonPropertyName("alltime_ul")] + public long? AllTimeUploaded { get; } + + [JsonPropertyName("average_time_queue")] + public int? AverageTimeQueue { get; } + + [JsonPropertyName("free_space_on_disk")] + public long? FreeSpaceOnDisk { get; } + + [JsonPropertyName("global_ratio")] + public float? GlobalRatio { get; } + + [JsonPropertyName("queued_io_jobs")] + public int? QueuedIOJobs { get; } + + [JsonPropertyName("queueing")] + public bool? Queuing { get; } + + [JsonPropertyName("read_cache_hits")] + public float? ReadCacheHits { get; } + + [JsonPropertyName("read_cache_overload")] + public float? ReadCacheOverload { get; } + + [JsonPropertyName("refresh_interval")] + public int? RefreshInterval { get; } + + [JsonPropertyName("total_buffers_size")] + public int? TotalBuffersSize { get; } + + [JsonPropertyName("total_peer_connections")] + public int? TotalPeerConnections { get; } + + [JsonPropertyName("total_queued_size")] + public int? TotalQueuedSize { get; } + + [JsonPropertyName("total_wasted_session")] + public long? TotalWastedSession { get; } + + [JsonPropertyName("use_alt_speed_limits")] + public bool? UseAltSpeedLimits { get; } + + [JsonPropertyName("use_subcategories")] + public bool? UseSubcategories { get; } + + [JsonPropertyName("write_cache_overload")] + public float? WriteCacheOverload { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/Torrent.cs b/Lantean.QBitTorrentClient/Models/Torrent.cs new file mode 100644 index 0000000..cf750c8 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/Torrent.cs @@ -0,0 +1,249 @@ +using Lantean.QBitTorrentClient.Converters; +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? 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? tags, + int? timeActive, + long? totalSize, + string? tracker, + long? uploadLimit, + long? uploaded, + long? uploadedSession, + long? uploadSpeed, + long? reannounce) + { + 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; + 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; + } + + [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("infohash_v1")] + public string? InfoHashV1 { get; } + + [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; } + + [JsonPropertyName("name")] + public string? Name { get; } + + [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("size")] + public long? Size { get; } + + [JsonPropertyName("state")] + public string? State { get; } + + [JsonPropertyName("super_seeding")] + public bool? SuperSeeding { get; } + + [JsonPropertyName("tags")] + [JsonConverter(typeof(CommaSeparatedJsonConverter))] + public IReadOnlyList? Tags { get; } + + [JsonPropertyName("time_active")] + public int? TimeActive { get; } + + [JsonPropertyName("total_size")] + public long? TotalSize { get; } + + [JsonPropertyName("tracker")] + public string? Tracker { get; } + + [JsonPropertyName("up_limit")] + public long? UploadLimit { get; } + + [JsonPropertyName("uploaded")] + public long? Uploaded { get; } + + [JsonPropertyName("uploaded_session")] + public long? UploadedSession { get; } + + [JsonPropertyName("upspeed")] + public long? UploadSpeed { get; } + + [JsonPropertyName("reannounce")] + public long? Reannounce { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/TorrentPeers.cs b/Lantean.QBitTorrentClient/Models/TorrentPeers.cs new file mode 100644 index 0000000..02b1b4e --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/TorrentPeers.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record TorrentPeers + { + [JsonConstructor] + public TorrentPeers( + bool fullUpdate, + IReadOnlyDictionary? peers, + IReadOnlyList? peersRemoved, + int requestId, + bool? showFlags) + { + FullUpdate = fullUpdate; + Peers = peers; + PeersRemoved = peersRemoved; + RequestId = requestId; + ShowFlags = showFlags; + } + + [JsonPropertyName("full_update")] + public bool FullUpdate { get; } + + [JsonPropertyName("peers")] + public IReadOnlyDictionary? Peers { get; } + + [JsonPropertyName("peers_removed")] + public IReadOnlyList? PeersRemoved { get; } + + [JsonPropertyName("rid")] + public int RequestId { get; } + + [JsonPropertyName("show_flags")] + public bool? ShowFlags { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/TorrentProperties.cs b/Lantean.QBitTorrentClient/Models/TorrentProperties.cs new file mode 100644 index 0000000..7453e5b --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/TorrentProperties.cs @@ -0,0 +1,187 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record TorrentProperties + { + [JsonConstructor] + public TorrentProperties( + long additionDate, + string comment, + long completionDate, + string createdBy, + long creationDate, + long downloadLimit, + long downloadSpeed, + long downloadSpeedAverage, + int estimatedTimeOfArrival, + long lastSeen, + int connections, + int connectionsLimit, + int peers, + int peersTotal, + int pieceSize, + int piecesHave, + int piecesNum, + int reannounce, + string savePath, + int seedingTime, + int seeds, + int seedsTotal, + float shareRatio, + int timeElapsed, + long totalDownloaded, + long totalDownloadedSession, + long totalSize, + long totalUploaded, + long totalUploadedSession, + long totalWasted, + long uploadLimit, + long uploadSpeed, + long uploadSpeedAverage, + string infoHashV1, + string infoHashV2) + { + AdditionDate = additionDate; + Comment = comment; + CompletionDate = completionDate; + CreatedBy = createdBy; + CreationDate = creationDate; + DownloadLimit = downloadLimit; + DownloadSpeed = downloadSpeed; + DownloadSpeedAverage = downloadSpeedAverage; + EstimatedTimeOfArrival = estimatedTimeOfArrival; + LastSeen = lastSeen; + Connections = connections; + ConnectionsLimit = connectionsLimit; + Peers = peers; + PeersTotal = peersTotal; + PieceSize = pieceSize; + PiecesHave = piecesHave; + PiecesNum = piecesNum; + Reannounce = reannounce; + SavePath = savePath; + SeedingTime = seedingTime; + Seeds = seeds; + SeedsTotal = seedsTotal; + ShareRatio = shareRatio; + TimeElapsed = timeElapsed; + TotalDownloaded = totalDownloaded; + TotalDownloadedSession = totalDownloadedSession; + TotalSize = totalSize; + TotalUploaded = totalUploaded; + TotalUploadedSession = totalUploadedSession; + TotalWasted = totalWasted; + UploadLimit = uploadLimit; + UploadSpeed = uploadSpeed; + UploadSpeedAverage = uploadSpeedAverage; + InfoHashV1 = infoHashV1; + InfoHashV2 = infoHashV2; + } + + [JsonPropertyName("addition_date")] + public long AdditionDate { get; } + + [JsonPropertyName("comment")] + public string Comment { get; } + + [JsonPropertyName("completion_date")] + public long CompletionDate { get; } + + [JsonPropertyName("created_by")] + public string CreatedBy { get; } + + [JsonPropertyName("creation_date")] + public long CreationDate { get; } + + [JsonPropertyName("dl_limit")] + public long DownloadLimit { get; } + + [JsonPropertyName("dl_speed")] + public long DownloadSpeed { get; } + + [JsonPropertyName("dl_speed_avg")] + public long DownloadSpeedAverage { get; } + + [JsonPropertyName("eta")] + public int EstimatedTimeOfArrival { get; } + + [JsonPropertyName("last_seen")] + public long LastSeen { get; } + + [JsonPropertyName("nb_connections")] + public int Connections { get; } + + [JsonPropertyName("nb_connections_limit")] + public int ConnectionsLimit { get; } + + [JsonPropertyName("peers")] + public int Peers { get; } + + [JsonPropertyName("peers_total")] + public int PeersTotal { get; } + + [JsonPropertyName("piece_size")] + public int PieceSize { get; } + + [JsonPropertyName("pieces_have")] + public int PiecesHave { get; } + + [JsonPropertyName("pieces_num")] + public int PiecesNum { get; } + + [JsonPropertyName("reannounce")] + public int Reannounce { get; } + + [JsonPropertyName("save_path")] + public string SavePath { get; } + + [JsonPropertyName("seeding_time")] + public int SeedingTime { get; } + + [JsonPropertyName("seeds")] + public int Seeds { get; } + + [JsonPropertyName("seeds_total")] + public int SeedsTotal { get; } + + [JsonPropertyName("share_ratio")] + public float ShareRatio { get; } + + [JsonPropertyName("time_elapsed")] + public int TimeElapsed { get; } + + [JsonPropertyName("total_downloaded")] + public long TotalDownloaded { get; } + + [JsonPropertyName("total_downloaded_session")] + public long TotalDownloadedSession { get; } + + [JsonPropertyName("total_size")] + public long TotalSize { get; } + + [JsonPropertyName("total_uploaded")] + public long TotalUploaded { get; } + + [JsonPropertyName("total_uploaded_session")] + public long TotalUploadedSession { get; } + + [JsonPropertyName("total_wasted")] + public long TotalWasted { get; } + + [JsonPropertyName("up_limit")] + public long UploadLimit { get; } + + [JsonPropertyName("up_speed")] + public long UploadSpeed { get; } + + [JsonPropertyName("up_speed_avg")] + public long UploadSpeedAverage { get; } + + [JsonPropertyName("infohash_v1")] + public string InfoHashV1 { get; } + + [JsonPropertyName("infohash_v2")] + public string InfoHashV2 { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/TorrentTrackers.cs b/Lantean.QBitTorrentClient/Models/TorrentTrackers.cs new file mode 100644 index 0000000..e065a9a --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/TorrentTrackers.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record TorrentTrackers + { + [JsonConstructor] + public TorrentTrackers( + string url, + TrackerStatus status, + int tier, + int peers, + int seeds, + int leeches, + int downloads, + string message) + { + Url = url; + Status = status; + Tier = tier; + Peers = peers; + Seeds = seeds; + Leeches = leeches; + Downloads = downloads; + Message = message; + } + + [JsonPropertyName("url")] + public string Url { get; } + + [JsonPropertyName("status")] + public TrackerStatus Status { get; } + + [JsonPropertyName("tier")] + public int Tier { get; } + + [JsonPropertyName("num_peers")] + public int Peers { get; } + + [JsonPropertyName("num_seeds")] + public int Seeds { get; } + + [JsonPropertyName("num_leeches")] + public int Leeches { get; } + + [JsonPropertyName("num_downloaded")] + public int Downloads { get; } + + [JsonPropertyName("msg")] + public string Message { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/TrackerStatus.cs b/Lantean.QBitTorrentClient/Models/TrackerStatus.cs new file mode 100644 index 0000000..1e5c634 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/TrackerStatus.cs @@ -0,0 +1,11 @@ +namespace Lantean.QBitTorrentClient.Models +{ + public enum TrackerStatus + { + Disabled = 0, + Uncontacted = 1, + Working = 2, + Updating = 3, + NotWorking = 4, + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/UpdatePreferences.cs b/Lantean.QBitTorrentClient/Models/UpdatePreferences.cs new file mode 100644 index 0000000..b0c1972 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/UpdatePreferences.cs @@ -0,0 +1,613 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record UpdatePreferences + { + [JsonPropertyName("add_to_top_of_queue")] + public bool? AddToTopOfQueue { get; set; } + + [JsonPropertyName("add_trackers")] + public string? AddTrackers { get; set; } + + [JsonPropertyName("add_trackers_enabled")] + public bool? AddTrackersEnabled { get; set; } + + [JsonPropertyName("alt_dl_limit")] + public int? AltDlLimit { get; set; } + + [JsonPropertyName("alt_up_limit")] + public int? AltUpLimit { get; set; } + + [JsonPropertyName("alternative_webui_enabled")] + public bool? AlternativeWebuiEnabled { get; set; } + + [JsonPropertyName("alternative_webui_path")] + public string? AlternativeWebuiPath { get; set; } + + [JsonPropertyName("announce_ip")] + public string? AnnounceIp { get; set; } + + [JsonPropertyName("announce_to_all_tiers")] + public bool? AnnounceToAllTiers { get; set; } + + [JsonPropertyName("announce_to_all_trackers")] + public bool? AnnounceToAllTrackers { get; set; } + + [JsonPropertyName("anonymous_mode")] + public bool? AnonymousMode { get; set; } + + [JsonPropertyName("async_io_threads")] + public int? AsyncIoThreads { get; set; } + + [JsonPropertyName("auto_delete_mode")] + public int? AutoDeleteMode { get; set; } + + [JsonPropertyName("auto_tmm_enabled")] + public bool? AutoTmmEnabled { get; set; } + + [JsonPropertyName("autorun_enabled")] + public bool? AutorunEnabled { get; set; } + + [JsonPropertyName("autorun_on_torrent_added_enabled")] + public bool? AutorunOnTorrentAddedEnabled { get; set; } + + [JsonPropertyName("autorun_on_torrent_added_program")] + public string? AutorunOnTorrentAddedProgram { get; set; } + + [JsonPropertyName("autorun_program")] + public string? AutorunProgram { get; set; } + + [JsonPropertyName("banned_IPs")] + public string? BannedIPs { get; set; } + + [JsonPropertyName("bdecode_depth_limit")] + public int? BdecodeDepthLimit { get; set; } + + [JsonPropertyName("bdecode_token_limit")] + public int? BdecodeTokenLimit { get; set; } + + [JsonPropertyName("bittorrent_protocol")] + public int? BittorrentProtocol { get; set; } + + [JsonPropertyName("block_peers_on_privileged_ports")] + public bool? BlockPeersOnPrivilegedPorts { get; set; } + + [JsonPropertyName("bypass_auth_subnet_whitelist")] + public string? BypassAuthSubnetWhitelist { get; set; } + + [JsonPropertyName("bypass_auth_subnet_whitelist_enabled")] + public bool? BypassAuthSubnetWhitelistEnabled { get; set; } + + [JsonPropertyName("bypass_local_auth")] + public bool? BypassLocalAuth { get; set; } + + [JsonPropertyName("category_changed_tmm_enabled")] + public bool? CategoryChangedTmmEnabled { get; set; } + + [JsonPropertyName("checking_memory_use")] + public int? CheckingMemoryUse { get; set; } + + [JsonPropertyName("connection_speed")] + public int? ConnectionSpeed { get; set; } + + [JsonPropertyName("current_interface_address")] + public string? CurrentInterfaceAddress { get; set; } + + [JsonPropertyName("current_interface_name")] + public string? CurrentInterfaceName { get; set; } + + [JsonPropertyName("current_network_interface")] + public string? CurrentNetworkInterface { get; set; } + + [JsonPropertyName("dht")] + public bool? Dht { get; set; } + + [JsonPropertyName("disk_cache")] + public int? DiskCache { get; set; } + + [JsonPropertyName("disk_cache_ttl")] + public int? DiskCacheTtl { get; set; } + + [JsonPropertyName("disk_io_read_mode")] + public int? DiskIoReadMode { get; set; } + + [JsonPropertyName("disk_io_type")] + public int? DiskIoType { get; set; } + + [JsonPropertyName("disk_io_write_mode")] + public int? DiskIoWriteMode { get; set; } + + [JsonPropertyName("disk_queue_size")] + public int? DiskQueueSize { get; set; } + + [JsonPropertyName("dl_limit")] + public int? DlLimit { get; set; } + + [JsonPropertyName("dont_count_slow_torrents")] + public bool? DontCountSlowTorrents { get; set; } + + [JsonPropertyName("dyndns_domain")] + public string? DyndnsDomain { get; set; } + + [JsonPropertyName("dyndns_enabled")] + public bool? DyndnsEnabled { get; set; } + + [JsonPropertyName("dyndns_password")] + public string? DyndnsPassword { get; set; } + + [JsonPropertyName("dyndns_service")] + public int? DyndnsService { get; set; } + + [JsonPropertyName("dyndns_username")] + public string? DyndnsUsername { get; set; } + + [JsonPropertyName("embedded_tracker_port")] + public int? EmbeddedTrackerPort { get; set; } + + [JsonPropertyName("embedded_tracker_port_forwarding")] + public bool? EmbeddedTrackerPortForwarding { get; set; } + + [JsonPropertyName("enable_coalesce_read_write")] + public bool? EnableCoalesceReadWrite { get; set; } + + [JsonPropertyName("enable_embedded_tracker")] + public bool? EnableEmbeddedTracker { get; set; } + + [JsonPropertyName("enable_multi_connections_from_same_ip")] + public bool? EnableMultiConnectionsFromSameIp { get; set; } + + [JsonPropertyName("enable_piece_extent_affinity")] + public bool? EnablePieceExtentAffinity { get; set; } + + [JsonPropertyName("enable_upload_suggestions")] + public bool? EnableUploadSuggestions { get; set; } + + [JsonPropertyName("encryption")] + public int? Encryption { get; set; } + + [JsonPropertyName("excluded_file_names")] + public string? ExcludedFileNames { get; set; } + + [JsonPropertyName("excluded_file_names_enabled")] + public bool? ExcludedFileNamesEnabled { get; set; } + + [JsonPropertyName("export_dir")] + public string? ExportDir { get; set; } + + [JsonPropertyName("export_dir_fin")] + public string? ExportDirFin { get; set; } + + [JsonPropertyName("file_log_age")] + public int? FileLogAge { get; set; } + + [JsonPropertyName("file_log_age_type")] + public int? FileLogAgeType { get; set; } + + [JsonPropertyName("file_log_backup_enabled")] + public bool? FileLogBackupEnabled { get; set; } + + [JsonPropertyName("file_log_delete_old")] + public bool? FileLogDeleteOld { get; set; } + + [JsonPropertyName("file_log_enabled")] + public bool? FileLogEnabled { get; set; } + + [JsonPropertyName("file_log_max_size")] + public int? FileLogMaxSize { get; set; } + + [JsonPropertyName("file_log_path")] + public string? FileLogPath { get; set; } + + [JsonPropertyName("file_pool_size")] + public int? FilePoolSize { get; set; } + + [JsonPropertyName("hashing_threads")] + public int? HashingThreads { get; set; } + + [JsonPropertyName("i2p_address")] + public string? I2pAddress { get; set; } + + [JsonPropertyName("i2p_enabled")] + public bool? I2pEnabled { get; set; } + + [JsonPropertyName("i2p_inbound_length")] + public int? I2pInboundLength { get; set; } + + [JsonPropertyName("i2p_inbound_quantity")] + public int? I2pInboundQuantity { get; set; } + + [JsonPropertyName("i2p_mixed_mode")] + public bool? I2pMixedMode { get; set; } + + [JsonPropertyName("i2p_outbound_length")] + public int? I2pOutboundLength { get; set; } + + [JsonPropertyName("i2p_outbound_quantity")] + public int? I2pOutboundQuantity { get; set; } + + [JsonPropertyName("i2p_port")] + public int? I2pPort { get; set; } + + [JsonPropertyName("idn_support_enabled")] + public bool? IdnSupportEnabled { get; set; } + + [JsonPropertyName("incomplete_files_ext")] + public bool? IncompleteFilesExt { get; set; } + + [JsonPropertyName("ip_filter_enabled")] + public bool? IpFilterEnabled { get; set; } + + [JsonPropertyName("ip_filter_path")] + public string? IpFilterPath { get; set; } + + [JsonPropertyName("ip_filter_trackers")] + public bool? IpFilterTrackers { get; set; } + + [JsonPropertyName("limit_lan_peers")] + public bool? LimitLanPeers { get; set; } + + [JsonPropertyName("limit_tcp_overhead")] + public bool? LimitTcpOverhead { get; set; } + + [JsonPropertyName("limit_utp_rate")] + public bool? LimitUtpRate { get; set; } + + [JsonPropertyName("listen_port")] + public int? ListenPort { get; set; } + + [JsonPropertyName("locale")] + public string? Locale { get; set; } + + [JsonPropertyName("lsd")] + public bool? Lsd { get; set; } + + [JsonPropertyName("mail_notification_auth_enabled")] + public bool? MailNotificationAuthEnabled { get; set; } + + [JsonPropertyName("mail_notification_email")] + public string? MailNotificationEmail { get; set; } + + [JsonPropertyName("mail_notification_enabled")] + public bool? MailNotificationEnabled { get; set; } + + [JsonPropertyName("mail_notification_password")] + public string? MailNotificationPassword { get; set; } + + [JsonPropertyName("mail_notification_sender")] + public string? MailNotificationSender { get; set; } + + [JsonPropertyName("mail_notification_smtp")] + public string? MailNotificationSmtp { get; set; } + + [JsonPropertyName("mail_notification_ssl_enabled")] + public bool? MailNotificationSslEnabled { get; set; } + + [JsonPropertyName("mail_notification_username")] + public string? MailNotificationUsername { get; set; } + + [JsonPropertyName("max_active_checking_torrents")] + public int? MaxActiveCheckingTorrents { get; set; } + + [JsonPropertyName("max_active_downloads")] + public int? MaxActiveDownloads { get; set; } + + [JsonPropertyName("max_active_torrents")] + public int? MaxActiveTorrents { get; set; } + + [JsonPropertyName("max_active_uploads")] + public int? MaxActiveUploads { get; set; } + + [JsonPropertyName("max_concurrent_http_announces")] + public int? MaxConcurrentHttpAnnounces { get; set; } + + [JsonPropertyName("max_connec")] + public int? MaxConnec { get; set; } + + [JsonPropertyName("max_connec_per_torrent")] + public int? MaxConnecPerTorrent { get; set; } + + [JsonPropertyName("max_inactive_seeding_time")] + public int? MaxInactiveSeedingTime { get; set; } + + [JsonPropertyName("max_inactive_seeding_time_enabled")] + public bool? MaxInactiveSeedingTimeEnabled { get; set; } + + [JsonPropertyName("max_ratio")] + public int? MaxRatio { get; set; } + + [JsonPropertyName("max_ratio_act")] + public int? MaxRatioAct { get; set; } + + [JsonPropertyName("max_ratio_enabled")] + public bool? MaxRatioEnabled { get; set; } + + [JsonPropertyName("max_seeding_time")] + public int? MaxSeedingTime { get; set; } + + [JsonPropertyName("max_seeding_time_enabled")] + public bool? MaxSeedingTimeEnabled { get; set; } + + [JsonPropertyName("max_uploads")] + public int? MaxUploads { get; set; } + + [JsonPropertyName("max_uploads_per_torrent")] + public int? MaxUploadsPerTorrent { get; set; } + + [JsonPropertyName("memory_working_set_limit")] + public int? MemoryWorkingSetLimit { get; set; } + + [JsonPropertyName("merge_trackers")] + public bool? MergeTrackers { get; set; } + + [JsonPropertyName("outgoing_ports_max")] + public int? OutgoingPortsMax { get; set; } + + [JsonPropertyName("outgoing_ports_min")] + public int? OutgoingPortsMin { get; set; } + + [JsonPropertyName("peer_tos")] + public int? PeerTos { get; set; } + + [JsonPropertyName("peer_turnover")] + public int? PeerTurnover { get; set; } + + [JsonPropertyName("peer_turnover_cutoff")] + public int? PeerTurnoverCutoff { get; set; } + + [JsonPropertyName("peer_turnover_interval")] + public int? PeerTurnoverInterval { get; set; } + + [JsonPropertyName("performance_warning")] + public bool? PerformanceWarning { get; set; } + + [JsonPropertyName("pex")] + public bool? Pex { get; set; } + + [JsonPropertyName("preallocate_all")] + public bool? PreallocateAll { get; set; } + + [JsonPropertyName("proxy_auth_enabled")] + public bool? ProxyAuthEnabled { get; set; } + + [JsonPropertyName("proxy_bittorrent")] + public bool? ProxyBittorrent { get; set; } + + [JsonPropertyName("proxy_hostname_lookup")] + public bool? ProxyHostnameLookup { get; set; } + + [JsonPropertyName("proxy_ip")] + public string? ProxyIp { get; set; } + + [JsonPropertyName("proxy_misc")] + public bool? ProxyMisc { get; set; } + + [JsonPropertyName("proxy_password")] + public string? ProxyPassword { get; set; } + + [JsonPropertyName("proxy_peer_connections")] + public bool? ProxyPeerConnections { get; set; } + + [JsonPropertyName("proxy_port")] + public int? ProxyPort { get; set; } + + [JsonPropertyName("proxy_rss")] + public bool? ProxyRss { get; set; } + + [JsonPropertyName("proxy_type")] + public string? ProxyType { get; set; } + + [JsonPropertyName("proxy_username")] + public string? ProxyUsername { get; set; } + + [JsonPropertyName("queueing_enabled")] + public bool? QueueingEnabled { get; set; } + + [JsonPropertyName("random_port")] + public bool? RandomPort { get; set; } + + [JsonPropertyName("reannounce_when_address_changed")] + public bool? ReannounceWhenAddressChanged { get; set; } + + [JsonPropertyName("recheck_completed_torrents")] + public bool? RecheckCompletedTorrents { get; set; } + + [JsonPropertyName("refresh_interval")] + public int? RefreshInterval { get; set; } + + [JsonPropertyName("request_queue_size")] + public int? RequestQueueSize { get; set; } + + [JsonPropertyName("resolve_peer_countries")] + public bool? ResolvePeerCountries { get; set; } + + [JsonPropertyName("resume_data_storage_type")] + public string? ResumeDataStorageType { get; set; } + + [JsonPropertyName("rss_auto_downloading_enabled")] + public bool? RssAutoDownloadingEnabled { get; set; } + + [JsonPropertyName("rss_download_repack_proper_episodes")] + public bool? RssDownloadRepackProperEpisodes { get; set; } + + [JsonPropertyName("rss_max_articles_per_feed")] + public int? RssMaxArticlesPerFeed { get; set; } + + [JsonPropertyName("rss_processing_enabled")] + public bool? RssProcessingEnabled { get; set; } + + [JsonPropertyName("rss_refresh_interval")] + public int? RssRefreshInterval { get; set; } + + [JsonPropertyName("rss_smart_episode_filters")] + public string? RssSmartEpisodeFilters { get; set; } + + [JsonPropertyName("save_path")] + public string? SavePath { get; set; } + + [JsonPropertyName("save_path_changed_tmm_enabled")] + public bool? SavePathChangedTmmEnabled { get; set; } + + [JsonPropertyName("save_resume_data_interval")] + public int? SaveResumeDataInterval { get; set; } + + [JsonPropertyName("scan_dirs")] + public Dictionary? ScanDirs { get; set; } + + [JsonPropertyName("schedule_from_hour")] + public int? ScheduleFromHour { get; set; } + + [JsonPropertyName("schedule_from_min")] + public int? ScheduleFromMin { get; set; } + + [JsonPropertyName("schedule_to_hour")] + public int? ScheduleToHour { get; set; } + + [JsonPropertyName("schedule_to_min")] + public int? ScheduleToMin { get; set; } + + [JsonPropertyName("scheduler_days")] + public int? SchedulerDays { get; set; } + + [JsonPropertyName("scheduler_enabled")] + public bool? SchedulerEnabled { get; set; } + + [JsonPropertyName("send_buffer_low_watermark")] + public int? SendBufferLowWatermark { get; set; } + + [JsonPropertyName("send_buffer_watermark")] + public int? SendBufferWatermark { get; set; } + + [JsonPropertyName("send_buffer_watermark_factor")] + public int? SendBufferWatermarkFactor { get; set; } + + [JsonPropertyName("slow_torrent_dl_rate_threshold")] + public int? SlowTorrentDlRateThreshold { get; set; } + + [JsonPropertyName("slow_torrent_inactive_timer")] + public int? SlowTorrentInactiveTimer { get; set; } + + [JsonPropertyName("slow_torrent_ul_rate_threshold")] + public int? SlowTorrentUlRateThreshold { get; set; } + + [JsonPropertyName("socket_backlog_size")] + public int? SocketBacklogSize { get; set; } + + [JsonPropertyName("socket_receive_buffer_size")] + public int? SocketReceiveBufferSize { get; set; } + + [JsonPropertyName("socket_send_buffer_size")] + public int? SocketSendBufferSize { get; set; } + + [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; } + + [JsonPropertyName("temp_path")] + public string? TempPath { get; set; } + + [JsonPropertyName("temp_path_enabled")] + public bool? TempPathEnabled { get; set; } + + [JsonPropertyName("torrent_changed_tmm_enabled")] + public bool? TorrentChangedTmmEnabled { get; set; } + + [JsonPropertyName("torrent_content_layout")] + public string? TorrentContentLayout { get; set; } + + [JsonPropertyName("torrent_file_size_limit")] + public int? TorrentFileSizeLimit { get; set; } + + [JsonPropertyName("torrent_stop_condition")] + public string? TorrentStopCondition { get; set; } + + [JsonPropertyName("up_limit")] + public int? UpLimit { get; set; } + + [JsonPropertyName("upload_choking_algorithm")] + public int? UploadChokingAlgorithm { get; set; } + + [JsonPropertyName("upload_slots_behavior")] + public int? UploadSlotsBehavior { get; set; } + + [JsonPropertyName("upnp")] + public bool? Upnp { get; set; } + + [JsonPropertyName("upnp_lease_duration")] + public int? UpnpLeaseDuration { get; set; } + + [JsonPropertyName("use_category_paths_in_manual_mode")] + public bool? UseCategoryPathsInManualMode { get; set; } + + [JsonPropertyName("use_https")] + public bool? UseHttps { get; set; } + + [JsonPropertyName("use_subcategories")] + public bool? UseSubcategories { get; set; } + + [JsonPropertyName("utp_tcp_mixed_mode")] + public int? UtpTcpMixedMode { get; set; } + + [JsonPropertyName("validate_https_tracker_certificate")] + public bool? ValidateHttpsTrackerCertificate { get; set; } + + [JsonPropertyName("web_ui_address")] + public string? WebUiAddress { get; set; } + + [JsonPropertyName("web_ui_ban_duration")] + public int? WebUiBanDuration { get; set; } + + [JsonPropertyName("web_ui_clickjacking_protection_enabled")] + public bool? WebUiClickjackingProtectionEnabled { get; set; } + + [JsonPropertyName("web_ui_csrf_protection_enabled")] + public bool? WebUiCsrfProtectionEnabled { get; set; } + + [JsonPropertyName("web_ui_custom_http_headers")] + public string? WebUiCustomHttpHeaders { get; set; } + + [JsonPropertyName("web_ui_domain_list")] + public string? WebUiDomainList { get; set; } + + [JsonPropertyName("web_ui_host_header_validation_enabled")] + public bool? WebUiHostHeaderValidationEnabled { get; set; } + + [JsonPropertyName("web_ui_https_cert_path")] + public string? WebUiHttpsCertPath { get; set; } + + [JsonPropertyName("web_ui_https_key_path")] + public string? WebUiHttpsKeyPath { get; set; } + + [JsonPropertyName("web_ui_max_auth_fail_count")] + public int? WebUiMaxAuthFailCount { get; set; } + + [JsonPropertyName("web_ui_port")] + public int? WebUiPort { get; set; } + + [JsonPropertyName("web_ui_reverse_proxies_list")] + public string? WebUiReverseProxiesList { get; set; } + + [JsonPropertyName("web_ui_reverse_proxy_enabled")] + public bool? WebUiReverseProxyEnabled { get; set; } + + [JsonPropertyName("web_ui_secure_cookie_enabled")] + public bool? WebUiSecureCookieEnabled { get; set; } + + [JsonPropertyName("web_ui_session_timeout")] + public int? WebUiSessionTimeout { get; set; } + + [JsonPropertyName("web_ui_upnp")] + public bool? WebUiUpnp { get; set; } + + [JsonPropertyName("web_ui_use_custom_http_headers_enabled")] + public bool? WebUiUseCustomHttpHeadersEnabled { get; set; } + + [JsonPropertyName("web_ui_username")] + public string? WebUiUsername { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/WebSeed.cs b/Lantean.QBitTorrentClient/Models/WebSeed.cs new file mode 100644 index 0000000..5fd2d9b --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/WebSeed.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record WebSeed + { + [JsonConstructor] + public WebSeed(string url) + { + Url = url; + } + + [JsonPropertyName("url")] + public string Url { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/MultipartFormDataContentExtensions.cs b/Lantean.QBitTorrentClient/MultipartFormDataContentExtensions.cs new file mode 100644 index 0000000..71eee18 --- /dev/null +++ b/Lantean.QBitTorrentClient/MultipartFormDataContentExtensions.cs @@ -0,0 +1,35 @@ +namespace Lantean.QBitTorrentClient +{ + public static class MultipartFormDataContentExtensions + { + public static void AddString(this MultipartFormDataContent content, string name, string value) + { + content.Add(new StringContent(value), name); + } + + public static void AddString(this MultipartFormDataContent content, string name, bool value) + { + content.AddString(name, value ? "true" : "false"); + } + + public static void AddString(this MultipartFormDataContent content, string name, int value) + { + content.AddString(name, value.ToString()); + } + + public static void AddString(this MultipartFormDataContent content, string name, long value) + { + content.AddString(name, value.ToString()); + } + + public static void AddString(this MultipartFormDataContent content, string name, float value) + { + content.AddString(name, value.ToString()); + } + + public static void AddString(this MultipartFormDataContent content, string name, DateTimeOffset value, bool useSeconds = true) + { + content.AddString(name, useSeconds ? value.ToUnixTimeSeconds() : value.ToUnixTimeMilliseconds()); + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/QueryBuilder.cs b/Lantean.QBitTorrentClient/QueryBuilder.cs new file mode 100644 index 0000000..09e0df4 --- /dev/null +++ b/Lantean.QBitTorrentClient/QueryBuilder.cs @@ -0,0 +1,67 @@ +using System.Text; + +namespace Lantean.QBitTorrentClient +{ + public class QueryBuilder + { + private readonly IList> _parameters; + + public QueryBuilder() + { + _parameters = []; + } + + public QueryBuilder(IList> parameters) + { + _parameters = parameters; + } + + public QueryBuilder Add(string key, string value) + { + _parameters.Add(new KeyValuePair(key, value)); + return this; + } + + public QueryBuilder AddIfNotNullOrEmpty(string key, string value) + { + if (!string.IsNullOrEmpty(value)) + { + _parameters.Add(new KeyValuePair(key, value)); + } + + return this; + } + + public string ToQueryString() + { + if (_parameters.Count == 0) + { + return string.Empty; + } + + var queryString = new StringBuilder(); + for (int i = 0; i < _parameters.Count; i++) + { + var kvp = _parameters[i]; + if (i == 0) + { + queryString.Append('?'); + } + else + { + queryString.Append('&'); + } + queryString.Append(Uri.EscapeDataString(kvp.Key)); + queryString.Append('='); + queryString.Append(Uri.EscapeDataString(kvp.Value)); + } + + return queryString.ToString(); + } + + public override string ToString() + { + return ToQueryString(); + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/QueryBuilderExtensions.cs b/Lantean.QBitTorrentClient/QueryBuilderExtensions.cs new file mode 100644 index 0000000..25a4530 --- /dev/null +++ b/Lantean.QBitTorrentClient/QueryBuilderExtensions.cs @@ -0,0 +1,40 @@ +namespace Lantean.QBitTorrentClient +{ + public static class QueryBuilderExtensions + { + public static QueryBuilder Add(this QueryBuilder builder, string key, bool value) + { + return builder.Add(key, value ? "true" : "false"); + } + + public static QueryBuilder Add(this QueryBuilder builder, string key, int value) + { + return builder.Add(key, value.ToString()); + } + + public static QueryBuilder Add(this QueryBuilder builder, string key, long value) + { + return builder.Add(key, value.ToString()); + } + + public static QueryBuilder Add(this QueryBuilder builder, string key, DateTimeOffset value, bool useSeconds = true) + { + return builder.Add(key, useSeconds ? value.ToUnixTimeSeconds() : value.ToUnixTimeMilliseconds()); + } + + public static QueryBuilder Add(this QueryBuilder builder, string key, Enum value) + { + return builder.Add(key, value.ToString()); + } + + public static QueryBuilder AddPipeSeparated(this QueryBuilder builder, string key, IEnumerable values) + { + return builder.Add(key, string.Join('|', values)); + } + + public static QueryBuilder AddCommaSeparated(this QueryBuilder builder, string key, IEnumerable values) + { + return builder.Add(key, string.Join(',', values)); + } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/SerializerOptions.cs b/Lantean.QBitTorrentClient/SerializerOptions.cs new file mode 100644 index 0000000..dba76ae --- /dev/null +++ b/Lantean.QBitTorrentClient/SerializerOptions.cs @@ -0,0 +1,19 @@ +using Lantean.QBitTorrentClient.Converters; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient +{ + public static class SerializerOptions + { + public static JsonSerializerOptions Options { get; } + + static SerializerOptions() + { + Options = new JsonSerializerOptions(); + Options.Converters.Add(new StringFloatJsonConverter()); + Options.Converters.Add(new SaveLocationJsonConverter()); + Options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + } + } +} \ No newline at end of file