Make AccountsSettings and Persister more clear

This commit is contained in:
Robert McRackan 2020-08-25 10:34:55 -04:00
parent 84a847a838
commit 4b31207f91
9 changed files with 175 additions and 114 deletions

View File

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

View File

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

View File

@ -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<AccountsSettings>
{
/// <summary>Alias for Target </summary>
public AccountsSettings AccountsSettings => Target;
/// <summary>uses path. create file if doesn't yet exist</summary>
public AccountsSettingsPersister(AccountsSettings target, string path, string jsonPath = null)
: base(target, path, jsonPath) { }
/// <summary>load from existing file</summary>
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<T>, Array
// JSON : Array (properties on the collection will not be serialized)
@ -49,7 +31,7 @@ namespace InternalUtilities
#region Accounts
private List<Account> _accounts_backing = new List<Account>();
[JsonProperty(PropertyName = "Accounts")]
[JsonProperty(PropertyName = nameof(Accounts))]
private List<Account> _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");
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using AudibleApi.Authorization;
using Dinah.Core.IO;
using Newtonsoft.Json;
namespace InternalUtilities
{
public class AccountsSettingsPersister : JsonFilePersister<AccountsSettings>
{
/// <summary>Alias for Target </summary>
public AccountsSettings AccountsSettings => Target;
/// <summary>uses path. create file if doesn't yet exist</summary>
public AccountsSettingsPersister(AccountsSettings target, string path, string jsonPath = null)
: base(target, path, jsonPath) { }
/// <summary>load from existing file</summary>
public AccountsSettingsPersister(string path, string jsonPath = null)
: base(path, jsonPath) { }
protected override JsonSerializerSettings GetSerializerSettings()
=> Identity.GetJsonSerializerSettings();
}
}

View File

@ -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)

View File

@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>3.1.12.255</Version>
<Version>3.1.12.260</Version>
</PropertyGroup>
<ItemGroup>

View File

@ -162,7 +162,7 @@ namespace LibationLauncher
};
// saves to new file
AudibleApiStorage.GetAccountsSettings().Add(account);
AudibleApiStorage.GetPersistentAccountsSettings().Add(account);
return account;
}

View File

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

View File

@ -115,7 +115,7 @@ namespace AccountsTests
{
""AccountId"": ""cng"",
""AccountName"": ""my main login"",
""LibraryScan"": false,
""LibraryScan"": true,
""DecryptKey"": ""asdfasdf"",
""IdentityTokens"": {
""LocaleName"": ""[empty]"",