From 4b31207f9180c6f9ca27f8dd764d58551b17cf1d Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Tue, 25 Aug 2020 10:34:55 -0400 Subject: [PATCH] Make AccountsSettings and Persister more clear --- FileLiberator/UNTESTED/DecryptBook.cs | 2 +- InternalUtilities/Account.cs | 95 ++++++++++++++++ .../{Accounts.cs => AccountsSettings.cs} | 106 ++---------------- .../AccountsSettingsPersister.cs | 24 ++++ .../UNTESTED/AudibleApiStorage.cs | 4 +- LibationLauncher/LibationLauncher.csproj | 2 +- LibationLauncher/UNTESTED/Program.cs | 2 +- .../UNTESTED/Dialogs/AccountsDialog.cs | 52 +++++++-- .../InternalUtilities.Tests/AccountTests.cs | 2 +- 9 files changed, 175 insertions(+), 114 deletions(-) create mode 100644 InternalUtilities/Account.cs rename InternalUtilities/{Accounts.cs => AccountsSettings.cs} (55%) create mode 100644 InternalUtilities/AccountsSettingsPersister.cs diff --git a/FileLiberator/UNTESTED/DecryptBook.cs b/FileLiberator/UNTESTED/DecryptBook.cs index e814b72f..dbc56796 100644 --- a/FileLiberator/UNTESTED/DecryptBook.cs +++ b/FileLiberator/UNTESTED/DecryptBook.cs @@ -59,7 +59,7 @@ namespace FileLiberator var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b"); var account = AudibleApiStorage - .GetAccountsSettings() + .GetPersistentAccountsSettings() .GetAccount(libraryBook.Account, libraryBook.Book.Locale); var outputAudioFilename = await aaxToM4bConverterDecrypt(proposedOutputFile, aaxFilename, account); diff --git a/InternalUtilities/Account.cs b/InternalUtilities/Account.cs new file mode 100644 index 00000000..c42cf989 --- /dev/null +++ b/InternalUtilities/Account.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AudibleApi; +using AudibleApi.Authorization; +using Dinah.Core; +using Newtonsoft.Json; + +namespace InternalUtilities +{ + public class Account : IUpdatable + { + public event EventHandler Updated; + private void update(object sender = null, EventArgs e = null) + => Updated?.Invoke(this, new EventArgs()); + + // canonical. immutable. email or phone number + public string AccountId { get; } + + // user-friendly, non-canonical name. mutable + private string _accountName; + public string AccountName + { + get => _accountName; + set + { + if (string.IsNullOrWhiteSpace(value)) + return; + var v = value.Trim(); + if (v == _accountName) + return; + _accountName = v; + update(); + } + } + + // whether to include this account when scanning libraries. + // technically this is an app setting; not an attribute of account. but since it's managed with accounts, it makes sense to put this exception-to-the-rule here + private bool _libraryScan = true; + public bool LibraryScan + { + get => _libraryScan; + set + { + if (value == _libraryScan) + return; + _libraryScan = value; + update(); + } + } + + private string _decryptKey = ""; + public string DecryptKey + { + get => _decryptKey; + set + { + var v = (value ?? "").Trim(); + if (v == _decryptKey) + return; + _decryptKey = v; + update(); + } + } + + private Identity _identity; + public Identity IdentityTokens + { + get => _identity; + set + { + if (_identity is null && value is null) + return; + + if (_identity != null) + _identity.Updated -= update; + + if (value != null) + value.Updated += update; + + _identity = value; + update(); + } + } + + [JsonIgnore] + public Locale Locale => IdentityTokens?.Locale; + + public Account(string accountId) + { + ArgumentValidator.EnsureNotNullOrWhiteSpace(accountId, nameof(accountId)); + AccountId = accountId.Trim(); + } + } +} diff --git a/InternalUtilities/Accounts.cs b/InternalUtilities/AccountsSettings.cs similarity index 55% rename from InternalUtilities/Accounts.cs rename to InternalUtilities/AccountsSettings.cs index 4c4ff232..29d8bac6 100644 --- a/InternalUtilities/Accounts.cs +++ b/InternalUtilities/AccountsSettings.cs @@ -1,32 +1,14 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using AudibleApi; using AudibleApi.Authorization; using Dinah.Core; -using Dinah.Core.IO; using Newtonsoft.Json; namespace InternalUtilities { - public class AccountsSettingsPersister : JsonFilePersister - { - /// Alias for Target - public AccountsSettings AccountsSettings => Target; - - /// uses path. create file if doesn't yet exist - public AccountsSettingsPersister(AccountsSettings target, string path, string jsonPath = null) - : base(target, path, jsonPath) { } - - /// load from existing file - public AccountsSettingsPersister(string path, string jsonPath = null) - : base(path, jsonPath) { } - - protected override JsonSerializerSettings GetSerializerSettings() - => Identity.GetJsonSerializerSettings(); - } - // 'AccountsSettings' is intentionally not IEnumerable<> so that properties can be added/extended + // 'AccountsSettings' is intentionally NOT IEnumerable<> so that properties can be added/extended // from newtonsoft (https://www.newtonsoft.com/json/help/html/SerializationGuide.htm): // .NET : IList, IEnumerable, IList, Array // JSON : Array (properties on the collection will not be serialized) @@ -49,7 +31,7 @@ namespace InternalUtilities #region Accounts private List _accounts_backing = new List(); - [JsonProperty(PropertyName = "Accounts")] + [JsonProperty(PropertyName = nameof(Accounts))] private List _accounts_json { get => _accounts_backing; @@ -141,84 +123,16 @@ namespace InternalUtilities var acct = GetAccount(accountId, locale); - if (acct is null || account is null) + // new: ok + if (acct is null) return; - if (acct != account) - throw new InvalidOperationException("Cannot add an account with the same account Id and Locale"); - } - } - public class Account : IUpdatable - { - public event EventHandler Updated; - private void update(object sender = null, EventArgs e = null) - => Updated?.Invoke(this, new EventArgs()); - - // canonical. immutable. email or phone number - public string AccountId { get; } - - // user-friendly, non-canonical name. mutable - private string _accountName; - public string AccountName - { - get => _accountName; - set - { - if (string.IsNullOrWhiteSpace(value)) - return; - var v = value.Trim(); - if (v == _accountName) - return; - _accountName = v; - update(); - } - } - - // whether to include this account when scanning libraries. - // technically this is an app setting; not an attribute of account. but since it's managed with accounts, it makes sense to put this exception-to-the-rule here - public bool LibraryScan { get; set; } - - private string _decryptKey = ""; - public string DecryptKey - { - get => _decryptKey; - set - { - var v = (value ?? "").Trim(); - if (v == _decryptKey) - return; - _decryptKey = v; - update(); - } - } - - private Identity _identity; - public Identity IdentityTokens - { - get => _identity; - set - { - if (_identity is null && value is null) - return; - - if (_identity != null) - _identity.Updated -= update; - - if (value != null) - value.Updated += update; - - _identity = value; - update(); - } - } - - [JsonIgnore] - public Locale Locale => IdentityTokens?.Locale; - - public Account(string accountId) - { - ArgumentValidator.EnsureNotNullOrWhiteSpace(accountId, nameof(accountId)); - AccountId = accountId.Trim(); + // same account instance: ok + if (acct == account) + return; + + // same account id + locale, different instance: bad + throw new InvalidOperationException("Cannot add an account with the same account Id and Locale"); } } } diff --git a/InternalUtilities/AccountsSettingsPersister.cs b/InternalUtilities/AccountsSettingsPersister.cs new file mode 100644 index 00000000..1368b614 --- /dev/null +++ b/InternalUtilities/AccountsSettingsPersister.cs @@ -0,0 +1,24 @@ +using System; +using AudibleApi.Authorization; +using Dinah.Core.IO; +using Newtonsoft.Json; + +namespace InternalUtilities +{ + public class AccountsSettingsPersister : JsonFilePersister + { + /// Alias for Target + public AccountsSettings AccountsSettings => Target; + + /// uses path. create file if doesn't yet exist + public AccountsSettingsPersister(AccountsSettings target, string path, string jsonPath = null) + : base(target, path, jsonPath) { } + + /// load from existing file + public AccountsSettingsPersister(string path, string jsonPath = null) + : base(path, jsonPath) { } + + protected override JsonSerializerSettings GetSerializerSettings() + => Identity.GetJsonSerializerSettings(); + } +} diff --git a/InternalUtilities/UNTESTED/AudibleApiStorage.cs b/InternalUtilities/UNTESTED/AudibleApiStorage.cs index e878fbd5..ffd68e62 100644 --- a/InternalUtilities/UNTESTED/AudibleApiStorage.cs +++ b/InternalUtilities/UNTESTED/AudibleApiStorage.cs @@ -24,9 +24,9 @@ namespace InternalUtilities => TEST_GetFirstAccount().GetIdentityTokensJsonPath(); // convenience for for tests and demos. don't use in production Libation public static Account TEST_GetFirstAccount() - => GetAccountsSettings().GetAll().FirstOrDefault(); + => GetPersistentAccountsSettings().GetAll().FirstOrDefault(); - public static AccountsSettings GetAccountsSettings() + public static AccountsSettings GetPersistentAccountsSettings() => new AccountsSettingsPersister(AccountsSettingsFile).AccountsSettings; public static string GetIdentityTokensJsonPath(this Account account) diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index 63ad01aa..af452c58 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -13,7 +13,7 @@ win-x64 - 3.1.12.255 + 3.1.12.260 diff --git a/LibationLauncher/UNTESTED/Program.cs b/LibationLauncher/UNTESTED/Program.cs index 3ee2fd74..3b133295 100644 --- a/LibationLauncher/UNTESTED/Program.cs +++ b/LibationLauncher/UNTESTED/Program.cs @@ -162,7 +162,7 @@ namespace LibationLauncher }; // saves to new file - AudibleApiStorage.GetAccountsSettings().Add(account); + AudibleApiStorage.GetPersistentAccountsSettings().Add(account); return account; } diff --git a/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.cs b/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.cs index c436f955..4b617e2a 100644 --- a/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.cs +++ b/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.cs @@ -28,6 +28,12 @@ namespace LibationWinForms.Dialogs populateGridValues(); } + struct OriginalValue + { + public string AccountId { get; set; } + public string LocaleName { get; set; } + } + private void populateDropDown() => (dataGridView1.Columns[COL_Locale] as DataGridViewComboBoxColumn).DataSource = Localization.Locales @@ -36,18 +42,16 @@ namespace LibationWinForms.Dialogs private void populateGridValues() { - // WARNING - // behind the scenes this returns a AccountsSettings - // accounts persister will write ANY EDIT to object immediately to file + // WARNING: accounts persister will write ANY EDIT to object immediately to file // here: copy strings // only persist in 'save' step - var accounts = AudibleApiStorage.GetAccountsSettings().Accounts; + var accounts = AudibleApiStorage.GetPersistentAccountsSettings().Accounts; if (!accounts.Any()) return; foreach (var account in accounts) dataGridView1.Rows.Add( - new { account.AccountId, account.Locale.Name }, + new OriginalValue { AccountId = account.AccountId, LocaleName = account.Locale.Name }, "X", account.LibraryScan, account.AccountId, @@ -57,6 +61,7 @@ namespace LibationWinForms.Dialogs private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e) { + e.Row.Cells[COL_Original].Value = new OriginalValue(); e.Row.Cells[COL_Delete].Value = "X"; e.Row.Cells[COL_LibraryScan].Value = true; } @@ -98,16 +103,39 @@ namespace LibationWinForms.Dialogs private void saveBtn_Click(object sender, EventArgs e) { - var accounts = AudibleApiStorage.GetAccountsSettings() - .Accounts; + foreach (DataGridViewRow row in this.dataGridView1.Rows) + { + if (row.IsNewRow) + continue; - // find added - // validate + var original = (OriginalValue)row.Cells[COL_Original].Value; + var originalAccountId = original.AccountId; + var originalLocaleName = original.LocaleName; - // find deleted + var libraryScan = (bool)row.Cells[COL_LibraryScan].Value; + var accountId = (string)row.Cells[COL_AccountId].Value; + var localeName = (string)row.Cells[COL_Locale].Value; + var accountName = (string)row.Cells[COL_AccountName].Value; + } - // find edited - // validate + // WARNING: accounts persister will write ANY EDIT immediately to file. + // Take NO action on persistent objects until the end + var accountsSettings = AudibleApiStorage.GetPersistentAccountsSettings(); + + var existingAccounts = accountsSettings.Accounts; + + foreach (var account in existingAccounts) + { + } + + // editing account id is a special case + // an account is defined by its account id, therefore this is really a different account. the user won't care about this distinction though + + // added: find and validate + + // edited: find and validate + + // deleted: find // persist diff --git a/_Tests/InternalUtilities.Tests/AccountTests.cs b/_Tests/InternalUtilities.Tests/AccountTests.cs index c9a8e403..76ced71d 100644 --- a/_Tests/InternalUtilities.Tests/AccountTests.cs +++ b/_Tests/InternalUtilities.Tests/AccountTests.cs @@ -115,7 +115,7 @@ namespace AccountsTests { ""AccountId"": ""cng"", ""AccountName"": ""my main login"", - ""LibraryScan"": false, + ""LibraryScan"": true, ""DecryptKey"": ""asdfasdf"", ""IdentityTokens"": { ""LocaleName"": ""[empty]"",