diff --git a/PiwigoDotnet/Piwigo.Client.Tests/CategoryApiTests.cs b/PiwigoDotnet/Piwigo.Client.Tests/CategoryApiTests.cs index f30aad7..fdf1d4c 100644 --- a/PiwigoDotnet/Piwigo.Client.Tests/CategoryApiTests.cs +++ b/PiwigoDotnet/Piwigo.Client.Tests/CategoryApiTests.cs @@ -33,7 +33,7 @@ public class CategoryApiTests : ApiTestsBase } }; SetJsonResult(serverResponse); - var response = await _categoryApi.GetAllCategoriesAsync(); + var response = await _categoryApi.GetAllAsync(); response.Should().HaveCount(3); response.Should().SatisfyRespectively(c => diff --git a/PiwigoDotnet/Piwigo.Client/CategoryApi.cs b/PiwigoDotnet/Piwigo.Client/CategoryApi.cs index 3d835b2..4f88462 100644 --- a/PiwigoDotnet/Piwigo.Client/CategoryApi.cs +++ b/PiwigoDotnet/Piwigo.Client/CategoryApi.cs @@ -1,10 +1,32 @@ using System.Collections.ObjectModel; -using Flurl.Http; using Microsoft.Extensions.Logging; using Piwigo.Client.Contract; namespace Piwigo.Client; +internal static class DictionaryExtensions +{ + public static IDictionary AddIfValueNotNull(this IDictionary dictionary, string key, string? value) + { + if (dictionary == null) + { + throw new ArgumentNullException(nameof(dictionary)); + } + + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(key)); + } + + if (value is not null) + { + dictionary.Add(key, value); + } + + return dictionary; + } +} + public class CategoryApi : ICategoryApi { private readonly IPiwigoContext _context; @@ -16,11 +38,43 @@ public class CategoryApi : ICategoryApi _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public async Task> GetAllCategoriesAsync() + public async Task AddAsync(string name, int? parentId = null, string? comment = null, bool? visible = null, CategoryStatus? status = null, bool? commentable = null, + CategoryPosition? position = null) + { + var statusValue = status switch + { + CategoryStatus.Public => "public", + CategoryStatus.Private => "private", + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(status), status, null) + }; + + var positionValue = position switch + { + CategoryPosition.First => "first", + CategoryPosition.Last => "last", + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(position), position, null) + }; + + var formParams = new Dictionary { { "name", name } }; + formParams.AddIfValueNotNull("parent", parentId?.ToString()).AddIfValueNotNull("comment", comment).AddIfValueNotNull("visible", visible?.ToString()) + .AddIfValueNotNull("status", statusValue).AddIfValueNotNull("commentable", visible?.ToString()).AddIfValueNotNull("position", positionValue); + + var response = await _context.PostAsync>(_logger, "pwg.categories.add", formParams); + if (!response.Result.Id.HasValue) + { + throw new PiwigoException($"Could not create album {name}: {response.Result.Info}"); + } + + return response.Result.Id.Value; + } + + public async Task> GetAllAsync() { _logger.LogInformation("Getting all existing categories from server"); - var response = await _context.ConfigureRequest(_logger).PostMultipartAsync(c => c.AddMethod("pwg.categories.getList").AddString("recursive", "true")); - var typedResponse = await response.GetJsonAsync>(); - return new ReadOnlyCollection(typedResponse.Result.Categories); + var formParams = new Dictionary { { "recursive", "true" } }; + var response = await _context.PostAsync>(_logger, "pwg.categories.getList", formParams); + return new ReadOnlyCollection(response.Result.Categories); } } \ No newline at end of file diff --git a/PiwigoDotnet/Piwigo.Client/CategoryPosition.cs b/PiwigoDotnet/Piwigo.Client/CategoryPosition.cs new file mode 100644 index 0000000..ace4bee --- /dev/null +++ b/PiwigoDotnet/Piwigo.Client/CategoryPosition.cs @@ -0,0 +1,7 @@ +namespace Piwigo.Client; + +public enum CategoryPosition +{ + First = 0, + Last = 1 +} \ No newline at end of file diff --git a/PiwigoDotnet/Piwigo.Client/CategoryStatus.cs b/PiwigoDotnet/Piwigo.Client/CategoryStatus.cs new file mode 100644 index 0000000..b719acf --- /dev/null +++ b/PiwigoDotnet/Piwigo.Client/CategoryStatus.cs @@ -0,0 +1,7 @@ +namespace Piwigo.Client; + +public enum CategoryStatus +{ + Public = 0, + Private = 1 +} \ No newline at end of file diff --git a/PiwigoDotnet/Piwigo.Client/Contract/AlbumAdded.cs b/PiwigoDotnet/Piwigo.Client/Contract/AlbumAdded.cs new file mode 100644 index 0000000..c3328e0 --- /dev/null +++ b/PiwigoDotnet/Piwigo.Client/Contract/AlbumAdded.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Piwigo.Client.Contract; + +internal class AlbumAdded +{ + [JsonProperty("info")] + public string? Info { get; init; } + + [JsonProperty("id")] + public int? Id { get; init; } +} \ No newline at end of file diff --git a/PiwigoDotnet/Piwigo.Client/Contract/PiwigoResponse.cs b/PiwigoDotnet/Piwigo.Client/Contract/PiwigoResponse.cs index a28ebce..772938c 100644 --- a/PiwigoDotnet/Piwigo.Client/Contract/PiwigoResponse.cs +++ b/PiwigoDotnet/Piwigo.Client/Contract/PiwigoResponse.cs @@ -7,6 +7,12 @@ internal class PiwigoResponse [JsonProperty("stat")] public string? Status { get; init; } + [JsonProperty("err")] + public int? Error { get; init; } + + [JsonProperty("message")] + public string? Message { get; init; } + [JsonProperty("result")] public T Result { get; init; } = default!; } \ No newline at end of file diff --git a/PiwigoDotnet/Piwigo.Client/ICategoryApi.cs b/PiwigoDotnet/Piwigo.Client/ICategoryApi.cs index 8f3ef97..a6a0b42 100644 --- a/PiwigoDotnet/Piwigo.Client/ICategoryApi.cs +++ b/PiwigoDotnet/Piwigo.Client/ICategoryApi.cs @@ -4,5 +4,8 @@ namespace Piwigo.Client; public interface ICategoryApi { - Task> GetAllCategoriesAsync(); + Task AddAsync(string name, int? parentId = null, string? comment = null, bool? visible = null, CategoryStatus? status = null, bool? commentable = null, + CategoryPosition? position = null); + + Task> GetAllAsync(); } \ No newline at end of file diff --git a/PiwigoDotnet/Piwigo.Client/IPiwigoContext.cs b/PiwigoDotnet/Piwigo.Client/IPiwigoContext.cs index 8ecf978..3ea7478 100644 --- a/PiwigoDotnet/Piwigo.Client/IPiwigoContext.cs +++ b/PiwigoDotnet/Piwigo.Client/IPiwigoContext.cs @@ -1,13 +1,12 @@ -using Flurl.Http; using Microsoft.Extensions.Logging; namespace Piwigo.Client; public interface IPiwigoContext { - IPiwigoConfiguration Config { get; } bool IsLoggedIn { get; } - IFlurlRequest ConfigureRequest(ILogger logger, bool requireLogin = true); - void LoggedOut(); - void LoggedIn(); + Task LoginAsync(); + Task LogoutAsync(); + Task PostAsync(ILogger logger, string method, IDictionary formParams); + Task PostAsync(ILogger logger, string method); } \ No newline at end of file diff --git a/PiwigoDotnet/Piwigo.Client/PiwigoContext.cs b/PiwigoDotnet/Piwigo.Client/PiwigoContext.cs index b52ba9d..84d057d 100644 --- a/PiwigoDotnet/Piwigo.Client/PiwigoContext.cs +++ b/PiwigoDotnet/Piwigo.Client/PiwigoContext.cs @@ -1,49 +1,126 @@ +using System.Net; using Flurl.Http; +using Flurl.Http.Content; using Microsoft.Extensions.Logging; namespace Piwigo.Client; public class PiwigoContext : IPiwigoContext { + private readonly IPiwigoConfiguration _config; private readonly CookieJar _cookies = new(); private readonly ILogger _logger; public PiwigoContext(IPiwigoConfiguration configuration, ILogger logger) { - Config = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _config = configuration ?? throw new ArgumentNullException(nameof(configuration)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public IPiwigoConfiguration Config { get; } - public bool IsLoggedIn { get; private set; } - public IFlurlRequest ConfigureRequest(ILogger logger, bool requireLogin = true) + public async Task LoginAsync() + { + var userName = _config.UserName; + if (string.IsNullOrWhiteSpace(userName)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(userName)); + } + + var password = _config.Password; + if (string.IsNullOrWhiteSpace(password)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(password)); + } + + if (IsLoggedIn) + { + throw new PiwigoException("The client is already logged in. Create a new instance or log out first!"); + } + + _logger.LogInformation("Logging into {PiwigoBaseUri} using username {Username}", _config.BaseUri, userName); + + var response = await ConfigureRequest(_logger).PostMultipartAsync(c => c.AddMethod("pwg.session.login").AddString("username", userName).AddString("password", password)); + + if (response.StatusCode != (int)HttpStatusCode.OK) + { + _logger.LogError("Failed to log in {StatusCode}", response.StatusCode); + throw new PiwigoException($"Could not log in to {_config.BaseUri} using username {userName}"); + } + + _logger.LogInformation("Logging in succeeded"); + _logger.LogInformation("logged in"); + IsLoggedIn = true; + } + + public async Task LogoutAsync() + { + if (!IsLoggedIn) + { + _logger.LogWarning("Tried to log out from {Uri} but was not logged in!", _config.BaseUri); + return; + } + + _logger.LogInformation("Logging out from {Uri}", _config.BaseUri); + await ConfigureRequest(_logger).PostMultipartAsync(c => c.AddMethod("pwg.session.logout")); + _logger.LogInformation("logged out, clearing cookies"); + IsLoggedIn = false; + _cookies.Clear(); + } + + public Task PostAsync(ILogger logger, string method) + { + return PostInternalAsync(logger, method, null); + } + + public Task PostAsync(ILogger logger, string method, IDictionary formParams) + { + return PostInternalAsync(logger, method, formParams); + } + + private static void AddFormParams(CapturedMultipartContent c, IDictionary formParams) + { + foreach (var formParam in formParams) + { + c.AddString(formParam.Key, formParam.Value); + } + } + + private async Task PostInternalAsync(ILogger logger, string method, IDictionary? formParams) + { + await EnsureLoggedInAsync(); + + var response = await ConfigureRequest(logger).PostMultipartAsync(c => + { + c.AddMethod(method); + if (formParams != null) + { + AddFormParams(c, formParams); + } + }); + + var typedResponse = await response.GetJsonAsync(); + return typedResponse; + } + + private async ValueTask EnsureLoggedInAsync() + { + if (IsLoggedIn) + { + return; + } + + await LoginAsync(); + } + + private IFlurlRequest ConfigureRequest(ILogger logger) { if (logger == null) { throw new ArgumentNullException(nameof(logger)); } - if (requireLogin && !IsLoggedIn) - { - throw new InvalidOperationException("User is not logged in. Ensure login is called before accessing any piwigo methods"); - } - - return Config.BaseUri.WithCookies(_cookies).ConfigureRequest(r => r.AfterCallAsync = call => LogResponse(call, logger)); - } - - public void LoggedOut() - { - _logger.LogInformation("logged out, clearing cookies"); - IsLoggedIn = false; - _cookies.Clear(); - } - - public void LoggedIn() - { - _logger.LogInformation("logged in"); - IsLoggedIn = true; + return _config.BaseUri.WithCookies(_cookies).ConfigureRequest(r => r.AfterCallAsync = call => LogResponse(call, logger)); } private static async Task LogResponse(FlurlCall call, ILogger logger) diff --git a/PiwigoDotnet/Piwigo.Client/SessionApi.cs b/PiwigoDotnet/Piwigo.Client/SessionApi.cs index e98a03b..1da2683 100644 --- a/PiwigoDotnet/Piwigo.Client/SessionApi.cs +++ b/PiwigoDotnet/Piwigo.Client/SessionApi.cs @@ -1,5 +1,3 @@ -using System.Net; -using Flurl.Http; using Microsoft.Extensions.Logging; using Piwigo.Client.Contract; @@ -16,52 +14,20 @@ internal class SessionApi : ISessionApi _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public async Task LogoutAsync() + public Task LogoutAsync() { - _logger.LogInformation("Logging out from {Uri}", _context.Config.BaseUri); - await _context.ConfigureRequest(_logger).PostMultipartAsync(c => c.AddMethod("pwg.session.logout")); - _context.LoggedOut(); + return _context.LogoutAsync(); } - public async Task LoginAsync() + public Task LoginAsync() { - var userName = _context.Config.UserName; - if (string.IsNullOrWhiteSpace(userName)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(userName)); - } - - var password = _context.Config.Password; - if (string.IsNullOrWhiteSpace(password)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(password)); - } - - if (_context.IsLoggedIn) - { - throw new PiwigoException("The client is already logged in. Create a new instance or log out first!"); - } - - _logger.LogInformation("Logging into {PiwigoBaseUri} using username {Username}", _context.Config.BaseUri, userName); - - var response = await _context.ConfigureRequest(_logger, false).PostMultipartAsync(c => - c.AddMethod("pwg.session.login").AddString("username", userName).AddString("password", password)); - - if (response.StatusCode != (int)HttpStatusCode.OK) - { - _logger.LogError("Failed to log in {StatusCode}", response.StatusCode); - throw new PiwigoException($"Could not log in to {_context.Config.BaseUri} using username {userName}"); - } - - _logger.LogInformation("Logging in succeeded"); - _context.LoggedIn(); + return _context.LoginAsync(); } public async Task GetStatusAsync() { _logger.LogInformation("Getting status"); - var response = await _context.ConfigureRequest(_logger).PostMultipartAsync(c => c.AddMethod("pwg.session.getStatus")); - var typedResponse = await response.GetJsonAsync>(); + var typedResponse = await _context.PostAsync>(_logger, "pwg.session.getStatus"); return typedResponse.Result; } } \ No newline at end of file