Compare commits

...

118 Commits

Author SHA1 Message Date
Robert McRackan
bc6f53c8ea bug fix around latest skip-bad-book feature 2020-12-23 16:07:47 -05:00
Robert McRackan
cefab86ce1 bug fixes around new skip-bad-book feature 2020-12-23 14:05:18 -05:00
Robert McRackan
249a2f3b59 bug fix: libhack files: directory not found 2020-12-22 15:54:46 -05:00
Robert McRackan
0e9f2c7681 Truncate too-long error message 2020-12-22 11:38:03 -05:00
Robert McRackan
d25c32ff45 When there's a problem downloading a book, you get the option to skip the file temporarily or permanently. This can be useful with extremely old audible titles where the modern download may no longer be supported 2020-12-21 16:25:42 -05:00
Robert McRackan
642a500f87 "Locale" typo. Make user msg more clear 2020-12-16 12:57:26 -05:00
Robert McRackan
0e2469db64 If no codec during download, retry with all library flags enabled 2020-12-16 11:19:12 -05:00
Robert McRackan
9aa4ef70af increase version 2020-12-14 15:56:30 -05:00
Robert McRackan
1812fc2c7c - Increase account privacy in logs
- Improve book download retry
2020-12-14 15:42:27 -05:00
Robert McRackan
f9849abb7b Better error logging when codecs are not known 2020-12-08 16:16:16 -05:00
Robert McRackan
9cfe8ee6ca bug fix: null ref exception 2020-12-03 10:58:45 -05:00
Robert McRackan
44e2cef18c New in v4.1.0
- upgrade all to .NET5
- bug fix: when codec doesn't appear in prioritized list, just get the 1st available
- add more account privacy in logs
2020-12-02 15:23:08 -05:00
Robert McRackan
4dc29affc3 Incl. audible api bug fix. Also add more account privacy in logs 2020-12-02 15:16:48 -05:00
Robert McRackan
2df38706f7 upgrade to .NET5 2020-11-18 09:32:15 -05:00
Robert McRackan
f30e9dae6f bug fix: include new ApprovalNeeded in enumeration singletons 2020-10-08 22:20:26 -04:00
Robert McRackan
50843e5102 add ApprovalNeeded page in login 2020-10-08 16:56:14 -04:00
Robert McRackan
a13b00d520 - better logging for LoginFailedException
- upgrade nuget pkg.s
2020-10-08 11:48:48 -04:00
Robert McRackan
b5ebe3db23 increm version 2020-10-07 17:48:30 -04:00
Robert McRackan
40f3e4503b Version # 2020-10-02 16:22:42 -04:00
Robert McRackan
0d93243b66 Obscure account names in logs 2020-10-02 16:10:35 -04:00
Robert McRackan
59c3845d21 Standardize logging 2020-10-02 09:35:58 -04:00
Robert McRackan
a3ee3c2881 v4.0.9 2020-10-01 12:35:16 -04:00
Robert McRackan
e971d34948 Bug fix: downloading PDFs without also Liberating books -- post-download verification step was failing 2020-09-22 15:43:13 -04:00
Robert McRackan
2b3f67fb99 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-09-21 13:10:45 -04:00
Robert McRackan
4509b8c8eb Audible whack-a-mole: they changed how to download pdfs 2020-09-21 13:10:36 -04:00
rmcrackan
2e40bebd7d Update README.md 2020-09-11 22:12:25 -04:00
Robert McRackan
dfc4121ab0 add pics for readme 2020-09-11 22:09:44 -04:00
Robert McRackan
3648607d4d export to xlsx 2020-09-11 21:56:56 -04:00
Robert McRackan
b22c35f841 new feature: json export 2020-09-11 21:07:20 -04:00
Robert McRackan
2795690199 New feature: csv export 2020-09-11 17:04:36 -04:00
Robert McRackan
b1f92343cf increm version 2020-09-10 09:07:04 -04:00
Robert McRackan
9e1d657f60 Config setting to retain aax file after decrypt 2020-09-10 09:06:34 -04:00
Robert McRackan
389761355d New version. Failed attempt to fix publish error 2020-09-07 15:45:08 -04:00
Robert McRackan
69054afaa0 update version # 2020-09-01 09:54:55 -04:00
Robert McRackan
aacdcea1e1 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-09-01 09:35:38 -04:00
Robert McRackan
0beb3bf437 Add logging 2020-09-01 09:35:13 -04:00
rmcrackan
e925b57f7f Update README.md 2020-08-31 23:03:28 -04:00
rmcrackan
5deaa06d78 Update README.md 2020-08-31 22:57:47 -04:00
Robert McRackan
eda62975ba Screenshots 2020-08-31 22:57:27 -04:00
Robert McRackan
d91e02db29 Increase version. 4.0! 2020-08-31 22:30:50 -04:00
Robert McRackan
cd604d03b1 Fix v3 => v4 migration bug. Improved error handing 2020-08-31 21:27:56 -04:00
Robert McRackan
d5cd569319 clean up comments 2020-08-30 13:01:39 -04:00
Robert McRackan
a58f51a8ce Libation 4.0 prep: do not allow user to change login id in the middle of logging in. If they do then jsonpath will fail 2020-08-28 15:03:55 -04:00
Robert McRackan
d24c10ddf5 Pass account info to login dialogs 2020-08-28 13:55:03 -04:00
Robert McRackan
a12391f0ab Serialize getting API instances so that logins don't conflict 2020-08-28 10:51:42 -04:00
Robert McRackan
60f1d8117d Remove reliance on persistent Account objects across boundaries. If you open an account persister, then dispose of it 2020-08-27 23:05:46 -04:00
Robert McRackan
20b6f28cb5 Add locale and account to search/filter options 2020-08-27 15:33:14 -04:00
Robert McRackan
9a1fa89f6f Grid, misc column: incl locale and acct 2020-08-27 15:08:36 -04:00
Robert McRackan
2a294f4f85 add locale and account to logging 2020-08-27 14:54:11 -04:00
Robert McRackan
0938c84929 Do not import non-library 'audible plus' titles 2020-08-27 13:57:53 -04:00
Robert McRackan
99cc6a6425 Accounts are editable from ScanAccountsDialog via new edit button 2020-08-27 11:36:38 -04:00
Robert McRackan
0025825d5c If no accounts, Import>Scan Library to prompt user to create account 2020-08-27 10:46:47 -04:00
Robert McRackan
81b6833118 Incl note that defaults can be changed in account settings 2020-08-26 12:59:32 -04:00
Robert McRackan
a51e76d44d Libation 4.0 prep: full multiple account support 2020-08-26 12:50:12 -04:00
Robert McRackan
755a7338e9 Account to be included on each import item, not just on the aggr group 2020-08-26 10:25:24 -04:00
Robert McRackan
56732a5365 Add stub form for 'scan mult accounts' dialog 2020-08-25 16:34:06 -04:00
Robert McRackan
dd3b032b21 Fix account persistence edge case 2020-08-25 15:58:56 -04:00
Robert McRackan
dd25792864 Multi-account scan library UI stubs 2020-08-25 15:25:28 -04:00
Robert McRackan
6979ab4450 Libation 4.0 prep: account management complete 2020-08-25 14:21:14 -04:00
Robert McRackan
4b31207f91 Make AccountsSettings and Persister more clear 2020-08-25 10:34:55 -04:00
Robert McRackan
84a847a838 Fix unit test 2020-08-24 23:15:58 -04:00
Robert McRackan
6900a68b9d Rename for clarity: AccountsSettings <=> Accounts 2020-08-24 22:57:08 -04:00
Robert McRackan
743644c4e9 account management UI 2020-08-24 16:22:55 -04:00
Robert McRackan
e9e380dbe6 Account management UI, add 'original' column 2020-08-24 14:16:20 -04:00
Robert McRackan
515dfceb73 begin account management UI 2020-08-24 14:08:07 -04:00
Robert McRackan
3941906d72 Book download validation: librarybook.accountname and/or book.locale is null/blank 2020-08-22 07:32:39 -04:00
Robert McRackan
6407d15fe0 Libation 4.0 prep: use new Accounts decrypt key 2020-08-21 22:15:25 -04:00
Robert McRackan
be84fb317e DecryptKey: static => account instance 2020-08-21 22:01:23 -04:00
Robert McRackan
3af010c1f5 Update ProcessRunner 2020-08-21 17:03:39 -04:00
Robert McRackan
714bb2ba50 Downloading to use new instance locale and account 2020-08-21 13:36:01 -04:00
Robert McRackan
2e5360f0ba Libation 4.0 prep: purge global static config Configuration.LocaleCountryCode 2020-08-21 11:50:11 -04:00
Robert McRackan
258775ff3f Purge static current locale from API: complete 2020-08-21 11:18:10 -04:00
Robert McRackan
82318ffab7 Progress toward purging static current locale 2020-08-21 09:29:33 -04:00
Robert McRackan
901572e7bb intermediate steps toward purging static current locale 2020-08-20 22:53:44 -04:00
Robert McRackan
cfa938360a Library to store book's account in db 2020-08-20 17:03:55 -04:00
Robert McRackan
80017ce9fd populate book locale on library update 2020-08-20 16:49:50 -04:00
Robert McRackan
c67972a327 Importers need access to Account 2020-08-20 16:09:07 -04:00
Robert McRackan
57ee150d3c api to use hardcoded jsonpath for now. Libation is fully functional again 2020-08-20 14:47:26 -04:00
Robert McRackan
57302e1b5c Create methods for test and demo use. Can also use temporarily in Libation until migration is complete 2020-08-19 15:42:54 -04:00
Robert McRackan
09dbc67914 refactor file migration 2020-08-18 07:14:58 -04:00
Robert McRackan
b768362eae migrate old files/settings 2020-08-18 06:51:12 -04:00
Robert McRackan
04a32533cb Libation 4.0 prep: migrate old settings => new files. remove old values. remove old file 2020-08-18 06:32:45 -04:00
Robert McRackan
1ad2135a3f Complete: Accounts, persistence, unit tests 2020-08-16 23:04:23 -04:00
Robert McRackan
643ae09b2b Libation 4.0 prep: in process of migrating to new settings files 2020-08-16 15:44:47 -04:00
Robert McRackan
8391e43b03 begin token file migration. INCOMPLETE 2020-08-13 17:01:06 -04:00
Robert McRackan
8a54eda4a0 Add temp jsonpath options during v3 => v4 migration prep 2020-08-13 15:06:22 -04:00
Robert McRackan
e0406378cb Libation 4.0 prep: Add legacy settings-file support. Move AudibleApiStorage 2020-08-13 14:37:16 -04:00
Robert McRackan
e1299331cc "ApiConnectionSettings" => "AccountsSettings" 2020-08-13 14:26:48 -04:00
Robert McRackan
248b336867 minor csproj changes 2020-08-13 14:21:59 -04:00
Robert McRackan
b7d96ae447 tiny refactor 2020-08-13 09:53:29 -04:00
Robert McRackan
8ab2af1c5d Rename file path var for clarity 2020-08-13 09:14:16 -04:00
Robert McRackan
2d459bb2cf Libation 4.0 prep: add new fields Book.Locale, LibraryBook.Account. Migrate db 2020-08-12 11:31:48 -04:00
Robert McRackan
aeb0d2a82b update nuget pkg.s 2020-08-12 10:15:04 -04:00
Robert McRackan
f50dab94a4 Libation 4.0 prep: incrementally incorporate jsonpath (1) all AccountsSettings.json access must use centralized jsonpath. For now == null 2020-08-11 14:19:35 -04:00
Robert McRackan
efa5cefa23 Libation 4.0 prep. EzApiCreator: remove locale option, add JsonPath support, libation wrapper to pass in jsonpath 2020-08-10 10:31:41 -04:00
Robert McRackan
2e4a97fde7 4.0 prep:
move IdentityTokens data to new AccountsSettings.json file
2020-08-06 14:01:12 -04:00
Robert McRackan
2f241806fa version update 2020-07-31 14:32:51 -04:00
Robert McRackan
e417f60a36 When changing locale, clear previous locale specific settings 2020-07-31 14:21:59 -04:00
Robert McRackan
b00f2bd908 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-07-31 14:05:44 -04:00
Robert McRackan
220cda42e7 Downloader internationalization bug 2020-07-31 14:03:49 -04:00
rmcrackan
f992a7ec64 Update README.md
Add download link to 'getting started'
2020-07-03 18:15:03 -04:00
Robert McRackan
c54c45df33 Bugfix. Audible changed how they handle categories, causing a new bug. Temp fix to get everything working again -- part 2 2020-06-01 21:51:47 -04:00
Robert McRackan
a8b9e187e6 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-05-26 14:38:34 -04:00
Robert McRackan
53f252e56f Bugfix. Audible changed how they handle categories, causing a new bug. Temp fix to get everything working again 2020-05-26 14:38:29 -04:00
rmcrackan
2827bc8904 Update README.md 2020-05-25 07:22:06 -04:00
rmcrackan
98a775fc5a Update README.md 2020-05-25 07:21:35 -04:00
Robert McRackan
f28a729d36 forgot to increment build number 2020-04-18 22:27:59 -04:00
Robert McRackan
00a6a4bf50 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-04-18 22:23:04 -04:00
Robert McRackan
fdefa7c3bf Dependency updated. Increase version 2020-04-18 22:22:59 -04:00
rmcrackan
244862299f Update README.md
Add Australia
2020-03-04 09:47:20 -05:00
Robert McRackan
4decf9d3b7 Experimental: add Australia to locale options 2020-03-03 15:31:41 -05:00
Robert McRackan
83f538d304 Improved logging 2020-02-18 12:20:13 -05:00
Robert McRackan
9e0e06e436 Bugfix: some series indexes/sequences formats cause library not to import 2020-02-17 16:41:12 -05:00
Robert McRackan
f27ac279b2 Bugfix: some series indexes/sequences could cause library not to import 2020-02-17 15:17:59 -05:00
Robert McRackan
ed03fd2451 increment csproj version 2020-02-14 14:08:51 -05:00
Robert McRackan
ccb60ae367 Bugfix: IsAuthorNarrated was returning no books 2020-02-14 14:01:36 -05:00
Robert McRackan
6ad541c199 fix weirdness with build number 2020-01-02 09:01:04 -05:00
Robert McRackan
9606acda26 update release notes 2019-12-31 13:00:36 -05:00
99 changed files with 4420 additions and 853 deletions

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -276,13 +276,13 @@ namespace AaxDecrypter
};
info.EnvironmentVariables["VARIABLE"] = decryptKey;
var (output, exitCode) = info.RunHidden();
var result = info.RunHidden();
// bad checksum -- bad decrypt key
if (output.Contains("checksums mismatch, aborting!"))
if (result.Output.Contains("checksums mismatch, aborting!"))
return -99;
return exitCode;
return result.ExitCode;
}
// temp file names for steps 3, 4, 5

View File

@@ -21,8 +21,7 @@ namespace AaxDecrypter
};
// checksum is in the debug info. ffprobe's debug info is written to stderr, not stdout
var readErrorOutput = true;
var ffprobeStderr = info.RunHidden(readErrorOutput).Output;
var ffprobeStderr = info.RunHidden().Error;
// example checksum line:
// ... [aax] file checksum == 0c527840c4f18517157eb0b4f9d6f9317ce60cd1

View File

@@ -1,9 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="16.0.0" />
<PackageReference Include="NPOI" Version="2.5.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />

View File

@@ -1,39 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using DataLayer;
using Dinah.Core;
using DtoImporterService;
using InternalUtilities;
using Serilog;
namespace ApplicationServices
{
public static class LibraryCommands
{
public static async Task<(int totalCount, int newCount)> ImportLibraryAsync(ILoginCallback callback)
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, params Account[] accounts)
{
if (accounts is null || accounts.Length == 0)
return (0, 0);
try
{
var audibleApiActions = new AudibleApiActions();
var items = await audibleApiActions.GetAllLibraryItemsAsync(callback);
var totalCount = items.Count;
Serilog.Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
var importItems = await scanAccountsAsync(loginCallbackFactoryFunc, accounts);
using var context = DbContexts.GetContext();
var libImporter = new LibraryImporter(context);
var newCount = await Task.Run(() => libImporter.Import(items));
context.SaveChanges();
Serilog.Log.Logger.Information($"Import: New count {newCount}");
var totalCount = importItems.Count;
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
var newCount = await importIntoDbAsync(importItems);
Log.Logger.Information($"Import: New count {newCount}");
await Task.Run(() => SearchEngineCommands.FullReIndex());
Serilog.Log.Logger.Information("FullReIndex: success");
Log.Logger.Information("FullReIndex: success");
return (totalCount, newCount);
}
catch (Exception ex)
catch (AudibleApi.Authentication.LoginFailedException lfEx)
{
Serilog.Log.Logger.Error(ex, "Error importing library");
lfEx.MoveResponseBodyFile(FileManager.Configuration.Instance.LibationFiles);
// nuget Serilog.Exceptions would automatically log custom properties
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
// https://github.com/RehanSaeed/Serilog.Exceptions
// work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc
Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new {
lfEx.RequestUrl,
ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
lfEx.ResponseInputFields,
lfEx.ResponseBodyFilePath
});
throw;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error importing library");
throw;
}
}
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, Account[] accounts)
{
var tasks = new List<Task<List<ImportItem>>>();
foreach (var account in accounts)
{
var callback = loginCallbackFactoryFunc(account);
// get APIs in serial, esp b/c of logins
var api = await AudibleApiActions.GetApiAsync(callback, account);
// add scanAccountAsync as a TASK: do not await
tasks.Add(scanAccountAsync(api, account));
}
// import library in parallel
var arrayOfLists = await Task.WhenAll(tasks);
var importItems = arrayOfLists.SelectMany(a => a).ToList();
return importItems;
}
private static async Task<List<ImportItem>> scanAccountAsync(Api api, Account account)
{
ArgumentValidator.EnsureNotNull(account, nameof(account));
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
{
Account = account?.MaskedLogEntry ?? "[null]"
});
var dtoItems = await AudibleApiActions.GetLibraryValidatedAsync(api);
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
}
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
{
using var context = DbContexts.GetContext();
var libraryImporter = new LibraryImporter(context);
var newCount = await Task.Run(() => libraryImporter.Import(importItems));
context.SaveChanges();
return newCount;
}
public static int UpdateTags(this LibationContext context, Book book, string newTags)
@@ -51,7 +115,7 @@ namespace ApplicationServices
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error updating tags");
Log.Logger.Error(ex, "Error updating tags");
throw;
}
}

View File

@@ -0,0 +1,268 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CsvHelper;
using CsvHelper.Configuration.Attributes;
using DataLayer;
using NPOI.XSSF.UserModel;
using Serilog;
namespace ApplicationServices
{
public class ExportDto
{
public static string GetName(string fieldName)
{
var property = typeof(ExportDto).GetProperty(fieldName);
var attribute = property.GetCustomAttributes(typeof(NameAttribute), true)[0];
var description = (NameAttribute)attribute;
var text = description.Names;
return text[0];
}
[Name("Account")]
public string Account { get; set; }
[Name("Date Added to library")]
public DateTime DateAdded { get; set; }
[Name("Audible Product Id")]
public string AudibleProductId { get; set; }
[Name("Locale")]
public string Locale { get; set; }
[Name("Title")]
public string Title { get; set; }
[Name("Authors")]
public string AuthorNames { get; set; }
[Name("Narrators")]
public string NarratorNames { get; set; }
[Name("Length In Minutes")]
public int LengthInMinutes { get; set; }
[Name("Publisher")]
public string Publisher { get; set; }
[Name("Pdf url")]
public string PdfUrl { get; set; }
[Name("Series Names")]
public string SeriesNames { get; set; }
[Name("Series Order")]
public string SeriesOrder { get; set; }
[Name("Community Rating: Overall")]
public float? CommunityRatingOverall { get; set; }
[Name("Community Rating: Performance")]
public float? CommunityRatingPerformance { get; set; }
[Name("Community Rating: Story")]
public float? CommunityRatingStory { get; set; }
[Name("Cover Id")]
public string PictureId { get; set; }
[Name("Is Abridged?")]
public bool IsAbridged { get; set; }
[Name("Date Published")]
public DateTime? DatePublished { get; set; }
[Name("Categories")]
public string CategoriesNames { get; set; }
[Name("My Rating: Overall")]
public float? MyRatingOverall { get; set; }
[Name("My Rating: Performance")]
public float? MyRatingPerformance { get; set; }
[Name("My Rating: Story")]
public float? MyRatingStory { get; set; }
[Name("My Libation Tags")]
public string MyLibationTags { get; set; }
}
public static class LibToDtos
{
public static List<ExportDto> ToDtos(this IEnumerable<LibraryBook> library)
=> library.Select(a => new ExportDto
{
Account = a.Account,
DateAdded = a.DateAdded,
AudibleProductId = a.Book.AudibleProductId,
Locale = a.Book.Locale,
Title = a.Book.Title,
AuthorNames = a.Book.AuthorNames,
NarratorNames = a.Book.NarratorNames,
LengthInMinutes = a.Book.LengthInMinutes,
Publisher = a.Book.Publisher,
PdfUrl = a.Book.Supplements?.FirstOrDefault()?.Url,
SeriesNames = a.Book.SeriesNames,
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Index} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
CommunityRatingOverall = a.Book.Rating?.OverallRating,
CommunityRatingPerformance = a.Book.Rating?.PerformanceRating,
CommunityRatingStory = a.Book.Rating?.StoryRating,
PictureId = a.Book.PictureId,
IsAbridged = a.Book.IsAbridged,
DatePublished = a.Book.DatePublished,
CategoriesNames = a.Book.CategoriesNames.Any() ? a.Book.CategoriesNames.Aggregate((a, b) => $"{a}, {b}") : "",
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
MyLibationTags = a.Book.UserDefinedItem.Tags
}).ToList();
}
public static class LibraryExporter
{
public static void ToCsv(string saveFilePath)
{
using var context = DbContexts.GetContext();
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
if (!dtos.Any())
return;
using var writer = new System.IO.StreamWriter(saveFilePath);
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
csv.WriteHeader(typeof(ExportDto));
csv.NextRecord();
csv.WriteRecords(dtos);
}
public static void ToJson(string saveFilePath)
{
using var context = DbContexts.GetContext();
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
var json = Newtonsoft.Json.JsonConvert.SerializeObject(dtos, Newtonsoft.Json.Formatting.Indented);
System.IO.File.WriteAllText(saveFilePath, json);
}
public static void ToXlsx(string saveFilePath)
{
using var context = DbContexts.GetContext();
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
var workbook = new XSSFWorkbook();
var sheet = workbook.CreateSheet("Library");
var detailSubtotalFont = workbook.CreateFont();
detailSubtotalFont.IsBold = true;
var detailSubtotalCellStyle = workbook.CreateCellStyle();
detailSubtotalCellStyle.SetFont(detailSubtotalFont);
// headers
var rowIndex = 0;
var row = sheet.CreateRow(rowIndex);
var columns = new[] {
nameof (ExportDto.Account),
nameof (ExportDto.DateAdded),
nameof (ExportDto.AudibleProductId),
nameof (ExportDto.Locale),
nameof (ExportDto.Title),
nameof (ExportDto.AuthorNames),
nameof (ExportDto.NarratorNames),
nameof (ExportDto.LengthInMinutes),
nameof (ExportDto.Publisher),
nameof (ExportDto.PdfUrl),
nameof (ExportDto.SeriesNames),
nameof (ExportDto.SeriesOrder),
nameof (ExportDto.CommunityRatingOverall),
nameof (ExportDto.CommunityRatingPerformance),
nameof (ExportDto.CommunityRatingStory),
nameof (ExportDto.PictureId),
nameof (ExportDto.IsAbridged),
nameof (ExportDto.DatePublished),
nameof (ExportDto.CategoriesNames),
nameof (ExportDto.MyRatingOverall),
nameof (ExportDto.MyRatingPerformance),
nameof (ExportDto.MyRatingStory),
nameof (ExportDto.MyLibationTags)
};
var col = 0;
foreach (var c in columns)
{
var cell = row.CreateCell(col++);
var name = ExportDto.GetName(c);
cell.SetCellValue(name);
cell.CellStyle = detailSubtotalCellStyle;
}
var dateFormat = workbook.CreateDataFormat();
var dateStyle = workbook.CreateCellStyle();
dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss");
rowIndex++;
// Add data rows
foreach (var dto in dtos)
{
col = 0;
row = sheet.CreateRow(rowIndex);
row.CreateCell(col++).SetCellValue(dto.Account);
var dateAddedCell = row.CreateCell(col++);
dateAddedCell.CellStyle = dateStyle;
dateAddedCell.SetCellValue(dto.DateAdded);
row.CreateCell(col++).SetCellValue(dto.AudibleProductId);
row.CreateCell(col++).SetCellValue(dto.Locale);
row.CreateCell(col++).SetCellValue(dto.Title);
row.CreateCell(col++).SetCellValue(dto.AuthorNames);
row.CreateCell(col++).SetCellValue(dto.NarratorNames);
row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
row.CreateCell(col++).SetCellValue(dto.Publisher);
row.CreateCell(col++).SetCellValue(dto.PdfUrl);
row.CreateCell(col++).SetCellValue(dto.SeriesNames);
row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
col = createCell(row, col, dto.CommunityRatingOverall);
col = createCell(row, col, dto.CommunityRatingPerformance);
col = createCell(row, col, dto.CommunityRatingStory);
row.CreateCell(col++).SetCellValue(dto.PictureId);
row.CreateCell(col++).SetCellValue(dto.IsAbridged);
var datePubCell = row.CreateCell(col++);
datePubCell.CellStyle = dateStyle;
if (dto.DatePublished.HasValue)
datePubCell.SetCellValue(dto.DatePublished.Value);
else
datePubCell.SetCellValue("");
row.CreateCell(col++).SetCellValue(dto.CategoriesNames);
col = createCell(row, col, dto.MyRatingOverall);
col = createCell(row, col, dto.MyRatingPerformance);
col = createCell(row, col, dto.MyRatingStory);
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
rowIndex++;
}
using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
workbook.Write(fileData);
}
private static int createCell(NPOI.SS.UserModel.IRow row, int col, float? nullableFloat)
{
if (nullableFloat.HasValue)
row.CreateCell(col++).SetCellValue(nullableFloat.Value);
else
row.CreateCell(col++).SetCellValue("");
return col;
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;netstandard2.1</TargetFrameworks>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
@@ -12,13 +12,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -0,0 +1,338 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20200812152646_AddLocaleAndAccount")]
partial class AddLocaleAndAccount
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.7");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.WithOwner("Book")
.HasForeignKey("BookId");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
});
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class AddLocaleAndAccount : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Account",
table: "Library",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Locale",
table: "Books",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Account",
table: "Library");
migrationBuilder.DropColumn(
name: "Locale",
table: "Books");
}
}
}

View File

@@ -14,7 +14,7 @@ namespace DataLayer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.0.0");
.HasAnnotation("ProductVersion", "3.1.7");
modelBuilder.Entity("DataLayer.Book", b =>
{
@@ -40,6 +40,9 @@ namespace DataLayer.Migrations
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
@@ -141,6 +144,9 @@ namespace DataLayer.Migrations
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");

View File

@@ -26,6 +26,9 @@ namespace DataLayer
public string Description { get; private set; }
public int LengthInMinutes { get; private set; }
// immutable-ish. should be immutable. mutability is necessary for v3 => v4 upgrades
public string Locale { get; private set; }
// mutable
public string PictureId { get; set; }
@@ -63,7 +66,7 @@ namespace DataLayer
int lengthInMinutes,
IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators,
Category category)
Category category, string localeName)
{
// validate
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
@@ -72,6 +75,7 @@ namespace DataLayer
// assign as soon as possible. stuff below relies on this
AudibleProductId = productId;
Locale = localeName;
ArgumentValidator.EnsureNotNullOrWhiteSpace(title, nameof(title));
@@ -240,6 +244,10 @@ namespace DataLayer
Category = category;
}
public override string ToString() => $"[{AudibleProductId}] {Title}";
// needed for v3 => v4 upgrade
public void UpdateLocale(string localeName)
=> Locale ??= localeName;
public override string ToString() => $"[{AudibleProductId}] {Title}";
}
}

View File

@@ -10,14 +10,24 @@ namespace DataLayer
public DateTime DateAdded { get; private set; }
// immutable-ish. should be immutable. mutability is necessary for v3 => v4 upgrades
public string Account { get; private set; }
private LibraryBook() { }
public LibraryBook(Book book, DateTime dateAdded)
public LibraryBook(Book book, DateTime dateAdded, string account)
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
ArgumentValidator.EnsureNotNull(account, nameof(account));
Book = book;
DateAdded = dateAdded;
Account = account;
}
public override string ToString() => $"{DateAdded:d} {Book}";
// needed for v3 => v4 upgrade
public void UpdateAccount(string account)
=> Account ??= account;
public override string ToString() => $"{DateAdded:d} {Book}";
}
}

View File

@@ -1,4 +1,8 @@
HOW TO CREATE: EF CORE PROJECT
FOR QUICK MIGRATION INSTRUCTIONS:
_DB_NOTES.txt
HOW TO CREATE: EF CORE PROJECT
==============================
example is for sqlite but the same works with MsSql

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -11,23 +11,23 @@ namespace DtoImporterService
{
public BookImporter(LibationContext context) : base(context) { }
public override IEnumerable<Exception> Validate(IEnumerable<Item> items) => new BookValidator().Validate(items);
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new BookValidator().Validate(importItems.Select(i => i.DtoItem));
protected override int DoImport(IEnumerable<Item> items)
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
// pre-req.s
new ContributorImporter(DbContext).Import(items);
new SeriesImporter(DbContext).Import(items);
new CategoryImporter(DbContext).Import(items);
new ContributorImporter(DbContext).Import(importItems);
new SeriesImporter(DbContext).Import(importItems);
new CategoryImporter(DbContext).Import(importItems);
// get distinct
var productIds = items.Select(i => i.ProductId).ToList();
var productIds = importItems.Select(i => i.DtoItem.ProductId).ToList();
// load db existing => .Local
loadLocal_books(productIds);
// upsert
var qtyNew = upsertBooks(items);
var qtyNew = upsertBooks(importItems);
return qtyNew;
}
@@ -44,13 +44,13 @@ namespace DtoImporterService
DbContext.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
}
private int upsertBooks(IEnumerable<Item> items)
private int upsertBooks(IEnumerable<ImportItem> importItems)
{
var qtyNew = 0;
foreach (var item in items)
foreach (var item in importItems)
{
var book = DbContext.Books.Local.SingleOrDefault(p => p.AudibleProductId == item.ProductId);
var book = DbContext.Books.Local.SingleOrDefault(p => p.AudibleProductId == item.DtoItem.ProductId);
if (book is null)
{
book = createNewBook(item);
@@ -63,8 +63,10 @@ namespace DtoImporterService
return qtyNew;
}
private Book createNewBook(Item item)
private Book createNewBook(ImportItem importItem)
{
var item = importItem.DtoItem;
// absence of authors is very rare, but possible
if (!item.Authors?.Any() ?? true)
item.Authors = new[] { new Person { Name = "", Asin = null } };
@@ -86,8 +88,16 @@ namespace DtoImporterService
.ToList();
// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
// absence of categories is very rare, but possible
var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";
// absence of categories is also possible
// CATEGORY HACK: only use the 1st 2 categories
// (real impl: var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";)
var lastCategory
= item.Categories.Length == 0 ? ""
: item.Categories.Length == 1 ? item.Categories[0].CategoryId
// 2+
: item.Categories[1].CategoryId;
var category = DbContext.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == lastCategory);
var book = DbContext.Books.Add(new Book(
@@ -97,7 +107,8 @@ namespace DtoImporterService
item.LengthInMinutes,
authors,
narrators,
category)
category,
importItem.LocaleName)
).Entity;
var publisherName = item.Publisher;
@@ -115,12 +126,17 @@ namespace DtoImporterService
return book;
}
private void updateBook(Item item, Book book)
private void updateBook(ImportItem importItem, Book book)
{
var item = importItem.DtoItem;
// set/update book-specific info which may have changed
book.PictureId = item.PictureId;
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
// needed during v3 => v4 migration
book.UpdateLocale(importItem.LocaleName);
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);
@@ -131,7 +147,18 @@ namespace DtoImporterService
foreach (var seriesEntry in item.Series)
{
var series = DbContext.Series.Local.Single(s => seriesEntry.SeriesId == s.AudibleSeriesId);
book.UpsertSeries(series, seriesEntry.Index);
var index = 0f;
try
{
index = seriesEntry.Index;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Error parsing series index. Title: {item.Title}. ASIN: {item.Asin}. Series index: {seriesEntry.Sequence}");
}
book.UpsertSeries(series, index);
}
}
}

View File

@@ -11,18 +11,24 @@ namespace DtoImporterService
{
public CategoryImporter(LibationContext context) : base(context) { }
public override IEnumerable<Exception> Validate(IEnumerable<Item> items) => new CategoryValidator().Validate(items);
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new CategoryValidator().Validate(importItems.Select(i => i.DtoItem));
protected override int DoImport(IEnumerable<Item> items)
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
// get distinct
var categoryIds = items.GetCategoriesDistinct().Select(c => c.CategoryId).ToList();
var categoryIds = importItems
.Select(i => i.DtoItem)
.GetCategoriesDistinct()
.Select(c => c.CategoryId).ToList();
// load db existing => .Local
loadLocal_categories(categoryIds);
// upsert
var categoryPairs = items.GetCategoryPairsDistinct().ToList();
var categoryPairs = importItems
.Select(i => i.DtoItem)
.GetCategoryPairsDistinct()
.ToList();
var qtyNew = upsertCategories(categoryPairs);
return qtyNew;
}
@@ -51,6 +57,10 @@ namespace DtoImporterService
{
for (var i = 0; i < pair.Length; i++)
{
// CATEGORY HACK: not yet supported: depth beyond 0 and 1
if (i > 1)
break;
var id = pair[i].CategoryId;
var name = pair[i].CategoryName;

View File

@@ -11,14 +11,23 @@ namespace DtoImporterService
{
public ContributorImporter(LibationContext context) : base(context) { }
public override IEnumerable<Exception> Validate(IEnumerable<Item> items) => new ContributorValidator().Validate(items);
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new ContributorValidator().Validate(importItems.Select(i => i.DtoItem));
protected override int DoImport(IEnumerable<Item> items)
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
// get distinct
var authors = items.GetAuthorsDistinct().ToList();
var narrators = items.GetNarratorsDistinct().ToList();
var publishers = items.GetPublishersDistinct().ToList();
var authors = importItems
.Select(i => i.DtoItem)
.GetAuthorsDistinct()
.ToList();
var narrators = importItems
.Select(i => i.DtoItem)
.GetNarratorsDistinct()
.ToList();
var publishers = importItems
.Select(i => i.DtoItem)
.GetPublishersDistinct()
.ToList();
// load db existing => .Local
var allNames = publishers

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
using DataLayer;
using Dinah.Core;
using InternalUtilities;
namespace DtoImporterService
{
@@ -11,7 +11,7 @@ namespace DtoImporterService
{
protected LibationContext DbContext { get; }
public ImporterBase(LibationContext context)
protected ImporterBase(LibationContext context)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
DbContext = context;
@@ -50,8 +50,8 @@ namespace DtoImporterService
public abstract IEnumerable<Exception> Validate(T param);
}
public abstract class ItemsImporterBase : ImporterBase<IEnumerable<Item>>
public abstract class ItemsImporterBase : ImporterBase<IEnumerable<ImportItem>>
{
public ItemsImporterBase(LibationContext context) : base(context) { }
protected ItemsImporterBase(LibationContext context) : base(context) { }
}
}

View File

@@ -11,29 +11,50 @@ namespace DtoImporterService
{
public LibraryImporter(LibationContext context) : base(context) { }
public override IEnumerable<Exception> Validate(IEnumerable<Item> items) => new LibraryValidator().Validate(items);
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new LibraryValidator().Validate(importItems.Select(i => i.DtoItem));
protected override int DoImport(IEnumerable<Item> items)
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
new BookImporter(DbContext).Import(items);
new BookImporter(DbContext).Import(importItems);
var qtyNew = upsertLibraryBooks(items);
var qtyNew = upsertLibraryBooks(importItems);
return qtyNew;
}
private int upsertLibraryBooks(IEnumerable<Item> items)
private int upsertLibraryBooks(IEnumerable<ImportItem> importItems)
{
// technically, we should be able to have duplicate books from separate accounts.
// this would violate the current pk and would be difficult to deal with elsewhere:
// - what to show in the grid
// - which to consider liberated
//
// sqlite cannot alter pk. the work around is an extensive headache. it'll be fixed in pre .net5/efcore5
//
// currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region.
//
// CURRENT SOLUTION: don't re-insert
var currentLibraryProductIds = DbContext.Library.Select(l => l.Book.AudibleProductId).ToList();
var newItems = items.Where(dto => !currentLibraryProductIds.Contains(dto.ProductId)).ToList();
var newItems = importItems.Where(dto => !currentLibraryProductIds.Contains(dto.DtoItem.ProductId)).ToList();
foreach (var newItem in newItems)
{
var libraryBook = new LibraryBook(
DbContext.Books.Local.Single(b => b.AudibleProductId == newItem.ProductId),
newItem.DateAdded);
DbContext.Books.Local.Single(b => b.AudibleProductId == newItem.DtoItem.ProductId),
newItem.DtoItem.DateAdded,
newItem.AccountId);
DbContext.Library.Add(libraryBook);
}
// needed for v3 => v4 upgrade
var toUpdate = DbContext.Library.Where(l => l.Account == null);
foreach (var u in toUpdate)
{
var item = importItems.FirstOrDefault(ii => ii.DtoItem.ProductId == u.Book.AudibleProductId);
if (item != null)
u.UpdateAccount(item.AccountId);
}
var qtyNew = newItems.Count;
return qtyNew;
}

View File

@@ -11,12 +11,15 @@ namespace DtoImporterService
{
public SeriesImporter(LibationContext context) : base(context) { }
public override IEnumerable<Exception> Validate(IEnumerable<Item> items) => new SeriesValidator().Validate(items);
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new SeriesValidator().Validate(importItems.Select(i => i.DtoItem));
protected override int DoImport(IEnumerable<Item> items)
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
// get distinct
var series = items.GetSeriesDistinct().ToList();
var series = importItems
.Select(i => i.DtoItem)
.GetSeriesDistinct()
.ToList();
// load db existing => .Local
loadLocal_series(series);

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -8,6 +8,7 @@ using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using FileManager;
using InternalUtilities;
namespace FileLiberator
{
@@ -55,22 +56,33 @@ namespace FileLiberator
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b");
var outputAudioFilename = await aaxToM4bConverterDecrypt(proposedOutputFile, aaxFilename);
var outputAudioFilename = await aaxToM4bConverterDecrypt(aaxFilename, libraryBook);
// decrypt failed
if (outputAudioFilename == null)
return new StatusHandler { "Decrypt failed" };
moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
var destinationDir = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
Dinah.Core.IO.FileExt.SafeDelete(aaxFilename);
var config = Configuration.Instance;
if (config.RetainAaxFiles)
{
var newAaxFilename = FileUtility.GetValidFilename(
destinationDir,
Path.GetFileNameWithoutExtension(aaxFilename),
"aax");
File.Move(aaxFilename, newAaxFilename);
}
else
{
Dinah.Core.IO.FileExt.SafeDelete(aaxFilename);
}
var statusHandler = new StatusHandler();
var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
if (!finalAudioExists)
statusHandler.AddError("Cannot find final audio file after decryption");
return statusHandler;
return new StatusHandler { "Cannot find final audio file after decryption" };
return new StatusHandler();
}
finally
{
@@ -78,13 +90,19 @@ namespace FileLiberator
}
}
private async Task<string> aaxToM4bConverterDecrypt(string proposedOutputFile, string aaxFilename)
private async Task<string> aaxToM4bConverterDecrypt(string aaxFilename, LibraryBook libraryBook)
{
DecryptBegin?.Invoke(this, $"Begin decrypting {aaxFilename}");
try
{
var converter = await AaxToM4bConverter.CreateAsync(aaxFilename, Configuration.Instance.DecryptKey);
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var account = persister
.AccountsSettings
.GetAccount(libraryBook.Account, libraryBook.Book.Locale);
var converter = await AaxToM4bConverter.CreateAsync(aaxFilename, account.DecryptKey);
converter.AppName = "Libation";
TitleDiscovered?.Invoke(this, converter.tags.title);
@@ -92,7 +110,8 @@ namespace FileLiberator
NarratorsDiscovered?.Invoke(this, converter.tags.narrator);
CoverImageFilepathDiscovered?.Invoke(this, converter.coverBytes);
// override default which was set in CreateAsync
// override default which was set in CreateAsync
var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b");
converter.SetOutputFilename(proposedOutputFile);
converter.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
@@ -103,7 +122,7 @@ namespace FileLiberator
if (!success)
return null;
Configuration.Instance.DecryptKey = converter.decryptKey;
account.DecryptKey = converter.decryptKey;
return converter.outputFileName;
}
@@ -113,12 +132,12 @@ namespace FileLiberator
}
}
private static void moveFilesToBooksDir(Book product, string outputAudioFilename)
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
{
// create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]"
var destinationDir = getDestDir(product);
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
Directory.CreateDirectory(destinationDir);
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
@@ -135,19 +154,9 @@ namespace FileLiberator
File.Move(f.FullName, dest);
}
}
private static string getDestDir(Book product)
{
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
var underscoreIndex = product.Title.IndexOf(':');
var titleDir
= underscoreIndex < 4
? product.Title
: product.Title.Substring(0, underscoreIndex);
var finalDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, product.AudibleProductId);
return finalDir;
}
return destinationDir;
}
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
{

View File

@@ -1,10 +1,12 @@
using System;
using System.IO;
using System.Threading.Tasks;
using AudibleApi;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using FileManager;
using InternalUtilities;
namespace FileLiberator
{
@@ -18,6 +20,8 @@ namespace FileLiberator
/// </summary>
public class DownloadBook : DownloadableBase
{
private const string SERVICE_UNAVAILABLE = "Content Delivery Companion Service is not available.";
public override bool Validate(LibraryBook libraryBook)
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)
&& !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId);
@@ -39,7 +43,9 @@ namespace FileLiberator
private async Task<string> downloadBookAsync(LibraryBook libraryBook, string tempAaxFilename)
{
var api = await AudibleApi.EzApiCreator.GetApiAsync(AudibleApiStorage.IdentityTokensFile);
validate(libraryBook);
var api = await GetApiAsync(libraryBook);
var actualFilePath = await PerformDownloadAsync(
tempAaxFilename,
@@ -48,18 +54,53 @@ namespace FileLiberator
System.Threading.Thread.Sleep(100);
// if bad file download, a 0-33 byte file will be created
// if service unavailable, a 52 byte string will be saved as file
if (new FileInfo(actualFilePath).Length < 100)
var length = new FileInfo(actualFilePath).Length;
if (length > 100)
return actualFilePath;
var contents = File.ReadAllText(actualFilePath);
File.Delete(actualFilePath);
var exMsg = contents.StartsWithInsensitive(SERVICE_UNAVAILABLE)
? SERVICE_UNAVAILABLE
: "Error downloading file";
var ex = new Exception(exMsg);
Serilog.Log.Logger.Error(ex, "Download error {@DebugInfo}", new
{
var contents = File.ReadAllText(actualFilePath);
File.Delete(actualFilePath);
libraryBook.Book.Title,
libraryBook.Book.AudibleProductId,
libraryBook.Book.Locale,
Account = libraryBook.Account?.ToMask() ?? "[empty]",
tempAaxFilename,
actualFilePath,
length,
contents
});
throw ex;
}
var unavailable = "Content Delivery Companion Service is not available.";
if (contents.StartsWithInsensitive(unavailable))
throw new Exception(unavailable);
throw new Exception("Error downloading file");
}
private static void validate(LibraryBook libraryBook)
{
string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
return actualFilePath;
string errorTitle()
{
var title
= (libraryBook.Book.Title.Length > 53)
? $"{libraryBook.Book.Title.Truncate(50)}..."
: libraryBook.Book.Title;
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
return errorBookTitle;
};
if (string.IsNullOrWhiteSpace(libraryBook.Account))
throw new Exception(errorString("Account"));
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
throw new Exception(errorString("Locale"));
}
private void moveBook(LibraryBook libraryBook, string actualFilePath)

View File

@@ -31,17 +31,24 @@ namespace FileLiberator
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
{
// if audio file exists, get it's dir. else return base Book dir
var destinationDir =
// this is safe b/c GetDirectoryName(null) == null
Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId))
?? AudibleFileStorage.PDF.StorageDirectory;
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
var file = getdownloadUrl(libraryBook);
return Path.Combine(destinationDir, Path.GetFileName(getdownloadUrl(libraryBook)));
if (existingPath != null)
return Path.Combine(existingPath, Path.GetFileName(file));
var full = FileUtility.GetValidFilename(
AudibleFileStorage.PDF.StorageDirectory,
libraryBook.Book.Title,
Path.GetExtension(file),
libraryBook.Book.AudibleProductId);
return full;
}
private async Task downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
{
var downloadUrl = getdownloadUrl(libraryBook);
var api = await GetApiAsync(libraryBook);
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
var client = new HttpClient();
var actualDownloadedFilePath = await PerformDownloadAsync(

View File

@@ -38,6 +38,9 @@ namespace FileLiberator
}
}
protected static Task<AudibleApi.Api> GetApiAsync(LibraryBook libraryBook)
=> InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
protected async Task<string> PerformDownloadAsync(string proposedDownloadFilePath, Func<Progress<DownloadProgress>, Task<string>> func)
{
var progress = new Progress<DownloadProgress>();

View File

@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
namespace FileLiberator
@@ -15,56 +17,47 @@ namespace FileLiberator
//
/// <summary>Process the first valid product. Create default context</summary>
/// <returns>Returns either the status handler from the process, or null if all books have been processed</returns>
public static async Task<StatusHandler> ProcessFirstValidAsync(this IProcessable processable)
{
var libraryBook = processable.getNextValidBook();
if (libraryBook == null)
return null;
// when used in foreach: stateful. deferred execution
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable)
=> DbContexts.GetContext()
.GetLibrary_Flat_NoTracking()
.Where(libraryBook => processable.Validate(libraryBook));
return await processBookAsync(processable, libraryBook);
}
/// <summary>Process the first valid product. Create default context</summary>
/// <returns>Returns either the status handler from the process, or null if all books have been processed</returns>
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, string productId)
public static LibraryBook GetSingleLibraryBook(string productId)
{
using var context = DbContexts.GetContext();
var libraryBook = context
.Library
.GetLibrary()
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
return libraryBook;
}
if (libraryBook == null)
return null;
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook)
{
if (!processable.Validate(libraryBook))
return new StatusHandler { "Validation failed" };
return await processBookAsync(processable, libraryBook);
return await processable.ProcessBookAsync_NoValidation(libraryBook);
}
private static async Task<StatusHandler> processBookAsync(IProcessable processable, LibraryBook libraryBook)
public static async Task<StatusHandler> ProcessBookAsync_NoValidation(this IProcessable processable, LibraryBook libraryBook)
{
// this should never happen. check anyway. ProcessFirstValidAsync returning null is the signal that we're done. we can't let another IProcessable accidentally send this command
var status = await processable.ProcessAsync(libraryBook);
if (status == null)
throw new Exception("Processable should never return a null status");
Serilog.Log.Logger.Information("Begin " + nameof(ProcessBookAsync_NoValidation) + " {@DebugInfo}", new
{
libraryBook.Book.Title,
libraryBook.Book.AudibleProductId,
libraryBook.Book.Locale,
Account = libraryBook.Account?.ToMask() ?? "[empty]"
});
var status
= (await processable.ProcessAsync(libraryBook))
?? new StatusHandler { "Processable should never return a null status" };
return status;
}
private static LibraryBook getNextValidBook(this IProcessable processable)
{
var libraryBooks = DbContexts.GetContext().GetLibrary_Flat_NoTracking();
foreach (var libraryBook in libraryBooks)
if (processable.Validate(libraryBook))
return libraryBook;
return null;
}
public static async Task<StatusHandler> TryProcessAsync(this IProcessable processable, LibraryBook libraryBook)
=> processable.Validate(libraryBook)
? await processable.ProcessAsync(libraryBook)

View File

@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Polly" Version="7.2.0" />
<PackageReference Include="Polly" Version="7.2.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,10 +0,0 @@
using System.IO;
namespace FileManager
{
public static class AudibleApiStorage
{
// not customizable. don't move to config
public static string IdentityTokensFile => Path.Combine(Configuration.Instance.LibationFiles, "IdentityTokens.json");
}
}

View File

@@ -11,76 +11,77 @@ namespace FileManager
// could add images here, but for now images are stored in a well-known location
public enum FileType { Unknown, Audio, AAX, PDF }
/// <summary>
/// Files are large. File contents are never read by app.
/// Paths are varied.
/// Files are written during download/decrypt/backup/liberate.
/// Paths are read at app launch and during download/decrypt/backup/liberate.
/// Many files are often looked up at once
/// </summary>
public sealed class AudibleFileStorage : Enumeration<AudibleFileStorage>
{
#region static
public static AudibleFileStorage Audio { get; }
public static AudibleFileStorage AAX { get; }
public static AudibleFileStorage PDF { get; }
/// <summary>
/// Files are large. File contents are never read by app.
/// Paths are varied.
/// Files are written during download/decrypt/backup/liberate.
/// Paths are read at app launch and during download/decrypt/backup/liberate.
/// Many files are often looked up at once
/// </summary>
public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage>
{
public abstract string[] Extensions { get; }
public abstract string StorageDirectory { get; }
public static string DownloadsInProgress { get; }
public static string DecryptInProgress { get; }
public static string BooksDirectory => Configuration.Instance.Books;
// not customizable. don't move to config
public static string DownloadsFinal { get; }
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("DownloadsFinal").FullName;
#region static
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
public static AudibleFileStorage AAX { get; } = new AaxFileStorage();
public static AudibleFileStorage PDF { get; } = new PdfFileStorage();
static AudibleFileStorage()
public static string DownloadsInProgress
{
#region init DecryptInProgress
if (!Configuration.Instance.DecryptInProgressEnum.In("WinTemp", "LibationFiles"))
Configuration.Instance.DecryptInProgressEnum = "WinTemp";
var M4bRootDir
= Configuration.Instance.DecryptInProgressEnum == "WinTemp" // else "LibationFiles"
? Configuration.WinTemp
: Configuration.Instance.LibationFiles;
DecryptInProgress = Path.Combine(M4bRootDir, "DecryptInProgress");
Directory.CreateDirectory(DecryptInProgress);
#endregion
get
{
if (!Configuration.Instance.DownloadsInProgressEnum.In("WinTemp", "LibationFiles"))
Configuration.Instance.DownloadsInProgressEnum = "WinTemp";
var AaxRootDir
= Configuration.Instance.DownloadsInProgressEnum == "WinTemp"
? Configuration.WinTemp
: Configuration.Instance.LibationFiles;
#region init DownloadsInProgress
if (!Configuration.Instance.DownloadsInProgressEnum.In("WinTemp", "LibationFiles"))
Configuration.Instance.DownloadsInProgressEnum = "WinTemp";
var AaxRootDir
= Configuration.Instance.DownloadsInProgressEnum == "WinTemp" // else "LibationFiles"
? Configuration.WinTemp
: Configuration.Instance.LibationFiles;
DownloadsInProgress = Path.Combine(AaxRootDir, "DownloadsInProgress");
Directory.CreateDirectory(DownloadsInProgress);
#endregion
return Directory.CreateDirectory(Path.Combine(AaxRootDir, "DownloadsInProgress")).FullName;
}
}
#region init BooksDirectory
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
Configuration.Instance.Books = Path.Combine(Configuration.Instance.LibationFiles, "Books");
Directory.CreateDirectory(Configuration.Instance.Books);
#endregion
public static string DecryptInProgress
{
get
{
if (!Configuration.Instance.DecryptInProgressEnum.In("WinTemp", "LibationFiles"))
Configuration.Instance.DecryptInProgressEnum = "WinTemp";
// must do this in static ctor, not w/inline properties
// static properties init before static ctor so these dir.s would still be null
Audio = new AudibleFileStorage(FileType.Audio, BooksDirectory, "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac");
AAX = new AudibleFileStorage(FileType.AAX, DownloadsFinal, "aax");
PDF = new AudibleFileStorage(FileType.PDF, BooksDirectory, "pdf", "zip");
var M4bRootDir
= Configuration.Instance.DecryptInProgressEnum == "WinTemp"
? Configuration.WinTemp
: Configuration.Instance.LibationFiles;
return Directory.CreateDirectory(Path.Combine(M4bRootDir, "DecryptInProgress")).FullName;
}
}
// not customizable. don't move to config
public static string DownloadsFinal => new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("DownloadsFinal").FullName;
public static string BooksDirectory
{
get
{
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
Configuration.Instance.Books = Path.Combine(Configuration.Instance.LibationFiles, "Books");
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
}
}
#endregion
#region instance
public FileType FileType => (FileType)Value;
public string StorageDirectory => DisplayName;
private IEnumerable<string> extensions_noDots { get; }
private string extAggr { get; }
private AudibleFileStorage(FileType fileType, string storageDirectory, params string[] extensions) : base((int)fileType, storageDirectory)
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
{
extensions_noDots = extensions.Select(ext => ext.Trim('.')).ToList();
extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList();
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
}
@@ -90,30 +91,89 @@ namespace FileManager
/// - a directory name has the product id and an audio file is immediately inside
/// - any audio filename contains the product id
/// </summary>
public bool Exists(string productId)
=> GetPath(productId) != null;
public bool Exists(string productId) => GetPath(productId) != null;
public string GetPath(string productId)
{
{
var cachedFile = FilePathCache.GetPath(productId, FileType);
if (cachedFile != null)
return cachedFile;
}
var cachedFile = FilePathCache.GetPath(productId, FileType);
if (cachedFile != null)
return cachedFile;
var firstOrNull =
var firstOrNull =
Directory
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
.FirstOrDefault(s => Regex.IsMatch(s, $@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase));
if (firstOrNull is null)
return null;
FilePathCache.Upsert(productId, FileType, firstOrNull);
return firstOrNull;
}
public string GetDestDir(string title, string asin)
{
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
var underscoreIndex = title.IndexOf(':');
var titleDir
= underscoreIndex < 4
? title
: title.Substring(0, underscoreIndex);
var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin);
return finalDir;
}
public bool IsFileTypeMatch(FileInfo fileInfo)
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
#endregion
}
public class AudioFileStorage : AudibleFileStorage
{
public const string SKIP_FILE_EXT = "libhack";
public override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac", SKIP_FILE_EXT };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
public AudioFileStorage() : base(FileType.Audio) { }
public string CreateSkipFile(string title, string asin, string contents = null)
{
var destinationDir = GetDestDir(title, asin);
Directory.CreateDirectory(destinationDir);
var path = FileUtility.GetValidFilename(destinationDir, title, SKIP_FILE_EXT, asin);
File.WriteAllText(path, contents ?? string.Empty);
return path;
}
}
public class AaxFileStorage : AudibleFileStorage
{
public override string[] Extensions { get; } = new[] { "aax" };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => DownloadsFinal;
public AaxFileStorage() : base(FileType.AAX) { }
}
public class PdfFileStorage : AudibleFileStorage
{
public override string[] Extensions { get; } = new[] { "pdf", "zip" };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
public PdfFileStorage() : base(FileType.PDF) { }
}
}

View File

@@ -37,18 +37,11 @@ namespace FileManager
public bool FilesExist
=> File.Exists(APPSETTINGS_JSON)
&& File.Exists(SettingsJsonPath)
&& File.Exists(SettingsFilePath)
&& Directory.Exists(LibationFiles)
&& Directory.Exists(Books);
public string SettingsJsonPath => Path.Combine(LibationFiles, "Settings.json");
[Description("Your user-specific key used to decrypt your audible files (*.aax) into audio files you can use anywhere (*.m4b). Leave alone in most cases")]
public string DecryptKey
{
get => persistentDictionary.GetString(nameof(DecryptKey));
set => persistentDictionary.Set(nameof(DecryptKey), value);
}
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
[Description("Location for book storage. Includes destination of newly liberated books")]
public string Books
@@ -90,10 +83,11 @@ namespace FileManager
set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value);
}
public string LocaleCountryCode
[Description("Retain .aax files after decrypting?")]
public bool RetainAaxFiles
{
get => persistentDictionary.GetString(nameof(LocaleCountryCode));
set => persistentDictionary.Set(nameof(LocaleCountryCode), value);
get => persistentDictionary.Get<bool>(nameof(RetainAaxFiles));
set => persistentDictionary.Set(nameof(RetainAaxFiles), value);
}
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
@@ -115,11 +109,11 @@ namespace FileManager
if (wellKnownPaths.ContainsKey(value))
value = wellKnownPaths[value];
// must write here before SettingsJsonPath in next step reads cache
// must write here before SettingsFilePath in next step reads cache
libationFilesPathCache = value;
// load json values into memory. create if not exists
persistentDictionary = new PersistentDictionary(SettingsJsonPath);
persistentDictionary = new PersistentDictionary(SettingsFilePath);
return libationFilesPathCache;
}
@@ -171,7 +165,7 @@ namespace FileManager
// if moving from default, delete old settings file and dir (if empty)
if (LibationFiles.EqualsInsensitive(AppDir))
{
File.Delete(SettingsJsonPath);
File.Delete(SettingsFilePath);
System.Threading.Thread.Sleep(100);
if (!Directory.EnumerateDirectories(AppDir).Any() && !Directory.EnumerateFiles(AppDir).Any())
Directory.Delete(AppDir);

View File

@@ -40,8 +40,13 @@ namespace FileManager
return stringCache[propertyName];
}
public T Get<T>(string propertyName) where T : class
=> GetObject(propertyName) is T obj ? obj : default;
public T Get<T>(string propertyName)
{
var o = GetObject(propertyName);
if (o is null) return default;
if (o is JToken jt) return jt.Value<T>();
return (T)o;
}
public object GetObject(string propertyName)
{

View File

@@ -0,0 +1,103 @@
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 = "";
/// <summary>aka: activation bytes</summary>
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)
{
AccountId = ArgumentValidator.EnsureNotNullOrWhiteSpace(accountId, nameof(accountId)).Trim();
}
public override string ToString() => $"{AccountId} - {Locale?.Name ?? "[empty]"}";
public string MaskedLogEntry => @$"AccountId={mask(AccountId)}|AccountName={mask(AccountName)}|Locale={Locale?.Name ?? "[empty]"}";
private static string mask(string str)
=> str is null ? "[null]"
: str == string.Empty ? "[empty]"
: str.ToMask();
}
}

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApi;
using AudibleApi.Authorization;
using Dinah.Core;
using Newtonsoft.Json;
namespace InternalUtilities
{
// '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)
public class AccountsSettings : IUpdatable
{
public event EventHandler Updated;
private void update(object sender = null, EventArgs e = null)
{
foreach (var account in Accounts)
validate(account);
update_no_validate();
}
private void update_no_validate() => Updated?.Invoke(this, new EventArgs());
public AccountsSettings() { }
// for some reason this will make the json instantiator use _accounts_json.set()
[JsonConstructor]
protected AccountsSettings(List<Account> accountsSettings) { }
#region Accounts
private List<Account> _accounts_backing = new List<Account>();
[JsonProperty(PropertyName = nameof(Accounts))]
private List<Account> _accounts_json
{
get => _accounts_backing;
// 'set' is only used by json deser
set
{
if (value is null)
return;
foreach (var account in value)
_add(account);
update_no_validate();
}
}
[JsonIgnore]
public IReadOnlyList<Account> Accounts => _accounts_json.AsReadOnly();
#endregion
#region de/serialize
public static AccountsSettings FromJson(string json)
=> JsonConvert.DeserializeObject<AccountsSettings>(json, Identity.GetJsonSerializerSettings());
public string ToJson(Formatting formatting = Formatting.Indented)
=> JsonConvert.SerializeObject(this, formatting, Identity.GetJsonSerializerSettings());
#endregion
// more common naming convention alias for internal collection
public IReadOnlyList<Account> GetAll() => Accounts;
public Account Upsert(string accountId, string locale)
{
var acct = GetAccount(accountId, locale);
if (acct != null)
return acct;
var l = Localization.Get(locale);
var id = new Identity(l);
var account = new Account(accountId) { IdentityTokens = id };
Add(account);
return account;
}
public void Add(Account account)
{
_add(account);
update_no_validate();
}
public void _add(Account account)
{
validate(account);
_accounts_backing.Add(account);
account.Updated += update;
}
public Account GetAccount(string accountId, string locale)
{
if (locale is null)
return null;
return Accounts.SingleOrDefault(a => a.AccountId == accountId && a.IdentityTokens.Locale.Name == locale);
}
public bool Delete(string accountId, string locale)
{
var acct = GetAccount(accountId, locale);
if (acct is null)
return false;
return Delete(acct);
}
public bool Delete(Account account)
{
if (!_accounts_backing.Contains(account))
return false;
account.Updated -= update;
var result = _accounts_backing.Remove(account);
update_no_validate();
return result;
}
private void validate(Account account)
{
ArgumentValidator.EnsureNotNull(account, nameof(account));
var accountId = account.AccountId;
var locale = account?.IdentityTokens?.Locale?.Name;
var acct = GetAccount(accountId, locale);
// new: ok
if (acct is null)
return;
// 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

@@ -0,0 +1,12 @@
using System;
using AudibleApiDTOs;
namespace InternalUtilities
{
public class ImportItem
{
public Item DtoItem { get; set; }
public string AccountId { get; set; }
public string LocaleName { get; set; }
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -4,35 +4,64 @@ using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApiDTOs;
using FileManager;
using Dinah.Core;
using Polly;
using Polly.Retry;
namespace InternalUtilities
{
public class AudibleApiActions
public static class AudibleApiActions
{
private AsyncRetryPolicy policy { get; }
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
public static Task<Api> GetApiAsync(string username, string localeName, ILoginCallback loginCallback = null)
{
Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new
{
Username = username.ToMask(),
LocaleName = localeName,
});
return EzApiCreator.GetApiAsync(
Localization.Get(localeName),
AudibleApiStorage.AccountsSettingsFile,
AudibleApiStorage.GetIdentityTokensJsonPath(username, localeName),
loginCallback);
}
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
public static Task<Api> GetApiAsync(ILoginCallback loginCallback, Account account)
{
Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new
{
Account = account?.MaskedLogEntry ?? "[null]",
LocaleName = account?.Locale?.Name
});
return EzApiCreator.GetApiAsync(
account.Locale,
AudibleApiStorage.AccountsSettingsFile,
account.GetIdentityTokensJsonPath(),
loginCallback);
}
private static AsyncRetryPolicy policy { get; }
= Policy.Handle<Exception>()
// 2 retries == 3 total
.RetryAsync(2);
public async Task<List<Item>> GetAllLibraryItemsAsync(ILoginCallback callback)
public static Task<List<Item>> GetLibraryValidatedAsync(Api api)
{
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or tokens are refreshed
// worse, this 1st dummy call doesn't seem to help:
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
return await policy.ExecuteAsync(() => getItemsAsync(callback));
return policy.ExecuteAsync(() => getItemsAsync(api));
}
private async Task<List<Item>> getItemsAsync(ILoginCallback callback)
private static async Task<List<Item>> getItemsAsync(Api api)
{
var api = await EzApiCreator.GetApiAsync(AudibleApiStorage.IdentityTokensFile, callback, Configuration.Instance.LocaleCountryCode);
var items = await AudibleApiExtensions.GetAllLibraryItemsAsync(api);
var items = await api.GetAllLibraryItemsAsync();
// remove episode parents
items.RemoveAll(i => i.IsEpisodes);
// remove episode parents and 'audible plus' check-outs
items.RemoveAll(i => i.IsEpisodes || i.IsNonLibraryAudiblePlus);
#region // episode handling. doesn't quite work
// // add individual/children episodes

View File

@@ -1,55 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApiDTOs;
//
// probably not the best place for this
// but good enough for now
//
namespace InternalUtilities
{
public static class AudibleApiExtensions
{
public static async Task<List<Item>> GetAllLibraryItemsAsync(this Api api)
{
var allItems = new List<Item>();
for (var i = 1; ; i++)
{
var page = await api.GetLibraryAsync(new LibraryOptions
{
NumberOfResultPerPage = 1000,
PageNumber = i,
PurchasedAfter = new DateTime(2000, 1, 1),
ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS
});
var pageStr = page.ToString();
LibraryDtoV10 libResult;
try
{
// important! use this convert method
libResult = LibraryDtoV10.FromJson(pageStr);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error converting library for importing use. Full library:\r\n" + pageStr);
throw;
}
if (!libResult.Items.Any())
break;
else
Serilog.Log.Logger.Information($"Page {i}: {libResult.Items.Length} results");
allItems.AddRange(libResult.Items);
}
return allItems;
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.IO;
using FileManager;
using Newtonsoft.Json;
namespace InternalUtilities
{
public static class AudibleApiStorage
{
public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles, "AccountsSettings.json");
public static void EnsureAccountsSettingsFileExists()
{
// saves. BEWARE: this will overwrite an existing file
if (!File.Exists(AccountsSettingsFile))
_ = new AccountsSettingsPersister(new AccountsSettings(), AccountsSettingsFile);
}
/// <summary>If you use this, be a good citizen and DISPOSE of it</summary>
public static AccountsSettingsPersister GetAccountsSettingsPersister() => new AccountsSettingsPersister(AccountsSettingsFile);
public static string GetIdentityTokensJsonPath(this Account account)
=> GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name);
public static string GetIdentityTokensJsonPath(string username, string localeName)
{
var usernameSanitized = trimSurroundingQuotes(JsonConvert.ToString(username));
var localeNameSanitized = trimSurroundingQuotes(JsonConvert.ToString(localeName));
return $"$.Accounts[?(@.AccountId == '{usernameSanitized}' && @.IdentityTokens.LocaleName == '{localeNameSanitized}')].IdentityTokens";
}
private static string trimSurroundingQuotes(string str)
{
// SubString algo is better than .Trim("\"")
// orig string "
// json string "\""
// Eg:
// => str.Trim("\"")
// output \
// vs
// => str.Substring(1, str.Length - 2)
// output \"
// also works with surrounding single quotes
return str.Substring(1, str.Length - 2);
}
}
}

View File

@@ -51,9 +51,6 @@ namespace InternalUtilities
if (distinct.Any(s => s.CategoryName is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Categories)} with null {nameof(Ladder.CategoryName)}", nameof(items)));
if (items.GetCategoryPairsDistinct().Any(p => p.Length > 2))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Categories)} with wrong number of categories. Expecting 0, 1, or 2 categories per title", nameof(items)));
return exceptions;
}
}

View File

@@ -10,13 +10,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solutio
REFERENCE.txt = REFERENCE.txt
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3.2 Domain Utilities (database aware)", "3.2 Domain Utilities (database aware)", "{41CDCC73-9B81-49DD-9570-C54406E852AF}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5 Domain Utilities (db aware)", "5 Domain Utilities (db aware)", "{41CDCC73-9B81-49DD-9570-C54406E852AF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4 Application", "4 Application", "{8679CAC8-9164-4007-BDD2-F004810EDA14}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "6 Application", "6 Application", "{8679CAC8-9164-4007-BDD2-F004810EDA14}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1 Core Libraries", "1 Core Libraries", "{43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 Domain", "3 Domain", "{751093DD-5DBA-463E-ADBE-E05FAFB6983E}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4 Domain (db)", "4 Domain (db)", "{751093DD-5DBA-463E-ADBE-E05FAFB6983E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2 Utilities (domain ignorant)", "2 Utilities (domain ignorant)", "{7FBBB086-0807-4998-85BF-6D1A49C8AD05}"
EndProject
@@ -30,7 +30,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator", "FileLibera
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities", "InternalUtilities\InternalUtilities.csproj", "{06882742-27A6-4347-97D9-56162CEC9C11}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3.1 Domain Internal Utilities (db ignorant)", "3.1 Domain Internal Utilities (db ignorant)", "{F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 Domain Internal Utilities (db ignorant)", "3 Domain Internal Utilities (db ignorant)", "{F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine", "LibationSearchEngine\LibationSearchEngine.csproj", "{2E1F5DB4-40CC-4804-A893-5DCE0193E598}"
EndProject
@@ -80,7 +80,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.WindowsDesktop",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsDesktopUtilities", "WindowsDesktopUtilities\WindowsDesktopUtilities.csproj", "{E7EFD64D-6630-4426-B09C-B6862A92E3FD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibationLauncher", "LibationLauncher\LibationLauncher.csproj", "{F3B04A3A-20C8-4582-A54A-715AF6A5D859}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationLauncher", "LibationLauncher\LibationLauncher.csproj", "{F3B04A3A-20C8-4582-A54A-715AF6A5D859}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Libation Tests", "0 Libation Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -200,6 +204,10 @@ Global
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.Build.0 = Release|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -233,6 +241,7 @@ Global
{059CE32C-9AD6-45E9-A166-790DFFB0B730} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
{E7EFD64D-6630-4426-B09C-B6862A92E3FD} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
{F3B04A3A-20C8-4582-A54A-715AF6A5D859} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>libation.ico</ApplicationIcon>
<AssemblyName>Libation</AssemblyName>
@@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>3.1.2.0</Version>
<Version>4.1.8.1</Version>
</PropertyGroup>
<ItemGroup>
@@ -21,7 +21,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Octokit" Version="0.36.0" />
<PackageReference Include="Octokit" Version="0.48.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -2,10 +2,13 @@ using System;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using AudibleApi;
using FileManager;
using InternalUtilities;
using LibationWinForms;
using LibationWinForms.Dialogs;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
@@ -22,6 +25,11 @@ namespace LibationLauncher
createSettings();
AudibleApiStorage.EnsureAccountsSettingsFileExists();
migrate_to_v4_0_0();
migrate_to_v4_0_3(); // add setting for whether to delete/retain aax
ensureLoggingConfig();
ensureSerilogConfig();
configureLogging();
@@ -33,6 +41,11 @@ namespace LibationLauncher
private static void createSettings()
{
static bool configSetupIsComplete(Configuration config)
=> config.FilesExist
&& !string.IsNullOrWhiteSpace(config.DownloadsInProgressEnum)
&& !string.IsNullOrWhiteSpace(config.DecryptInProgressEnum);
var config = Configuration.Instance;
if (configSetupIsComplete(config))
return;
@@ -42,8 +55,6 @@ namespace LibationLauncher
var setupDialog = new SetupDialog();
setupDialog.NoQuestionsBtn_Click += (_, __) =>
{
config.DecryptKey ??= "";
config.LocaleCountryCode ??= "us";
config.DownloadsInProgressEnum ??= "WinTemp";
config.DecryptInProgressEnum ??= "WinTemp";
config.Books ??= Configuration.AppDir;
@@ -70,11 +81,151 @@ namespace LibationLauncher
Environment.Exit(0);
}
private static bool configSetupIsComplete(Configuration config)
=> config.FilesExist
&& !string.IsNullOrWhiteSpace(config.LocaleCountryCode)
&& !string.IsNullOrWhiteSpace(config.DownloadsInProgressEnum)
&& !string.IsNullOrWhiteSpace(config.DecryptInProgressEnum);
#region v3 => v4 migration
static string AccountsSettingsFileLegacy30 => Path.Combine(Configuration.Instance.LibationFiles, "IdentityTokens.json");
private static void migrate_to_v4_0_0()
{
migrateLegacyIdentityFile();
updateSettingsFile();
}
private static void migrateLegacyIdentityFile()
{
if (File.Exists(AccountsSettingsFileLegacy30))
{
// don't always rely on applicable POCOs. some is legacy and must be: json file => JObject
try
{
updateLegacyFileWithLocale();
var account = createAccountFromLegacySettings();
account.DecryptKey = getDecryptKey(account);
// the next few methods need persistence. to be a good citizen, dispose of persister at the end of current scope
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
persister.AccountsSettings.Add(account);
}
// migration is a convenience. if something goes wrong: just move on
catch { }
// delete legacy token file
File.Delete(AccountsSettingsFileLegacy30);
}
}
private static void updateLegacyFileWithLocale()
{
var legacyContents = File.ReadAllText(AccountsSettingsFileLegacy30);
var legacyJObj = JObject.Parse(legacyContents);
// attempt to update legacy token file with locale from settings
if (!legacyJObj.ContainsKey("LocaleName"))
{
var settings = File.ReadAllText(Configuration.Instance.SettingsFilePath);
var settingsJObj = JObject.Parse(settings);
if (settingsJObj.TryGetValue("LocaleCountryCode", out var localeName))
{
// update legacy token file with locale from settings
legacyJObj.AddFirst(new JProperty("LocaleName", localeName.Value<string>()));
// save
var newContents = legacyJObj.ToString(Formatting.Indented);
File.WriteAllText(AccountsSettingsFileLegacy30, newContents);
}
}
}
private static Account createAccountFromLegacySettings()
{
// get required locale from settings file
var settingsContents = File.ReadAllText(Configuration.Instance.SettingsFilePath);
if (!JObject.Parse(settingsContents).TryGetValue("LocaleCountryCode", out var jLocale))
return null;
var localeName = jLocale.Value<string>();
var locale = Localization.Get(localeName);
var api = EzApiCreator.GetApiAsync(locale, AccountsSettingsFileLegacy30).GetAwaiter().GetResult();
var email = api.GetEmailAsync().GetAwaiter().GetResult();
// identity has likely been updated above. re-get contents
var legacyContents = File.ReadAllText(AccountsSettingsFileLegacy30);
var identity = AudibleApi.Authorization.Identity.FromJson(legacyContents);
if (!identity.IsValid)
return null;
var account = new Account(email)
{
AccountName = $"{email} - {locale.Name}",
LibraryScan = true,
IdentityTokens = identity
};
return account;
}
private static string getDecryptKey(Account account)
{
if (!string.IsNullOrWhiteSpace(account?.DecryptKey))
return account.DecryptKey;
if (!File.Exists(Configuration.Instance.SettingsFilePath) || account is null)
return "";
var settingsContents = File.ReadAllText(Configuration.Instance.SettingsFilePath);
if (JObject.Parse(settingsContents).TryGetValue("DecryptKey", out var jToken))
return jToken.Value<string>() ?? "";
return "";
}
private static void updateSettingsFile()
{
if (!File.Exists(Configuration.Instance.SettingsFilePath))
return;
// use JObject to remove decrypt key and locale from Settings.json
var settingsContents = File.ReadAllText(Configuration.Instance.SettingsFilePath);
var jObj = JObject.Parse(settingsContents);
var jLocale = jObj.Property("LocaleCountryCode");
var jDecryptKey = jObj.Property("DecryptKey");
jDecryptKey?.Remove();
jLocale?.Remove();
if (jDecryptKey != null || jLocale != null)
{
var newContents = jObj.ToString(Formatting.Indented);
File.WriteAllText(Configuration.Instance.SettingsFilePath, newContents);
}
}
#endregion
#region migrate_to_v4_0_3 add setting for whether to delete/retain aax
private static void migrate_to_v4_0_3()
{
if (!File.Exists(Configuration.Instance.SettingsFilePath))
return;
// use JObject to remove decrypt key and locale from Settings.json
var settingsContents = File.ReadAllText(Configuration.Instance.SettingsFilePath);
var jObj = JObject.Parse(settingsContents);
var jRetainAaxFiles = jObj.Property("RetainAaxFiles");
if (jRetainAaxFiles is null)
{
jObj.Add("RetainAaxFiles", false);
var newContents = jObj.ToString(Formatting.Indented);
File.WriteAllText(Configuration.Instance.SettingsFilePath, newContents);
}
}
#endregion
private static string defaultLoggingLevel { get; } = "Information";
private static void ensureLoggingConfig()
@@ -169,11 +320,11 @@ namespace LibationLauncher
// CONFIGURATION-DRIVEN (json)
var configuration = new ConfigurationBuilder()
.AddJsonFile(config.SettingsJsonPath)
.AddJsonFile(config.SettingsFilePath)
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
.ReadFrom.Configuration(configuration)
.CreateLogger();
//// MANUAL HARD CODED
//Log.Logger = new LoggerConfiguration()
@@ -244,10 +395,26 @@ namespace LibationLauncher
private static void logStartupState()
{
Log.Logger.Information("Begin Libation");
Log.Logger.Information($"Version: {BuildVersion}");
Log.Logger.Information($"LibationFiles: {Configuration.Instance.LibationFiles}");
Log.Logger.Information($"Audible locale: {Configuration.Instance.LocaleCountryCode}");
var config = Configuration.Instance;
Log.Logger.Information("Begin Libation. {@DebugInfo}", new
{
Version = BuildVersion.ToString(),
config.LibationFiles,
AudibleFileStorage.BooksDirectory,
config.DownloadsInProgressEnum,
DownloadsInProgressDir = AudibleFileStorage.DownloadsInProgress,
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgress).Count(),
AudibleFileStorage.DownloadsFinal,
DownloadsFinalFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsFinal).Count(),
config.DecryptInProgressEnum,
DecryptInProgressDir = AudibleFileStorage.DecryptInProgress,
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(),
});
}
private static Version BuildVersion => System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -72,7 +72,12 @@ namespace LibationSearchEngine
["CategoriesId"] = lb => lb.Book.CategoriesIds == null ? null : string.Join(", ", lb.Book.CategoriesIds),
["CategoryId"] = lb => lb.Book.CategoriesIds == null ? null : string.Join(", ", lb.Book.CategoriesIds),
[TAGS.FirstCharToUpper()] = lb => lb.Book.UserDefinedItem.Tags
[TAGS.FirstCharToUpper()] = lb => lb.Book.UserDefinedItem.Tags,
["Locale"] = lb => lb.Book.Locale,
["Region"] = lb => lb.Book.Locale,
["Account"] = lb => lb.Account,
["Email"] = lb => lb.Account
}
);
@@ -105,14 +110,24 @@ namespace LibationSearchEngine
["HasPDF"] = lb => lb.Book.Supplements.Any(),
["PDFs"] = lb => lb.Book.Supplements.Any(),
["PDF"] = lb => lb.Book.Supplements.Any(),
["IsRated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
["Rated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
["IsAuthorNarrated"] = lb => lb.Book.Authors.Intersect(lb.Book.Narrators).Any(),
["AuthorNarrated"] = lb => lb.Book.Authors.Intersect(lb.Book.Narrators).Any(),
["IsAuthorNarrated"] = lb => isAuthorNarrated(lb),
["AuthorNarrated"] = lb => isAuthorNarrated(lb),
[nameof(Book.IsAbridged)] = lb => lb.Book.IsAbridged,
["Abridged"] = lb => lb.Book.IsAbridged,
});
private static bool isAuthorNarrated(LibraryBook lb)
{
var authors = lb.Book.Authors.Select(a => a.Name).ToArray();
var narrators = lb.Book.Narrators.Select(a => a.Name).ToArray();
return authors.Intersect(narrators).Any();
}
// use these common fields in the "all" default search field
private static IEnumerable<Func<LibraryBook, string>> allFieldIndexRules { get; }
= new List<Func<LibraryBook, string>>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>libation.ico</ApplicationIcon>
@@ -23,27 +23,27 @@
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="UNTESTED\Dialogs\LibationFilesDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="UNTESTED\Dialogs\LibationFilesDialog.cs" />
<Compile Update="UNTESTED\Dialogs\LibationFilesDialog.Designer.cs">
<DependentUpon>LibationFilesDialog.cs</DependentUpon>
</Compile>
<Compile Update="UNTESTED\Dialogs\SettingsDialog.cs">
<SubType>Form</SubType>
<Compile Update="UNTESTED\Dialogs\Login\ApprovalNeededDialog.cs" />
<Compile Update="UNTESTED\Dialogs\Login\ApprovalNeededDialog.Designer.cs">
<DependentUpon>ApprovalNeededDialog.cs</DependentUpon>
</Compile>
<Compile Update="UNTESTED\Dialogs\ScanAccountsDialog.cs" />
<Compile Update="UNTESTED\Dialogs\ScanAccountsDialog.Designer.cs">
<DependentUpon>ScanAccountsDialog.cs</DependentUpon>
</Compile>
<Compile Update="UNTESTED\Dialogs\SettingsDialog.cs" />
<Compile Update="UNTESTED\Dialogs\SettingsDialog.Designer.cs">
<DependentUpon>SettingsDialog.cs</DependentUpon>
</Compile>
<Compile Update="UNTESTED\Dialogs\IndexLibraryDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="UNTESTED\Dialogs\IndexLibraryDialog.cs" />
<Compile Update="UNTESTED\Dialogs\IndexLibraryDialog.Designer.cs">
<DependentUpon>IndexLibraryDialog.cs</DependentUpon>
</Compile>
<Compile Update="UNTESTED\Dialogs\SetupDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="UNTESTED\Dialogs\SetupDialog.cs" />
<Compile Update="UNTESTED\Dialogs\SetupDialog.Designer.cs">
<DependentUpon>SetupDialog.cs</DependentUpon>
</Compile>
@@ -57,6 +57,9 @@
<EmbeddedResource Update="UNTESTED\Dialogs\LibationFilesDialog.resx">
<DependentUpon>LibationFilesDialog.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Update="UNTESTED\Dialogs\ScanAccountsDialog.resx">
<DependentUpon>ScanAccountsDialog.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Update="UNTESTED\Dialogs\SettingsDialog.resx">
<DependentUpon>SettingsDialog.cs</DependentUpon>
</EmbeddedResource>

View File

@@ -24,24 +24,13 @@ namespace LibationWinForms.BookLiberation
InitializeComponent();
}
public void AppendError(Exception ex)
{
Serilog.Log.Logger.Error(ex, "Automated backup: error");
appendText("ERROR: " + ex.Message);
}
public void AppendText(string text)
{
Serilog.Log.Logger.Information($"Automated backup: {text}");
appendText(text);
}
private void appendText(string text)
public void WriteLine(string text)
=> logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
public void FinalizeUI()
{
keepGoingCb.Enabled = false;
logTb.AppendText("");
AppendText("DONE");
}
private void AutomatedBackupsForm_FormClosing(object sender, FormClosingEventArgs e) => keepGoingCb.Checked = false;

View File

@@ -17,15 +17,15 @@ namespace LibationWinForms.BookLiberation
// thread-safe UI updates
public void UpdateFilename(string title) => filenameLbl.UIThread(() => filenameLbl.Text = title);
public void DownloadProgressChanged(long BytesReceived, long TotalBytesToReceive)
public void DownloadProgressChanged(long BytesReceived, long? TotalBytesToReceive)
{
// this won't happen with download file. it will happen with download string
if (TotalBytesToReceive < 0)
if (!TotalBytesToReceive.HasValue || TotalBytesToReceive.Value <= 0)
return;
progressLbl.UIThread(() => progressLbl.Text = $"{BytesReceived:#,##0} of {TotalBytesToReceive:#,##0}");
progressLbl.UIThread(() => progressLbl.Text = $"{BytesReceived:#,##0} of {TotalBytesToReceive.Value:#,##0}");
var d = double.Parse(BytesReceived.ToString()) / double.Parse(TotalBytesToReceive.ToString()) * 100.0;
var d = double.Parse(BytesReceived.ToString()) / double.Parse(TotalBytesToReceive.Value.ToString()) * 100.0;
var i = int.Parse(Math.Truncate(d).ToString());
progressBar1.UIThread(() => progressBar1.Value = i);

View File

@@ -1,30 +1,69 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core.ErrorHandling;
using FileLiberator;
namespace LibationWinForms.BookLiberation
{
// decouple serilog and form. include convenience factory method
public class LogMe
{
public event EventHandler<string> LogInfo;
public event EventHandler<string> LogErrorString;
public event EventHandler<(Exception, string)> LogError;
public static LogMe RegisterForm(AutomatedBackupsForm form)
{
var logMe = new LogMe();
logMe.LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
logMe.LogInfo += (_, text) => form.WriteLine(text);
logMe.LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
logMe.LogErrorString += (_, text) => form.WriteLine(text);
logMe.LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
logMe.LogError += (_, tuple) =>
{
form.WriteLine(tuple.Item2 ?? "Automated backup: error");
form.WriteLine("ERROR: " + tuple.Item1.Message);
};
return logMe;
}
public void Info(string text) => LogInfo?.Invoke(this, text);
public void Error(string text) => LogErrorString?.Invoke(this, text);
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
}
public static class ProcessorAutomationController
{
public static async Task BackupSingleBookAsync(string productId, EventHandler<LibraryBook> completedAction = null)
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupSingleBookAsync) + " {@DebugInfo}", new { productId });
var backupBook = getWiredUpBackupBook(completedAction);
var automatedBackupsForm = attachToBackupsForm(backupBook);
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(backupBook);
automatedBackupsForm.KeepGoingVisible = false;
await runSingleBackupAsync(backupBook, automatedBackupsForm, productId);
var libraryBook = IProcessableExt.GetSingleLibraryBook(productId);
// continue even if libraryBook is null. we'll display even that in the processing box
await new BackupSingle(logMe, backupBook, automatedBackupsForm, libraryBook).RunBackupAsync();
}
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
var backupBook = getWiredUpBackupBook(completedAction);
var automatedBackupsForm = attachToBackupsForm(backupBook);
await runBackupLoopAsync(backupBook, automatedBackupsForm);
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(backupBook);
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
}
private static BackupBook getWiredUpBackupBook(EventHandler<LibraryBook> completedAction)
@@ -33,7 +72,7 @@ namespace LibationWinForms.BookLiberation
backupBook.DownloadBook.Begin += (_, __) => wireUpEvents(backupBook.DownloadBook);
backupBook.DecryptBook.Begin += (_, __) => wireUpEvents(backupBook.DecryptBook);
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
if (completedAction != null)
{
@@ -45,22 +84,23 @@ namespace LibationWinForms.BookLiberation
return backupBook;
}
private static AutomatedBackupsForm attachToBackupsForm(BackupBook backupBook)
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(BackupBook backupBook)
{
#region create form
#region create form and logger
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
#endregion
#region define how model actions will affect form behavior
void downloadBookBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Download Step, Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => automatedBackupsForm.AppendText("- " + str);
void downloadBookCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Download Step, Completed: {libraryBook.Book}");
void decryptBookBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Decrypt Step, Begin: {libraryBook.Book}");
void downloadBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Download Step, Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => logMe.Info("- " + str);
void downloadBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Download Step, Completed: {libraryBook.Book}");
void decryptBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Begin: {libraryBook.Book}");
// extra line after book is completely finished
void decryptBookCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
void downloadPdfBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"PDF Step, Begin: {libraryBook.Book}");
void decryptBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
void downloadPdfBegin(object _, LibraryBook libraryBook) => logMe.Info($"PDF Step, Begin: {libraryBook.Book}");
// extra line after book is completely finished
void downloadPdfCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"PDF Step, Completed: {libraryBook.Book}{Environment.NewLine}");
void downloadPdfCompleted(object _, LibraryBook libraryBook) => logMe.Info($"PDF Step, Completed: {libraryBook.Book}{Environment.NewLine}");
#endregion
#region subscribe new form to model's events
@@ -91,16 +131,17 @@ namespace LibationWinForms.BookLiberation
};
#endregion
return automatedBackupsForm;
return (automatedBackupsForm, logMe);
}
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
var downloadPdf = getWiredUpDownloadPdf(completedAction);
var automatedBackupsForm = attachToBackupsForm(downloadPdf);
await runBackupLoopAsync(downloadPdf, automatedBackupsForm);
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf);
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
}
private static DownloadPdf getWiredUpDownloadPdf(EventHandler<LibraryBook> completedAction)
@@ -126,7 +167,7 @@ namespace LibationWinForms.BookLiberation
downloadDialog.UpdateFilename(str);
downloadDialog.Show();
};
downloadFile.DownloadProgressChanged += (_, progress) => downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive.Value);
downloadFile.DownloadProgressChanged += (_, progress) => downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive);
downloadFile.DownloadCompleted += (_, __) => downloadDialog.Close();
await downloadFile.PerformDownloadFileAsync(url, destination);
@@ -161,7 +202,7 @@ namespace LibationWinForms.BookLiberation
void fileDownloadCompleted(object _, string __) => downloadDialog.Close();
void downloadProgressChanged(object _, Dinah.Core.Net.Http.DownloadProgress progress)
=> downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive.Value);
=> downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive);
void unsubscribe(object _ = null, EventArgs __ = null)
{
@@ -244,17 +285,18 @@ namespace LibationWinForms.BookLiberation
#endregion
}
private static AutomatedBackupsForm attachToBackupsForm(IDownloadableProcessable downloadable)
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(IDownloadableProcessable downloadable)
{
#region create form
#region create form and logger
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
#endregion
#region define how model actions will affect form behavior
void begin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => automatedBackupsForm.AppendText("- " + str);
void begin(object _, LibraryBook libraryBook) => logMe.Info($"Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => logMe.Info("- " + str);
// extra line after book is completely finished
void completed(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Completed: {libraryBook.Book}{Environment.NewLine}");
void completed(object _, LibraryBook libraryBook) => logMe.Info($"Completed: {libraryBook.Book}{Environment.NewLine}");
#endregion
#region subscribe new form to model's events
@@ -273,73 +315,150 @@ namespace LibationWinForms.BookLiberation
};
#endregion
return automatedBackupsForm;
return (automatedBackupsForm, logMe);
}
}
abstract class BackupRunner
{
protected LogMe LogMe { get; }
protected IProcessable Processable { get; }
protected AutomatedBackupsForm AutomatedBackupsForm { get; }
protected BackupRunner(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
{
LogMe = logMe;
Processable = processable;
AutomatedBackupsForm = automatedBackupsForm;
}
// automated backups looper feels like a composible IProcessable: logic, UI, begin + process child + end
// however the process step doesn't follow the pattern: Validate(product) + Process(product)
private static async Task runBackupLoopAsync(IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
protected abstract Task RunAsync();
protected abstract string SkipDialogText { get; }
protected abstract MessageBoxButtons SkipDialogButtons { get; }
protected abstract DialogResult CreateSkipFileResult { get; }
public async Task RunBackupAsync()
{
automatedBackupsForm.Show();
AutomatedBackupsForm.Show();
try
{
var shouldContinue = true;
while (shouldContinue)
{
var statusHandler = await processable.ProcessFirstValidAsync();
shouldContinue = validateStatus(statusHandler, automatedBackupsForm);
}
await RunAsync();
}
catch (Exception ex)
{
automatedBackupsForm.AppendError(ex);
LogMe.Error(ex);
}
automatedBackupsForm.FinalizeUI();
AutomatedBackupsForm.FinalizeUI();
LogMe.Info("DONE");
}
private static async Task runSingleBackupAsync(IProcessable processable, AutomatedBackupsForm automatedBackupsForm, string productId)
protected async Task<bool> ProcessOneAsync(Func<LibraryBook, Task<StatusHandler>> func, LibraryBook libraryBook)
{
automatedBackupsForm.Show();
string logMessage;
try
{
var statusHandler = await processable.ProcessSingleAsync(productId);
validateStatus(statusHandler, automatedBackupsForm);
}
catch (Exception ex)
{
automatedBackupsForm.AppendError(ex);
}
var statusHandler = await func(libraryBook);
automatedBackupsForm.FinalizeUI();
}
if (statusHandler.IsSuccess)
return true;
private static bool validateStatus(StatusHandler statusHandler, AutomatedBackupsForm automatedBackupsForm)
{
if (statusHandler == null)
{
automatedBackupsForm.AppendText("Done. All books have been processed");
return false;
}
if (statusHandler.HasErrors)
{
automatedBackupsForm.AppendText("ERROR. All books have not been processed. Most recent valid book: processing failed");
foreach (var errorMessage in statusHandler.Errors)
automatedBackupsForm.AppendText(errorMessage);
return false;
LogMe.Error(errorMessage);
logMessage = statusHandler.Errors.Aggregate((a, b) => $"{a}\r\n{b}");
}
catch (Exception ex)
{
LogMe.Error(ex);
logMessage = ex.Message + "\r\n|\r\n" + ex.StackTrace;
}
if (!automatedBackupsForm.KeepGoing)
{
if (automatedBackupsForm.KeepGoingVisible && !automatedBackupsForm.KeepGoingChecked)
automatedBackupsForm.AppendText("'Keep going' is unchecked");
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
var dialogResult = MessageBox.Show(SkipDialogText, "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question);
if (dialogResult == DialogResult.Abort)
return false;
if (dialogResult == CreateSkipFileResult)
{
var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage);
LogMe.Info($@"
Created new 'skip' file
[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}
{path}
".Trim());
}
return true;
}
}
class BackupSingle : BackupRunner
{
private LibraryBook _libraryBook { get; }
protected override string SkipDialogText => @"
An error occurred while trying to process this book. Skip this book permanently?
- Click YES to skip this book permanently.
- Click NO to skip the book this time only. We'll try again later.
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
protected override DialogResult CreateSkipFileResult => DialogResult.Yes;
public BackupSingle(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm, LibraryBook libraryBook)
: base(logMe, processable, automatedBackupsForm)
{
_libraryBook = libraryBook;
}
protected override async Task RunAsync()
{
if (_libraryBook is not null)
await ProcessOneAsync(Processable.ProcessSingleAsync, _libraryBook);
}
}
class BackupLoop : BackupRunner
{
protected override string SkipDialogText => @"
An error occurred while trying to process this book
- ABORT: stop processing books.
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
protected override DialogResult CreateSkipFileResult => DialogResult.Ignore;
public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
: base(logMe, processable, automatedBackupsForm) { }
protected override async Task RunAsync()
{
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
foreach (var libraryBook in Processable.GetValidLibraryBooks())
{
var keepGoing = await ProcessOneAsync(Processable.ProcessBookAsync_NoValidation, libraryBook);
if (!keepGoing)
return;
if (!AutomatedBackupsForm.KeepGoing)
{
if (AutomatedBackupsForm.KeepGoingVisible && !AutomatedBackupsForm.KeepGoingChecked)
LogMe.Info("'Keep going' is unchecked");
return;
}
}
LogMe.Info("Done. All books have been processed");
}
}
}

View File

@@ -0,0 +1,146 @@
namespace LibationWinForms.Dialogs
{
partial class AccountsDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.cancelBtn = new System.Windows.Forms.Button();
this.saveBtn = new System.Windows.Forms.Button();
this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn();
this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn();
this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Locale = new System.Windows.Forms.DataGridViewComboBoxColumn();
this.AccountName = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.SuspendLayout();
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(713, 415);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(75, 23);
this.cancelBtn.TabIndex = 2;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(612, 415);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(75, 23);
this.saveBtn.TabIndex = 1;
this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
//
// dataGridView1
//
this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.dataGridView1.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells;
this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.DeleteAccount,
this.LibraryScan,
this.AccountId,
this.Locale,
this.AccountName});
this.dataGridView1.Location = new System.Drawing.Point(12, 12);
this.dataGridView1.MultiSelect = false;
this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.Size = new System.Drawing.Size(776, 397);
this.dataGridView1.TabIndex = 0;
this.dataGridView1.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView1_CellContentClick);
this.dataGridView1.DefaultValuesNeeded += new System.Windows.Forms.DataGridViewRowEventHandler(this.dataGridView1_DefaultValuesNeeded);
//
// DeleteAccount
//
this.DeleteAccount.HeaderText = "Delete";
this.DeleteAccount.Name = "DeleteAccount";
this.DeleteAccount.ReadOnly = true;
this.DeleteAccount.Text = "x";
this.DeleteAccount.Width = 44;
//
// LibraryScan
//
this.LibraryScan.HeaderText = "Include in library scan?";
this.LibraryScan.Name = "LibraryScan";
this.LibraryScan.Width = 83;
//
// AccountId
//
this.AccountId.HeaderText = "Audible email/login";
this.AccountId.Name = "AccountId";
this.AccountId.Width = 111;
//
// Locale
//
this.Locale.HeaderText = "Locale";
this.Locale.Name = "Locale";
this.Locale.Width = 45;
//
// AccountName
//
this.AccountName.HeaderText = "Account nickname (optional)";
this.AccountName.Name = "AccountName";
this.AccountName.Width = 152;
//
// AccountsDialog
//
this.AcceptButton = this.saveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Controls.Add(this.dataGridView1);
this.Controls.Add(this.saveBtn);
this.Controls.Add(this.cancelBtn);
this.Name = "AccountsDialog";
this.Text = "Audible Accounts";
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.DataGridView dataGridView1;
private System.Windows.Forms.DataGridViewButtonColumn DeleteAccount;
private System.Windows.Forms.DataGridViewCheckBoxColumn LibraryScan;
private System.Windows.Forms.DataGridViewTextBoxColumn AccountId;
private System.Windows.Forms.DataGridViewComboBoxColumn Locale;
private System.Windows.Forms.DataGridViewTextBoxColumn AccountName;
}
}

View File

@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using AudibleApi;
using InternalUtilities;
namespace LibationWinForms.Dialogs
{
public partial class AccountsDialog : Form
{
const string COL_Delete = nameof(DeleteAccount);
const string COL_LibraryScan = nameof(LibraryScan);
const string COL_AccountId = nameof(AccountId);
const string COL_AccountName = nameof(AccountName);
const string COL_Locale = nameof(Locale);
Form1 _parent { get; }
public AccountsDialog(Form1 parent)
{
_parent = parent;
InitializeComponent();
dataGridView1.Columns[COL_AccountName].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
populateDropDown();
populateGridValues();
}
private void populateDropDown()
=> (dataGridView1.Columns[COL_Locale] as DataGridViewComboBoxColumn).DataSource
= Localization.Locales
.Select(l => l.Name)
.OrderBy(a => a).ToList();
private void populateGridValues()
{
// WARNING: accounts persister will write ANY EDIT to object immediately to file
// here: copy strings and dispose of persister
// only persist in 'save' step
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
if (!accounts.Any())
return;
foreach (var account in accounts)
dataGridView1.Rows.Add(
"X",
account.LibraryScan,
account.AccountId,
account.Locale.Name,
account.AccountName);
}
private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e)
{
e.Row.Cells[COL_Delete].Value = "X";
e.Row.Cells[COL_LibraryScan].Value = true;
}
private void DataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
var dgv = (DataGridView)sender;
var col = dgv.Columns[e.ColumnIndex];
if (col is DataGridViewButtonColumn && e.RowIndex >= 0)
{
var row = dgv.Rows[e.RowIndex];
switch (col.Name)
{
case COL_Delete:
// if final/edit row: do nothing
if (e.RowIndex < dgv.RowCount - 1)
dgv.Rows.Remove(row);
break;
//case COL_MoveUp:
// // if top: do nothing
// if (e.RowIndex < 1)
// break;
// dgv.Rows.Remove(row);
// dgv.Rows.Insert(e.RowIndex - 1, row);
// break;
//case COL_MoveDown:
// // if final/edit row or bottom filter row: do nothing
// if (e.RowIndex >= dgv.RowCount - 2)
// break;
// dgv.Rows.Remove(row);
// dgv.Rows.Insert(e.RowIndex + 1, row);
// break;
}
}
}
private void cancelBtn_Click(object sender, EventArgs e) => this.Close();
class AccountDto
{
public string AccountId { get; set; }
public string AccountName { get; set; }
public string LocaleName { get; set; }
public bool LibraryScan { get; set; }
}
private void saveBtn_Click(object sender, EventArgs e)
{
try
{
// without transaction, accounts persister will write ANY EDIT immediately to file
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
persister.BeginTransation();
persist(persister.AccountsSettings);
persister.CommitTransation();
_parent.RefreshImportMenu();
this.DialogResult = DialogResult.OK;
this.Close();
}
catch (Exception ex)
{
MessageBox.Show($"Error: {ex.Message}", "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void persist(AccountsSettings accountsSettings)
{
var existingAccounts = accountsSettings.Accounts;
var dtos = getRowDtos();
// 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.
// these will be caught below by normal means and re-created minus the convenience of persisting identity tokens
// delete
for (var i = existingAccounts.Count - 1; i >= 0; i--)
{
var existing = existingAccounts[i];
if (!dtos.Any(dto =>
dto.AccountId?.ToLower().Trim() == existing.AccountId.ToLower()
&& dto.LocaleName == existing.Locale?.Name))
{
accountsSettings.Delete(existing);
}
}
// upsert each. validation occurs through Account and AccountsSettings
foreach (var dto in dtos)
{
if (string.IsNullOrWhiteSpace(dto.AccountId))
throw new Exception("Please enter an account id for all accounts");
if (string.IsNullOrWhiteSpace(dto.LocaleName))
throw new Exception("Please select a locale (i.e.: country or region) for all accounts");
var acct = accountsSettings.Upsert(dto.AccountId, dto.LocaleName);
acct.LibraryScan = dto.LibraryScan;
acct.AccountName
= string.IsNullOrWhiteSpace(dto.AccountName)
? $"{dto.AccountId} - {dto.LocaleName}"
: dto.AccountName.Trim();
}
}
private List<AccountDto> getRowDtos()
=> dataGridView1.Rows
.Cast<DataGridViewRow>()
.Where(r => !r.IsNewRow)
.Select(r => new AccountDto
{
AccountId = (string)r.Cells[COL_AccountId].Value,
AccountName = (string)r.Cells[COL_AccountName].Value,
LocaleName = (string)r.Cells[COL_Locale].Value,
LibraryScan = (bool)r.Cells[COL_LibraryScan].Value
})
.ToList();
}
}

View File

@@ -117,4 +117,22 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="Original.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="DeleteAccount.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="LibraryScan.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="AccountId.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="AccountName.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Locale.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
</root>

View File

@@ -49,7 +49,7 @@
this.cancelBtn.TabIndex = 2;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.CancelBtn_Click);
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// saveBtn
//
@@ -60,7 +60,7 @@
this.saveBtn.TabIndex = 1;
this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.SaveBtn_Click);
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
//
// dataGridView1
//
@@ -81,6 +81,7 @@
this.dataGridView1.Size = new System.Drawing.Size(776, 397);
this.dataGridView1.TabIndex = 0;
this.dataGridView1.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView1_CellContentClick);
this.dataGridView1.DefaultValuesNeeded += new System.Windows.Forms.DataGridViewRowEventHandler(this.dataGridView1_DefaultValuesNeeded);
//
// Original
//

View File

@@ -1,23 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using FileManager;
namespace LibationWinForms.Dialogs
{
public partial class EditQuickFilters : Form
public partial class EditQuickFilters : Form
{
const string COL_Original = "Original";
const string COL_Delete = "Delete";
const string COL_Filter = "Filter";
const string COL_MoveUp = "MoveUp";
const string COL_MoveDown = "MoveDown";
const string BLACK_UP_POINTING_TRIANGLE = "\u25B2";
const string BLACK_DOWN_POINTING_TRIANGLE = "\u25BC";
const string COL_Original = nameof(Original);
const string COL_Delete = nameof(Delete);
const string COL_Filter = nameof(Filter);
const string COL_MoveUp = nameof(MoveUp);
const string COL_MoveDown = nameof(MoveDown);
Form1 _parent { get; }
@@ -29,20 +26,27 @@ namespace LibationWinForms.Dialogs
dataGridView1.Columns[COL_Filter].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
populateFilters();
populateGridValues();
}
private void populateFilters()
private void populateGridValues()
{
var filters = QuickFilters.Filters;
if (!filters.Any())
return;
foreach (var filter in filters)
dataGridView1.Rows.Add(filter, "X", filter, "\u25B2", "\u25BC");
dataGridView1.Rows.Add(filter, "X", filter, BLACK_UP_POINTING_TRIANGLE, BLACK_DOWN_POINTING_TRIANGLE);
}
private void SaveBtn_Click(object sender, EventArgs e)
private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e)
{
e.Row.Cells[COL_Delete].Value = "X";
e.Row.Cells[COL_MoveUp].Value = BLACK_UP_POINTING_TRIANGLE;
e.Row.Cells[COL_MoveDown].Value = BLACK_DOWN_POINTING_TRIANGLE;
}
private void saveBtn_Click(object sender, EventArgs e)
{
var list = dataGridView1.Rows
.OfType<DataGridViewRow>()
@@ -51,10 +55,11 @@ namespace LibationWinForms.Dialogs
QuickFilters.ReplaceAll(list);
_parent.UpdateFilterDropDown();
this.DialogResult = DialogResult.OK;
this.Close();
}
private void CancelBtn_Click(object sender, EventArgs e) => this.Close();
private void cancelBtn_Click(object sender, EventArgs e) => this.Close();
private void DataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
{

View File

@@ -36,7 +36,7 @@
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(28, 24);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(260, 13);
this.label1.Size = new System.Drawing.Size(263, 13);
this.label1.TabIndex = 0;
this.label1.Text = "Scanning Audible library. This may take a few minutes";
//
@@ -44,7 +44,7 @@
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(319, 63);
this.ClientSize = new System.Drawing.Size(440, 63);
this.Controls.Add(this.label1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;

View File

@@ -1,31 +1,44 @@
using System;
using System.Windows.Forms;
using ApplicationServices;
using InternalUtilities;
using LibationWinForms.Login;
namespace LibationWinForms.Dialogs
{
public partial class IndexLibraryDialog : Form
{
private Account[] _accounts { get; }
public int NewBooksAdded { get; private set; }
public int TotalBooksProcessed { get; private set; }
public IndexLibraryDialog()
public IndexLibraryDialog(params Account[] accounts)
{
_accounts = accounts;
InitializeComponent();
this.Shown += IndexLibraryDialog_Shown;
}
private async void IndexLibraryDialog_Shown(object sender, System.EventArgs e)
private async void IndexLibraryDialog_Shown(object sender, EventArgs e)
{
try
if (_accounts != null && _accounts.Length > 0)
{
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportLibraryAsync(new WinformResponder());
}
catch (Exception ex)
{
var msg = "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator";
MessageBox.Show(msg, "Error importing library", MessageBoxButtons.OK, MessageBoxIcon.Error);
this.label1.Text
= (_accounts.Length == 1)
? "Scanning Audible library. This may take a few minutes."
: $"Scanning Audible library: {_accounts.Length} accounts. This may take a few minutes per account.";
try
{
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync((account) => new WinformResponder(account), _accounts);
}
catch (Exception ex)
{
var msg = "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator";
Serilog.Log.Logger.Error(ex, msg);
MessageBox.Show(msg, "Error importing library", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
this.Close();

View File

@@ -0,0 +1,78 @@
namespace LibationWinForms.Dialogs.Login
{
partial class ApprovalNeededDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.approvedBtn = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// approvedBtn
//
this.approvedBtn.Location = new System.Drawing.Point(15, 25);
this.approvedBtn.Name = "approvedBtn";
this.approvedBtn.Size = new System.Drawing.Size(79, 23);
this.approvedBtn.TabIndex = 1;
this.approvedBtn.Text = "Approved";
this.approvedBtn.UseVisualStyleBackColor = true;
this.approvedBtn.Click += new System.EventHandler(this.approvedBtn_Click);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(104, 13);
this.label1.TabIndex = 0;
this.label1.Text = "Click after approving";
//
// ApprovalNeededDialog
//
this.AcceptButton = this.approvedBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(149, 60);
this.Controls.Add(this.label1);
this.Controls.Add(this.approvedBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "ApprovalNeededDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Approval Needed";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button approvedBtn;
private System.Windows.Forms.Label label1;
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public partial class ApprovalNeededDialog : Form
{
public ApprovalNeededDialog()
{
InitializeComponent();
}
private void approvedBtn_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.OK;
}
}
}

View File

@@ -29,10 +29,10 @@
private void InitializeComponent()
{
this.passwordLbl = new System.Windows.Forms.Label();
this.emailLbl = new System.Windows.Forms.Label();
this.passwordTb = new System.Windows.Forms.TextBox();
this.emailTb = new System.Windows.Forms.TextBox();
this.submitBtn = new System.Windows.Forms.Button();
this.localeLbl = new System.Windows.Forms.Label();
this.usernameLbl = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// passwordLbl
@@ -44,15 +44,6 @@
this.passwordLbl.TabIndex = 2;
this.passwordLbl.Text = "Password";
//
// emailLbl
//
this.emailLbl.AutoSize = true;
this.emailLbl.Location = new System.Drawing.Point(12, 15);
this.emailLbl.Name = "emailLbl";
this.emailLbl.Size = new System.Drawing.Size(32, 13);
this.emailLbl.TabIndex = 0;
this.emailLbl.Text = "Email";
//
// passwordTb
//
this.passwordTb.Location = new System.Drawing.Point(71, 38);
@@ -61,13 +52,6 @@
this.passwordTb.Size = new System.Drawing.Size(200, 20);
this.passwordTb.TabIndex = 3;
//
// emailTb
//
this.emailTb.Location = new System.Drawing.Point(71, 12);
this.emailTb.Name = "emailTb";
this.emailTb.Size = new System.Drawing.Size(200, 20);
this.emailTb.TabIndex = 1;
//
// submitBtn
//
this.submitBtn.Location = new System.Drawing.Point(196, 64);
@@ -78,17 +62,35 @@
this.submitBtn.UseVisualStyleBackColor = true;
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
//
// localeLbl
//
this.localeLbl.AutoSize = true;
this.localeLbl.Location = new System.Drawing.Point(12, 9);
this.localeLbl.Name = "localeLbl";
this.localeLbl.Size = new System.Drawing.Size(59, 13);
this.localeLbl.TabIndex = 0;
this.localeLbl.Text = "Locale: {0}";
//
// usernameLbl
//
this.usernameLbl.AutoSize = true;
this.usernameLbl.Location = new System.Drawing.Point(12, 22);
this.usernameLbl.Name = "usernameLbl";
this.usernameLbl.Size = new System.Drawing.Size(75, 13);
this.usernameLbl.TabIndex = 1;
this.usernameLbl.Text = "Username: {0}";
//
// AudibleLoginDialog
//
this.AcceptButton = this.submitBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(283, 99);
this.Controls.Add(this.usernameLbl);
this.Controls.Add(this.localeLbl);
this.Controls.Add(this.submitBtn);
this.Controls.Add(this.passwordLbl);
this.Controls.Add(this.emailLbl);
this.Controls.Add(this.passwordTb);
this.Controls.Add(this.emailTb);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
@@ -104,9 +106,9 @@
#endregion
private System.Windows.Forms.Label passwordLbl;
private System.Windows.Forms.Label emailLbl;
private System.Windows.Forms.TextBox passwordTb;
private System.Windows.Forms.TextBox emailTb;
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.Label localeLbl;
private System.Windows.Forms.Label usernameLbl;
}
}

View File

@@ -1,23 +1,36 @@
using System;
using System.Windows.Forms;
using InternalUtilities;
namespace LibationWinForms.Dialogs.Login
{
public partial class AudibleLoginDialog : Form
{
private string locale { get; }
private string accountId { get; }
public string Email { get; private set; }
public string Password { get; private set; }
public AudibleLoginDialog()
public AudibleLoginDialog(Account account)
{
InitializeComponent();
locale = account.Locale.Name;
accountId = account.AccountId;
// do not allow user to change login id here. if they do then jsonpath will fail
this.localeLbl.Text = string.Format(this.localeLbl.Text, locale);
this.usernameLbl.Text = string.Format(this.usernameLbl.Text, accountId);
}
private void submitBtn_Click(object sender, EventArgs e)
{
Email = this.emailTb.Text;
Email = accountId;
Password = this.passwordTb.Text;
DialogResult = DialogResult.OK;
// Close() not needed for AcceptButton
}
}
}

View File

@@ -25,7 +25,9 @@ namespace LibationWinForms.Dialogs.Login
private void submitBtn_Click(object sender, EventArgs e)
{
Answer = this.answerTb.Text;
DialogResult = DialogResult.OK;
// Close() not needed for AcceptButton
}
}
}

View File

@@ -1,10 +1,19 @@
using System;
using AudibleApi;
using InternalUtilities;
using LibationWinForms.Dialogs.Login;
namespace LibationWinForms.Login
{
public class WinformResponder : AudibleApi.ILoginCallback
public class WinformResponder : ILoginCallback
{
private Account _account { get; }
public WinformResponder(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
}
public string Get2faCode()
{
using var dialog = new _2faCodeDialog();
@@ -23,10 +32,16 @@ namespace LibationWinForms.Login
public (string email, string password) GetLogin()
{
using var dialog = new AudibleLoginDialog();
using var dialog = new AudibleLoginDialog(_account);
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
return (dialog.Email, dialog.Password);
return (null, null);
}
public void ShowApprovalNeeded()
{
using var dialog = new ApprovalNeededDialog();
dialog.ShowDialog();
}
}
}

View File

@@ -0,0 +1,121 @@
namespace LibationWinForms.Dialogs
{
partial class ScanAccountsDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.accountsLbl = new System.Windows.Forms.Label();
this.accountsClb = new System.Windows.Forms.CheckedListBox();
this.importBtn = new System.Windows.Forms.Button();
this.cancelBtn = new System.Windows.Forms.Button();
this.editBtn = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// accountsLbl
//
this.accountsLbl.AutoSize = true;
this.accountsLbl.Location = new System.Drawing.Point(12, 9);
this.accountsLbl.Name = "accountsLbl";
this.accountsLbl.Size = new System.Drawing.Size(467, 13);
this.accountsLbl.TabIndex = 0;
this.accountsLbl.Text = "Check the accounts to scan and import. To change default selections, go to: Setti" +
"ngs > Accounts";
//
// accountsClb
//
this.accountsClb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.accountsClb.FormattingEnabled = true;
this.accountsClb.Location = new System.Drawing.Point(12, 25);
this.accountsClb.Name = "accountsClb";
this.accountsClb.Size = new System.Drawing.Size(560, 94);
this.accountsClb.TabIndex = 1;
//
// importBtn
//
this.importBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.importBtn.Location = new System.Drawing.Point(396, 125);
this.importBtn.Name = "importBtn";
this.importBtn.Size = new System.Drawing.Size(75, 23);
this.importBtn.TabIndex = 3;
this.importBtn.Text = "Import";
this.importBtn.UseVisualStyleBackColor = true;
this.importBtn.Click += new System.EventHandler(this.importBtn_Click);
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(497, 125);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(75, 23);
this.cancelBtn.TabIndex = 4;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// editBtn
//
this.editBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.editBtn.Location = new System.Drawing.Point(12, 125);
this.editBtn.Name = "editBtn";
this.editBtn.Size = new System.Drawing.Size(90, 23);
this.editBtn.TabIndex = 2;
this.editBtn.Text = "Edit accounts";
this.editBtn.UseVisualStyleBackColor = true;
this.editBtn.Click += new System.EventHandler(this.editBtn_Click);
//
// ScanAccountsDialog
//
this.AcceptButton = this.importBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(584, 160);
this.Controls.Add(this.editBtn);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.importBtn);
this.Controls.Add(this.accountsClb);
this.Controls.Add(this.accountsLbl);
this.Name = "ScanAccountsDialog";
this.Text = "Which accounts?";
this.Load += new System.EventHandler(this.ScanAccountsDialog_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label accountsLbl;
private System.Windows.Forms.CheckedListBox accountsClb;
private System.Windows.Forms.Button importBtn;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.Button editBtn;
}
}

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using InternalUtilities;
namespace LibationWinForms.Dialogs
{
public partial class ScanAccountsDialog : Form
{
public List<Account> CheckedAccounts { get; } = new List<Account>();
Form1 _parent { get; }
public ScanAccountsDialog(Form1 parent)
{
_parent = parent;
InitializeComponent();
}
class listItem
{
public Account Account { get; set; }
public string Text { get; set; }
public override string ToString() => Text;
}
private void ScanAccountsDialog_Load(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
foreach (var account in accounts)
{
var item = new listItem
{
Account = account,
Text = $"{account.AccountName} ({account.AccountId} - {account.Locale.Name})"
};
this.accountsClb.Items.Add(item, account.LibraryScan);
}
}
private void editBtn_Click(object sender, EventArgs e)
{
if (new AccountsDialog(_parent).ShowDialog() == DialogResult.OK)
{
// clear grid
this.accountsClb.Items.Clear();
// reload grid and default checkboxes
ScanAccountsDialog_Load(sender, e);
}
}
private void importBtn_Click(object sender, EventArgs e)
{
foreach (listItem item in accountsClb.CheckedItems)
CheckedAccounts.Add(item.Account);
this.DialogResult = DialogResult.OK;
this.Close();
}
private void cancelBtn_Click(object sender, EventArgs e) => this.Close();
}
}

View File

@@ -73,7 +73,7 @@
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(168, 52);
this.label4.TabIndex = 3;
this.label4.Text = "BOOL FIELDS\r\n\r\nFind books that you haven\'t rated:\r\n -IsRated";
this.label4.Text = "BOOLEAN (TRUE/FALSE) FIELDS\r\n\r\nFind books that you haven\'t rated:\r\n -IsRated";
//
// label5
//
@@ -89,7 +89,7 @@
//
this.closeBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.closeBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.closeBtn.Location = new System.Drawing.Point(890, 415);
this.closeBtn.Location = new System.Drawing.Point(890, 465);
this.closeBtn.Name = "closeBtn";
this.closeBtn.Size = new System.Drawing.Size(75, 23);
this.closeBtn.TabIndex = 5;
@@ -103,7 +103,7 @@
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.closeBtn;
this.ClientSize = new System.Drawing.Size(977, 450);
this.ClientSize = new System.Drawing.Size(977, 500);
this.Controls.Add(this.closeBtn);
this.Controls.Add(this.label5);
this.Controls.Add(this.label4);

View File

@@ -28,12 +28,9 @@
/// </summary>
private void InitializeComponent()
{
this.decryptKeyLbl = new System.Windows.Forms.Label();
this.decryptKeyTb = new System.Windows.Forms.TextBox();
this.booksLocationLbl = new System.Windows.Forms.Label();
this.booksLocationTb = new System.Windows.Forms.TextBox();
this.booksLocationSearchBtn = new System.Windows.Forms.Button();
this.decryptKeyDescLbl = new System.Windows.Forms.Label();
this.booksLocationDescLbl = new System.Windows.Forms.Label();
this.downloadsInProgressGb = new System.Windows.Forms.GroupBox();
this.downloadsInProgressLibationFilesRb = new System.Windows.Forms.RadioButton();
@@ -45,30 +42,12 @@
this.decryptInProgressDescLbl = new System.Windows.Forms.Label();
this.saveBtn = new System.Windows.Forms.Button();
this.cancelBtn = new System.Windows.Forms.Button();
this.audibleLocaleLbl = new System.Windows.Forms.Label();
this.audibleLocaleCb = new System.Windows.Forms.ComboBox();
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.downloadsInProgressGb.SuspendLayout();
this.decryptInProgressGb.SuspendLayout();
this.groupBox1.SuspendLayout();
this.SuspendLayout();
//
// decryptKeyLbl
//
this.decryptKeyLbl.AutoSize = true;
this.decryptKeyLbl.Location = new System.Drawing.Point(6, 22);
this.decryptKeyLbl.Name = "decryptKeyLbl";
this.decryptKeyLbl.Size = new System.Drawing.Size(64, 13);
this.decryptKeyLbl.TabIndex = 0;
this.decryptKeyLbl.Text = "Decrypt key";
//
// decryptKeyTb
//
this.decryptKeyTb.Location = new System.Drawing.Point(76, 19);
this.decryptKeyTb.Name = "decryptKeyTb";
this.decryptKeyTb.Size = new System.Drawing.Size(100, 20);
this.decryptKeyTb.TabIndex = 1;
//
// booksLocationLbl
//
this.booksLocationLbl.AutoSize = true;
@@ -95,15 +74,6 @@
this.booksLocationSearchBtn.UseVisualStyleBackColor = true;
this.booksLocationSearchBtn.Click += new System.EventHandler(this.booksLocationSearchBtn_Click);
//
// decryptKeyDescLbl
//
this.decryptKeyDescLbl.AutoSize = true;
this.decryptKeyDescLbl.Location = new System.Drawing.Point(73, 42);
this.decryptKeyDescLbl.Name = "decryptKeyDescLbl";
this.decryptKeyDescLbl.Size = new System.Drawing.Size(36, 13);
this.decryptKeyDescLbl.TabIndex = 2;
this.decryptKeyDescLbl.Text = "[desc]";
//
// booksLocationDescLbl
//
this.booksLocationDescLbl.AutoSize = true;
@@ -118,7 +88,7 @@
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressLibationFilesRb);
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressWinTempRb);
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressDescLbl);
this.downloadsInProgressGb.Location = new System.Drawing.Point(15, 58);
this.downloadsInProgressGb.Location = new System.Drawing.Point(15, 19);
this.downloadsInProgressGb.Name = "downloadsInProgressGb";
this.downloadsInProgressGb.Size = new System.Drawing.Size(758, 117);
this.downloadsInProgressGb.TabIndex = 4;
@@ -163,7 +133,7 @@
this.decryptInProgressGb.Controls.Add(this.decryptInProgressLibationFilesRb);
this.decryptInProgressGb.Controls.Add(this.decryptInProgressWinTempRb);
this.decryptInProgressGb.Controls.Add(this.decryptInProgressDescLbl);
this.decryptInProgressGb.Location = new System.Drawing.Point(9, 183);
this.decryptInProgressGb.Location = new System.Drawing.Point(9, 144);
this.decryptInProgressGb.Name = "decryptInProgressGb";
this.decryptInProgressGb.Size = new System.Drawing.Size(758, 117);
this.decryptInProgressGb.TabIndex = 5;
@@ -206,7 +176,7 @@
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(612, 404);
this.saveBtn.Location = new System.Drawing.Point(612, 328);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(75, 23);
this.saveBtn.TabIndex = 7;
@@ -218,7 +188,7 @@
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(713, 404);
this.cancelBtn.Location = new System.Drawing.Point(713, 328);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(75, 23);
this.cancelBtn.TabIndex = 8;
@@ -226,40 +196,13 @@
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// audibleLocaleLbl
//
this.audibleLocaleLbl.AutoSize = true;
this.audibleLocaleLbl.Location = new System.Drawing.Point(12, 56);
this.audibleLocaleLbl.Name = "audibleLocaleLbl";
this.audibleLocaleLbl.Size = new System.Drawing.Size(77, 13);
this.audibleLocaleLbl.TabIndex = 4;
this.audibleLocaleLbl.Text = "Audible Locale";
//
// audibleLocaleCb
//
this.audibleLocaleCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.audibleLocaleCb.FormattingEnabled = true;
this.audibleLocaleCb.Items.AddRange(new object[] {
"us",
"uk",
"germany",
"france",
"canada"});
this.audibleLocaleCb.Location = new System.Drawing.Point(95, 53);
this.audibleLocaleCb.Name = "audibleLocaleCb";
this.audibleLocaleCb.Size = new System.Drawing.Size(53, 21);
this.audibleLocaleCb.TabIndex = 5;
//
// groupBox1
//
this.groupBox1.Controls.Add(this.decryptKeyTb);
this.groupBox1.Controls.Add(this.decryptKeyLbl);
this.groupBox1.Controls.Add(this.decryptKeyDescLbl);
this.groupBox1.Controls.Add(this.downloadsInProgressGb);
this.groupBox1.Controls.Add(this.decryptInProgressGb);
this.groupBox1.Location = new System.Drawing.Point(15, 90);
this.groupBox1.Location = new System.Drawing.Point(15, 53);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(773, 308);
this.groupBox1.Size = new System.Drawing.Size(773, 269);
this.groupBox1.TabIndex = 6;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "Advanced settings for control freaks";
@@ -270,10 +213,8 @@
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(800, 439);
this.ClientSize = new System.Drawing.Size(800, 363);
this.Controls.Add(this.groupBox1);
this.Controls.Add(this.audibleLocaleCb);
this.Controls.Add(this.audibleLocaleLbl);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn);
this.Controls.Add(this.booksLocationDescLbl);
@@ -290,19 +231,15 @@
this.decryptInProgressGb.ResumeLayout(false);
this.decryptInProgressGb.PerformLayout();
this.groupBox1.ResumeLayout(false);
this.groupBox1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label decryptKeyLbl;
private System.Windows.Forms.TextBox decryptKeyTb;
private System.Windows.Forms.Label booksLocationLbl;
private System.Windows.Forms.TextBox booksLocationTb;
private System.Windows.Forms.Button booksLocationSearchBtn;
private System.Windows.Forms.Label decryptKeyDescLbl;
private System.Windows.Forms.Label booksLocationDescLbl;
private System.Windows.Forms.GroupBox downloadsInProgressGb;
private System.Windows.Forms.Label downloadsInProgressDescLbl;
@@ -314,8 +251,6 @@
private System.Windows.Forms.RadioButton decryptInProgressWinTempRb;
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.Label audibleLocaleLbl;
private System.Windows.Forms.ComboBox audibleLocaleCb;
private System.Windows.Forms.GroupBox groupBox1;
}
}

View File

@@ -3,6 +3,7 @@ using System.IO;
using System.Windows.Forms;
using Dinah.Core;
using FileManager;
using InternalUtilities;
namespace LibationWinForms.Dialogs
{
@@ -18,8 +19,6 @@ namespace LibationWinForms.Dialogs
private void SettingsDialog_Load(object sender, EventArgs e)
{
this.decryptKeyTb.Text = config.DecryptKey;
this.decryptKeyDescLbl.Text = desc(nameof(config.DecryptKey));
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
this.downloadsInProgressDescLbl.Text = desc(nameof(config.DownloadsInProgressEnum));
this.decryptInProgressDescLbl.Text = desc(nameof(config.DecryptInProgressEnum));
@@ -37,11 +36,6 @@ namespace LibationWinForms.Dialogs
? config.Books
: Path.GetDirectoryName(Exe.FileLocationOnDisk);
this.audibleLocaleCb.Text
= !string.IsNullOrWhiteSpace(config.LocaleCountryCode)
? config.LocaleCountryCode
: "us";
switch (config.DownloadsInProgressEnum)
{
case "LibationFiles":
@@ -77,8 +71,6 @@ namespace LibationWinForms.Dialogs
private void saveBtn_Click(object sender, EventArgs e)
{
config.DecryptKey = this.decryptKeyTb.Text;
config.LocaleCountryCode = this.audibleLocaleCb.Text;
config.DownloadsInProgressEnum = downloadsInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp";
config.DecryptInProgressEnum = decryptInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp";

View File

@@ -35,15 +35,20 @@
this.filterSearchTb = new System.Windows.Forms.TextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.advancedSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
@@ -52,6 +57,8 @@
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.addFilterBtn = new System.Windows.Forms.Button();
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
this.SuspendLayout();
@@ -102,6 +109,7 @@
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.importToolStripMenuItem,
this.liberateToolStripMenuItem,
this.exportToolStripMenuItem,
this.quickFiltersToolStripMenuItem,
this.settingsToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
@@ -113,18 +121,41 @@
// importToolStripMenuItem
//
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.scanLibraryToolStripMenuItem});
this.noAccountsYetAddAccountToolStripMenuItem,
this.scanLibraryToolStripMenuItem,
this.scanLibraryOfAllAccountsToolStripMenuItem,
this.scanLibraryOfSomeAccountsToolStripMenuItem});
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
this.importToolStripMenuItem.Text = "&Import";
//
// noAccountsYetAddAccountToolStripMenuItem
//
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
//
// scanLibraryToolStripMenuItem
//
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(138, 22);
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
this.scanLibraryToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryToolStripMenuItem_Click);
//
// scanLibraryOfAllAccountsToolStripMenuItem
//
this.scanLibraryOfAllAccountsToolStripMenuItem.Name = "scanLibraryOfAllAccountsToolStripMenuItem";
this.scanLibraryOfAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfAllAccountsToolStripMenuItem.Text = "Scan Library of &All Accounts";
this.scanLibraryOfAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfAllAccountsToolStripMenuItem_Click);
//
// scanLibraryOfSomeAccountsToolStripMenuItem
//
this.scanLibraryOfSomeAccountsToolStripMenuItem.Name = "scanLibraryOfSomeAccountsToolStripMenuItem";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts...";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfSomeAccountsToolStripMenuItem_Click);
//
// liberateToolStripMenuItem
//
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
@@ -148,6 +179,14 @@
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
//
// exportToolStripMenuItem
//
this.exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.exportLibraryToolStripMenuItem});
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
this.exportToolStripMenuItem.Size = new System.Drawing.Size(53, 20);
this.exportToolStripMenuItem.Text = "E&xport";
//
// quickFiltersToolStripMenuItem
//
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
@@ -169,7 +208,7 @@
//
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters";
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters...";
this.editQuickFiltersToolStripMenuItem.Click += new System.EventHandler(this.EditQuickFiltersToolStripMenuItem_Click);
//
// toolStripSeparator1
@@ -180,24 +219,32 @@
// settingsToolStripMenuItem
//
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.accountsToolStripMenuItem,
this.basicSettingsToolStripMenuItem,
this.advancedSettingsToolStripMenuItem});
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.settingsToolStripMenuItem.Text = "&Settings";
//
// accountsToolStripMenuItem
//
this.accountsToolStripMenuItem.Name = "accountsToolStripMenuItem";
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
this.accountsToolStripMenuItem.Text = "&Accounts...";
this.accountsToolStripMenuItem.Click += new System.EventHandler(this.accountsToolStripMenuItem_Click);
//
// basicSettingsToolStripMenuItem
//
this.basicSettingsToolStripMenuItem.Name = "basicSettingsToolStripMenuItem";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.basicSettingsToolStripMenuItem.Text = "&Basic Settings";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
this.basicSettingsToolStripMenuItem.Text = "&Basic Settings...";
this.basicSettingsToolStripMenuItem.Click += new System.EventHandler(this.basicSettingsToolStripMenuItem_Click);
//
// advancedSettingsToolStripMenuItem
//
this.advancedSettingsToolStripMenuItem.Name = "advancedSettingsToolStripMenuItem";
this.advancedSettingsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.advancedSettingsToolStripMenuItem.Text = "&Advanced Settings";
this.advancedSettingsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
this.advancedSettingsToolStripMenuItem.Text = "Ad&vanced Settings...";
this.advancedSettingsToolStripMenuItem.Click += new System.EventHandler(this.advancedSettingsToolStripMenuItem_Click);
//
// statusStrip1
@@ -222,7 +269,7 @@
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(232, 17);
this.springLbl.Size = new System.Drawing.Size(233, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl
@@ -234,7 +281,7 @@
// pdfsCountsLbl
//
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
this.pdfsCountsLbl.Size = new System.Drawing.Size(219, 17);
this.pdfsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.pdfsCountsLbl.Text = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
//
// addFilterBtn
@@ -247,6 +294,20 @@
this.addFilterBtn.UseVisualStyleBackColor = true;
this.addFilterBtn.Click += new System.EventHandler(this.AddFilterBtn_Click);
//
// noAccountsYetAddAccountToolStripMenuItem
//
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
this.noAccountsYetAddAccountToolStripMenuItem.Click += new System.EventHandler(this.noAccountsYetAddAccountToolStripMenuItem_Click);
//
// exportLibraryToolStripMenuItem
//
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
this.exportLibraryToolStripMenuItem.Click += new System.EventHandler(this.exportLibraryToolStripMenuItem_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -274,6 +335,7 @@
}
#endregion
private System.Windows.Forms.Panel gridPanel;
private System.Windows.Forms.MenuStrip menuStrip1;
private System.Windows.Forms.ToolStripMenuItem importToolStripMenuItem;
@@ -297,6 +359,11 @@
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem basicSettingsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem advancedSettingsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem accountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryOfAllAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryOfSomeAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem noAccountsYetAddAccountToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exportToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exportLibraryToolStripMenuItem;
}
}

View File

@@ -8,6 +8,7 @@ using Dinah.Core;
using Dinah.Core.Drawing;
using Dinah.Core.Windows.Forms;
using FileManager;
using InternalUtilities;
using LibationWinForms.Dialogs;
namespace LibationWinForms
@@ -42,6 +43,8 @@ namespace LibationWinForms
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
RefreshImportMenu();
setVisibleCount(null, 0);
reloadGrid();
@@ -55,8 +58,8 @@ namespace LibationWinForms
setBackupCounts(null, null);
}
#region reload grid
bool isProcessingGridSelect = false;
#region reload grid
bool isProcessingGridSelect = false;
private void reloadGrid()
{
// suppressed filter while init'ing UI
@@ -127,20 +130,26 @@ namespace LibationWinForms
var downloadedOnly = results.Count(r => r == AudioFileState.aax);
var noProgress = results.Count(r => r == AudioFileState.none);
// enable/disable export
exportLibraryToolStripMenuItem.Enabled = results.Any();
// update bottom numbers
var pending = noProgress + downloadedOnly;
var text
var statusStripText
= !results.Any() ? "No books. Begin by importing your library"
: pending > 0 ? string.Format(backupsCountsLbl_Format, noProgress, downloadedOnly, fullyBackedUp)
: $"All {"book".PluralizeWithCount(fullyBackedUp)} backed up";
statusStrip1.UIThread(() => backupsCountsLbl.Text = text);
// update menu item
var menuItemText
= pending > 0
? $"{pending} remaining"
: "All books have been liberated";
Serilog.Log.Logger.Information(menuItemText);
Serilog.Log.Logger.Information("Book counts. {@DebugInfo}", new { fullyBackedUp, downloadedOnly, noProgress, pending, statusStripText, menuItemText });
// update UI
statusStrip1.UIThread(() => backupsCountsLbl.Text = statusStripText);
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
}
@@ -155,25 +164,28 @@ namespace LibationWinForms
var notDownloaded = boolResults.Count(r => !r);
// update bottom numbers
var text
var statusStripText
= !boolResults.Any() ? ""
: notDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, notDownloaded, downloaded)
: $"| All {downloaded} PDFs downloaded";
statusStrip1.UIThread(() => pdfsCountsLbl.Text = text);
// update menu item
var menuItemText
= notDownloaded > 0
? $"{notDownloaded} remaining"
: "All PDFs have been downloaded";
Serilog.Log.Logger.Information(menuItemText);
Serilog.Log.Logger.Information("PDF counts. {@DebugInfo}", new { downloaded, notDownloaded, statusStripText, menuItemText });
// update UI
statusStrip1.UIThread(() => pdfsCountsLbl.Text = statusStripText);
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = notDownloaded > 0);
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
}
#endregion
#region filter
private void filterHelpBtn_Click(object sender, EventArgs e) => new Dialogs.SearchSyntaxDialog().ShowDialog();
private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog();
private void AddFilterBtn_Click(object sender, EventArgs e)
{
@@ -217,12 +229,57 @@ namespace LibationWinForms
doFilter(lastGoodFilter);
}
}
#endregion
#endregion
#region index menu
private void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
#region Import menu
public void RefreshImportMenu()
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var count = persister.AccountsSettings.Accounts.Count;
noAccountsYetAddAccountToolStripMenuItem.Visible = count == 0;
scanLibraryToolStripMenuItem.Visible = count == 1;
scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1;
scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1;
}
private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e)
{
MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
new AccountsDialog(this).ShowDialog();
}
private void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
scanLibraries(firstAccount);
}
private void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
scanLibraries(allAccounts);
}
private void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog(this);
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
scanLibraries(scanAccountsDialog.CheckedAccounts);
}
private void scanLibraries(IEnumerable<Account> accounts) => scanLibraries(accounts.ToArray());
private void scanLibraries(params Account[] accounts)
{
using var dialog = new IndexLibraryDialog();
using var dialog = new IndexLibraryDialog(accounts);
dialog.ShowDialog();
var totalProcessed = dialog.TotalBooksProcessed;
@@ -232,7 +289,7 @@ namespace LibationWinForms
if (totalProcessed > 0)
reloadGrid();
}
}
#endregion
#region liberate menu
@@ -245,6 +302,45 @@ namespace LibationWinForms
private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId);
#endregion
#region Export menu
private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
var saveFileDialog = new SaveFileDialog
{
Title = "Where to export Library",
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
};
if (saveFileDialog.ShowDialog() != DialogResult.OK)
return;
// FilterIndex is 1-based, NOT 0-based
switch (saveFileDialog.FilterIndex)
{
case 1: // xlsx
default:
LibraryExporter.ToXlsx(saveFileDialog.FileName);
break;
case 2: // csv
LibraryExporter.ToCsv(saveFileDialog.FileName);
break;
case 3: // json
LibraryExporter.ToJson(saveFileDialog.FileName);
break;
}
MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error attempting to export library");
MessageBox.Show("Error attempting to export your library. Error message:\r\n\r\n" + ex.Message, "Error exporting", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
#endregion
#region quick filters menu
private void loadInitialQuickFilterState()
{
@@ -290,10 +386,12 @@ namespace LibationWinForms
}
}
private void EditQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new Dialogs.EditQuickFilters(this).ShowDialog();
private void EditQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters(this).ShowDialog();
#endregion
#region settings menu
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog(this).ShowDialog();
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
private void advancedSettingsToolStripMenuItem_Click(object sender, EventArgs e)

View File

@@ -174,17 +174,28 @@ namespace LibationWinForms
get
{
var details = new List<string>();
var locale
= string.IsNullOrWhiteSpace(book.Locale)
? "[unknown]"
: book.Locale;
var acct
= string.IsNullOrWhiteSpace(libraryBook.Account)
? "[unknown]"
: libraryBook.Account;
details.Add($"Account: {locale} - {acct}");
if (book.HasPdf)
details.Add("Has PDF");
if (book.IsAbridged)
details.Add("Abridged");
if (book.DatePublished.HasValue)
details.Add($"Date pub'd: {book.DatePublished.Value.ToString("MM/dd/yyyy")}");
details.Add($"Date pub'd: {book.DatePublished.Value:MM/dd/yyyy}");
// this goes last since it's most likely to have a line-break
if (!string.IsNullOrWhiteSpace(book.Publisher))
details.Add($"Pub: {book.Publisher}");
details.Add($"Pub: {book.Publisher.Trim()}");
if (!details.Any())
if (!details.Any())
return "[details not imported]";
return string.Join("\r\n", details);

View File

@@ -1,5 +1,7 @@
# Libation: Liberate your Library
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
# Table of Contents
1. [Audible audiobook manager](#audible-audiobook-manager)
@@ -7,10 +9,12 @@
- [The bad](#the-bad)
- [The ugly](#the-ugly)
2. [Getting started](#getting-started)
- [Create Accounts](#create-accounts)
- [Import your library](#import-your-library)
- [Download your books -- DRM-free!](#download-your-books----drm-free)
- [Download PDF attachments](#download-pdf-attachments)
- [Details of downloaded files](#details-of-downloaded-files)
- [Export your library](#export-your-library)
3. [Searching and filtering](#searching-and-filtering)
- [Tags](#tags)
- [Searches](#searches)
@@ -30,7 +34,7 @@
* Powerful advanced search built on the Lucene search engine
* Customizable saved filters for common searches
* Open source
* Tested on US Audible only. Should theoretically also work for Canada, UK, Germany, and France
* Tested on US Audible only. Should theoretically also work for Canada, UK, Germany, France, and Australia
<a name="theBad"/>
@@ -53,12 +57,28 @@ I made this for myself and I want to share it with the great programming and aud
## Getting started
#### [Download Libation](https://github.com/rmcrackan/Libation/releases)
### Create Accounts
Create your account(s):
![Create your accounts, menu](images/v40_accounts.png)
New locale options include many more regions including old audible accounts which pre-date the amazon acquisition
![Choose your account locales](images/v40_locales.png)
### Import your library
Select Import > Scan Library:
![Import step 1](images/Import1.png)
Or if you have multiple accounts, you'll get to choose whether to scan all accounts or just the ones you select:
![Import which accounts](images/v40_import.png)
You'll see this window while it's scanning:
![Import step 2](images/Import2.png)
@@ -125,6 +145,12 @@ When you set up Libation, you'll specify a Books directory. Libation looks insid
* .cue: this is a file which logs where chapter breaks occur. Many tools are able to use this if you want to split your book into files along chapter lines.
* .nfo: This is just some general info about the book and includes some technical stats about the audiofile.
### Export your library
![Export](images/Export.png)
Export your library to Excel, CSV, or JSON
## Searching and filtering
### Tags

View File

@@ -1,7 +1,13 @@
-- begin VERSIONING ---------------------------------------------------------------------------------------------------------------------
https://github.com/rmcrackan/Libation/releases
v3.1.2 : null checks
v3.1.8 : Experimental: add Australia to locale options
v3.1.7 : Improved logging
v3.1.6 : Bugfix: some series indexes/sequences formats cause library not to import
v3.1.5 : Bugfix: some series indexes/sequences could cause library not to import
v3.1.4 : Bugfix: IsAuthorNarrated was returning no books
v3.1.3 : fix weirdness with build number
v3.1.2 : minor bug fixes
v3.1.1 : Check if upgrade available on github
v3.1.0 : FIRST PUBLIC RELEASE
v3.1-beta.11 : Improved configuration and settings file management. Configurable logging

View File

@@ -0,0 +1,142 @@
namespace WinFormsDesigner.Dialogs
{
partial class AccountsDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.cancelBtn = new System.Windows.Forms.Button();
this.saveBtn = new System.Windows.Forms.Button();
this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn();
this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn();
this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Locale = new System.Windows.Forms.DataGridViewComboBoxColumn();
this.AccountName = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.SuspendLayout();
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(713, 415);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(75, 23);
this.cancelBtn.TabIndex = 2;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
//
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(612, 415);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(75, 23);
this.saveBtn.TabIndex = 1;
this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true;
//
// dataGridView1
//
this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.dataGridView1.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells;
this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.DeleteAccount,
this.LibraryScan,
this.AccountId,
this.Locale,
this.AccountName});
this.dataGridView1.Location = new System.Drawing.Point(12, 12);
this.dataGridView1.MultiSelect = false;
this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.Size = new System.Drawing.Size(776, 397);
this.dataGridView1.TabIndex = 0;
//
// DeleteAccount
//
this.DeleteAccount.HeaderText = "Delete";
this.DeleteAccount.Name = "DeleteAccount";
this.DeleteAccount.ReadOnly = true;
this.DeleteAccount.Text = "x";
this.DeleteAccount.Width = 44;
//
// LibraryScan
//
this.LibraryScan.HeaderText = "Include in library scan?";
this.LibraryScan.Name = "LibraryScan";
this.LibraryScan.Width = 83;
//
// AccountId
//
this.AccountId.HeaderText = "Audible email/login";
this.AccountId.Name = "AccountId";
this.AccountId.Width = 111;
//
// Locale
//
this.Locale.HeaderText = "Locale";
this.Locale.Name = "Locale";
this.Locale.Width = 45;
//
// AccountName
//
this.AccountName.HeaderText = "Account nickname (optional)";
this.AccountName.Name = "AccountName";
this.AccountName.Width = 152;
//
// AccountsDialog
//
this.AcceptButton = this.saveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Controls.Add(this.dataGridView1);
this.Controls.Add(this.saveBtn);
this.Controls.Add(this.cancelBtn);
this.Name = "AccountsDialog";
this.Text = "Audible Accounts";
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.DataGridView dataGridView1;
private System.Windows.Forms.DataGridViewButtonColumn DeleteAccount;
private System.Windows.Forms.DataGridViewCheckBoxColumn LibraryScan;
private System.Windows.Forms.DataGridViewTextBoxColumn AccountId;
private System.Windows.Forms.DataGridViewComboBoxColumn Locale;
private System.Windows.Forms.DataGridViewTextBoxColumn AccountName;
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Windows.Forms;
namespace WinFormsDesigner.Dialogs
{
public partial class AccountsDialog : Form
{
public AccountsDialog(Form1 parent)
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="Original.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="DeleteAccount.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="LibraryScan.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="AccountId.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="AccountName.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Locale.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
</root>

View File

@@ -36,15 +36,15 @@
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(28, 24);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(260, 13);
this.label1.Size = new System.Drawing.Size(263, 13);
this.label1.TabIndex = 0;
this.label1.Text = "Scanning Audible library. This may take a few minutes";
this.label1.Text = "Scanning Audible library. This may take a few minutes.";
//
// IndexLibraryDialog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(319, 63);
this.ClientSize = new System.Drawing.Size(440, 63);
this.Controls.Add(this.label1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;

View File

@@ -0,0 +1,77 @@
namespace WinFormsDesigner.Dialogs.Login
{
partial class ApprovalNeededDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.approvedBtn = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// approvedBtn
//
this.approvedBtn.Location = new System.Drawing.Point(15, 25);
this.approvedBtn.Name = "approvedBtn";
this.approvedBtn.Size = new System.Drawing.Size(79, 23);
this.approvedBtn.TabIndex = 1;
this.approvedBtn.Text = "Approved";
this.approvedBtn.UseVisualStyleBackColor = true;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(104, 13);
this.label1.TabIndex = 0;
this.label1.Text = "Click after approving";
//
// ApprovalNeededDialog
//
this.AcceptButton = this.approvedBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(149, 60);
this.Controls.Add(this.label1);
this.Controls.Add(this.approvedBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "ApprovalNeededDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Approval Needed";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button approvedBtn;
private System.Windows.Forms.Label label1;
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Windows.Forms;
namespace WinFormsDesigner.Dialogs.Login
{
public partial class ApprovalNeededDialog : Form
{
public ApprovalNeededDialog()
{
InitializeComponent();
}
}
}

View File

@@ -29,10 +29,10 @@
private void InitializeComponent()
{
this.passwordLbl = new System.Windows.Forms.Label();
this.emailLbl = new System.Windows.Forms.Label();
this.passwordTb = new System.Windows.Forms.TextBox();
this.emailTb = new System.Windows.Forms.TextBox();
this.submitBtn = new System.Windows.Forms.Button();
this.localeLbl = new System.Windows.Forms.Label();
this.usernameLbl = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// passwordLbl
@@ -44,15 +44,6 @@
this.passwordLbl.TabIndex = 2;
this.passwordLbl.Text = "Password";
//
// emailLbl
//
this.emailLbl.AutoSize = true;
this.emailLbl.Location = new System.Drawing.Point(12, 15);
this.emailLbl.Name = "emailLbl";
this.emailLbl.Size = new System.Drawing.Size(32, 13);
this.emailLbl.TabIndex = 0;
this.emailLbl.Text = "Email";
//
// passwordTb
//
this.passwordTb.Location = new System.Drawing.Point(71, 38);
@@ -61,13 +52,6 @@
this.passwordTb.Size = new System.Drawing.Size(200, 20);
this.passwordTb.TabIndex = 3;
//
// emailTb
//
this.emailTb.Location = new System.Drawing.Point(71, 12);
this.emailTb.Name = "emailTb";
this.emailTb.Size = new System.Drawing.Size(200, 20);
this.emailTb.TabIndex = 1;
//
// submitBtn
//
this.submitBtn.Location = new System.Drawing.Point(196, 64);
@@ -77,17 +61,35 @@
this.submitBtn.Text = "Submit";
this.submitBtn.UseVisualStyleBackColor = true;
//
// localeLbl
//
this.localeLbl.AutoSize = true;
this.localeLbl.Location = new System.Drawing.Point(12, 9);
this.localeLbl.Name = "localeLbl";
this.localeLbl.Size = new System.Drawing.Size(59, 13);
this.localeLbl.TabIndex = 0;
this.localeLbl.Text = "Locale: {0}";
//
// usernameLbl
//
this.usernameLbl.AutoSize = true;
this.usernameLbl.Location = new System.Drawing.Point(12, 22);
this.usernameLbl.Name = "usernameLbl";
this.usernameLbl.Size = new System.Drawing.Size(75, 13);
this.usernameLbl.TabIndex = 1;
this.usernameLbl.Text = "Username: {0}";
//
// AudibleLoginDialog
//
this.AcceptButton = this.submitBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(283, 99);
this.Controls.Add(this.usernameLbl);
this.Controls.Add(this.localeLbl);
this.Controls.Add(this.submitBtn);
this.Controls.Add(this.passwordLbl);
this.Controls.Add(this.emailLbl);
this.Controls.Add(this.passwordTb);
this.Controls.Add(this.emailTb);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
@@ -103,9 +105,9 @@
#endregion
private System.Windows.Forms.Label passwordLbl;
private System.Windows.Forms.Label emailLbl;
private System.Windows.Forms.TextBox passwordTb;
private System.Windows.Forms.TextBox emailTb;
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.Label localeLbl;
private System.Windows.Forms.Label usernameLbl;
}
}

View File

@@ -5,8 +5,6 @@ namespace WinFormsDesigner.Dialogs.Login
{
public partial class _2faCodeDialog : Form
{
public string NewTags { get; private set; }
public _2faCodeDialog()
{
InitializeComponent();

View File

@@ -0,0 +1,117 @@
namespace WinFormsDesigner.Dialogs
{
partial class ScanAccountsDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.accountsLbl = new System.Windows.Forms.Label();
this.accountsClb = new System.Windows.Forms.CheckedListBox();
this.importBtn = new System.Windows.Forms.Button();
this.cancelBtn = new System.Windows.Forms.Button();
this.editBtn = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// accountsLbl
//
this.accountsLbl.AutoSize = true;
this.accountsLbl.Location = new System.Drawing.Point(12, 9);
this.accountsLbl.Name = "accountsLbl";
this.accountsLbl.Size = new System.Drawing.Size(467, 13);
this.accountsLbl.TabIndex = 0;
this.accountsLbl.Text = "Check the accounts to scan and import. To change default selections, go to: Setti" +
"ngs > Accounts";
//
// accountsClb
//
this.accountsClb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.accountsClb.FormattingEnabled = true;
this.accountsClb.Location = new System.Drawing.Point(12, 25);
this.accountsClb.Name = "accountsClb";
this.accountsClb.Size = new System.Drawing.Size(560, 94);
this.accountsClb.TabIndex = 1;
//
// importBtn
//
this.importBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.importBtn.Location = new System.Drawing.Point(396, 125);
this.importBtn.Name = "importBtn";
this.importBtn.Size = new System.Drawing.Size(75, 23);
this.importBtn.TabIndex = 3;
this.importBtn.Text = "Import";
this.importBtn.UseVisualStyleBackColor = true;
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(497, 125);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(75, 23);
this.cancelBtn.TabIndex = 4;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
//
// editBtn
//
this.editBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.editBtn.Location = new System.Drawing.Point(12, 125);
this.editBtn.Name = "editBtn";
this.editBtn.Size = new System.Drawing.Size(90, 23);
this.editBtn.TabIndex = 2;
this.editBtn.Text = "Edit accounts";
this.editBtn.UseVisualStyleBackColor = true;
//
// ScanAccountsDialog
//
this.AcceptButton = this.importBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(584, 160);
this.Controls.Add(this.editBtn);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.importBtn);
this.Controls.Add(this.accountsClb);
this.Controls.Add(this.accountsLbl);
this.Name = "ScanAccountsDialog";
this.Text = "Which accounts?";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label accountsLbl;
private System.Windows.Forms.CheckedListBox accountsClb;
private System.Windows.Forms.Button importBtn;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.Button editBtn;
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsDesigner.Dialogs
{
public partial class ScanAccountsDialog : Form
{
public ScanAccountsDialog(Form1 parent)
{
InitializeComponent();
}
}
}

View File

@@ -28,95 +28,95 @@
/// </summary>
private void InitializeComponent()
{
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.label4 = new System.Windows.Forms.Label();
this.label5 = new System.Windows.Forms.Label();
this.closeBtn = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(358, 52);
this.label1.TabIndex = 0;
this.label1.Text = "Full Lucene query syntax is supported\r\nFields with similar names are synomyns (eg" +
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.label4 = new System.Windows.Forms.Label();
this.label5 = new System.Windows.Forms.Label();
this.closeBtn = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(358, 52);
this.label1.TabIndex = 0;
this.label1.Text = "Full Lucene query syntax is supported\r\nFields with similar names are synomyns (eg" +
": Author, Authors, AuthorNames)\r\n\r\nTAG FORMAT: [tagName]";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(12, 71);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(118, 65);
this.label2.TabIndex = 1;
this.label2.Text = "STRING FIELDS\r\n\r\nSearch for wizard of oz:\r\n title:oz\r\n title:\"wizard of o" +
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(12, 71);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(118, 65);
this.label2.TabIndex = 1;
this.label2.Text = "STRING FIELDS\r\n\r\nSearch for wizard of oz:\r\n title:oz\r\n title:\"wizard of o" +
"z\"";
//
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(233, 71);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(195, 78);
this.label3.TabIndex = 2;
this.label3.Text = "NUMBER FIELDS\r\n\r\nFind books between 1-100 minutes long\r\n length:[1 TO 100]\r\nF" +
//
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(233, 71);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(195, 78);
this.label3.TabIndex = 2;
this.label3.Text = "NUMBER FIELDS\r\n\r\nFind books between 1-100 minutes long\r\n length:[1 TO 100]\r\nF" +
"ind books exactly 1 hr long\r\n length:60";
//
// label4
//
this.label4.AutoSize = true;
this.label4.Location = new System.Drawing.Point(454, 71);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(168, 52);
this.label4.TabIndex = 3;
this.label4.Text = "BOOL FIELDS\r\n\r\nFind books that you haven\'t rated:\r\n -IsRated";
//
// label5
//
this.label5.AutoSize = true;
this.label5.Location = new System.Drawing.Point(673, 71);
this.label5.Name = "label5";
this.label5.Size = new System.Drawing.Size(257, 78);
this.label5.TabIndex = 4;
this.label5.Text = "ID FIELDS\r\n\r\nAlice\'s Adventures in Wonderland (ID: B015D78L0U)\r\n id:B015D78L0" +
//
// label4
//
this.label4.AutoSize = true;
this.label4.Location = new System.Drawing.Point(454, 71);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(168, 52);
this.label4.TabIndex = 3;
this.label4.Text = "BOOL FIELDS\r\n\r\nFind books that you haven\'t rated:\r\n -IsRated";
//
// label5
//
this.label5.AutoSize = true;
this.label5.Location = new System.Drawing.Point(673, 71);
this.label5.Name = "label5";
this.label5.Size = new System.Drawing.Size(257, 78);
this.label5.TabIndex = 4;
this.label5.Text = "ID FIELDS\r\n\r\nAlice\'s Adventures in Wonderland (ID: B015D78L0U)\r\n id:B015D78L0" +
"U\r\n\r\nAll of these are synonyms for the ID field";
//
// closeBtn
//
this.closeBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.closeBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.closeBtn.Location = new System.Drawing.Point(890, 415);
this.closeBtn.Name = "closeBtn";
this.closeBtn.Size = new System.Drawing.Size(75, 23);
this.closeBtn.TabIndex = 5;
this.closeBtn.Text = "Close";
this.closeBtn.UseVisualStyleBackColor = true;
//
// SearchSyntaxDialog
//
this.AcceptButton = this.closeBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.closeBtn;
this.ClientSize = new System.Drawing.Size(977, 450);
this.Controls.Add(this.closeBtn);
this.Controls.Add(this.label5);
this.Controls.Add(this.label4);
this.Controls.Add(this.label3);
this.Controls.Add(this.label2);
this.Controls.Add(this.label1);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "SearchSyntaxDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Filter options";
this.ResumeLayout(false);
this.PerformLayout();
//
// closeBtn
//
this.closeBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.closeBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.closeBtn.Location = new System.Drawing.Point(890, 465);
this.closeBtn.Name = "closeBtn";
this.closeBtn.Size = new System.Drawing.Size(75, 23);
this.closeBtn.TabIndex = 5;
this.closeBtn.Text = "Close";
this.closeBtn.UseVisualStyleBackColor = true;
//
// SearchSyntaxDialog
//
this.AcceptButton = this.closeBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.closeBtn;
this.ClientSize = new System.Drawing.Size(977, 500);
this.Controls.Add(this.closeBtn);
this.Controls.Add(this.label5);
this.Controls.Add(this.label4);
this.Controls.Add(this.label3);
this.Controls.Add(this.label2);
this.Controls.Add(this.label1);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "SearchSyntaxDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Filter options";
this.ResumeLayout(false);
this.PerformLayout();
}

View File

@@ -28,12 +28,9 @@
/// </summary>
private void InitializeComponent()
{
this.decryptKeyLbl = new System.Windows.Forms.Label();
this.decryptKeyTb = new System.Windows.Forms.TextBox();
this.booksLocationLbl = new System.Windows.Forms.Label();
this.booksLocationTb = new System.Windows.Forms.TextBox();
this.booksLocationSearchBtn = new System.Windows.Forms.Button();
this.decryptKeyDescLbl = new System.Windows.Forms.Label();
this.booksLocationDescLbl = new System.Windows.Forms.Label();
this.downloadsInProgressGb = new System.Windows.Forms.GroupBox();
this.downloadsInProgressLibationFilesRb = new System.Windows.Forms.RadioButton();
@@ -45,30 +42,12 @@
this.decryptInProgressDescLbl = new System.Windows.Forms.Label();
this.saveBtn = new System.Windows.Forms.Button();
this.cancelBtn = new System.Windows.Forms.Button();
this.audibleLocaleLbl = new System.Windows.Forms.Label();
this.audibleLocaleCb = new System.Windows.Forms.ComboBox();
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.downloadsInProgressGb.SuspendLayout();
this.decryptInProgressGb.SuspendLayout();
this.groupBox1.SuspendLayout();
this.SuspendLayout();
//
// decryptKeyLbl
//
this.decryptKeyLbl.AutoSize = true;
this.decryptKeyLbl.Location = new System.Drawing.Point(6, 22);
this.decryptKeyLbl.Name = "decryptKeyLbl";
this.decryptKeyLbl.Size = new System.Drawing.Size(64, 13);
this.decryptKeyLbl.TabIndex = 0;
this.decryptKeyLbl.Text = "Decrypt key";
//
// decryptKeyTb
//
this.decryptKeyTb.Location = new System.Drawing.Point(76, 19);
this.decryptKeyTb.Name = "decryptKeyTb";
this.decryptKeyTb.Size = new System.Drawing.Size(100, 20);
this.decryptKeyTb.TabIndex = 1;
//
// booksLocationLbl
//
this.booksLocationLbl.AutoSize = true;
@@ -94,15 +73,6 @@
this.booksLocationSearchBtn.Text = "...";
this.booksLocationSearchBtn.UseVisualStyleBackColor = true;
//
// decryptKeyDescLbl
//
this.decryptKeyDescLbl.AutoSize = true;
this.decryptKeyDescLbl.Location = new System.Drawing.Point(73, 42);
this.decryptKeyDescLbl.Name = "decryptKeyDescLbl";
this.decryptKeyDescLbl.Size = new System.Drawing.Size(36, 13);
this.decryptKeyDescLbl.TabIndex = 2;
this.decryptKeyDescLbl.Text = "[desc]";
//
// booksLocationDescLbl
//
this.booksLocationDescLbl.AutoSize = true;
@@ -117,7 +87,7 @@
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressLibationFilesRb);
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressWinTempRb);
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressDescLbl);
this.downloadsInProgressGb.Location = new System.Drawing.Point(15, 58);
this.downloadsInProgressGb.Location = new System.Drawing.Point(15, 19);
this.downloadsInProgressGb.Name = "downloadsInProgressGb";
this.downloadsInProgressGb.Size = new System.Drawing.Size(758, 117);
this.downloadsInProgressGb.TabIndex = 4;
@@ -162,7 +132,7 @@
this.decryptInProgressGb.Controls.Add(this.decryptInProgressLibationFilesRb);
this.decryptInProgressGb.Controls.Add(this.decryptInProgressWinTempRb);
this.decryptInProgressGb.Controls.Add(this.decryptInProgressDescLbl);
this.decryptInProgressGb.Location = new System.Drawing.Point(9, 183);
this.decryptInProgressGb.Location = new System.Drawing.Point(9, 144);
this.decryptInProgressGb.Name = "decryptInProgressGb";
this.decryptInProgressGb.Size = new System.Drawing.Size(758, 117);
this.decryptInProgressGb.TabIndex = 5;
@@ -205,7 +175,7 @@
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(612, 404);
this.saveBtn.Location = new System.Drawing.Point(612, 328);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(75, 23);
this.saveBtn.TabIndex = 7;
@@ -216,47 +186,20 @@
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(713, 404);
this.cancelBtn.Location = new System.Drawing.Point(713, 328);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(75, 23);
this.cancelBtn.TabIndex = 8;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
//
// audibleLocaleLbl
//
this.audibleLocaleLbl.AutoSize = true;
this.audibleLocaleLbl.Location = new System.Drawing.Point(12, 56);
this.audibleLocaleLbl.Name = "audibleLocaleLbl";
this.audibleLocaleLbl.Size = new System.Drawing.Size(77, 13);
this.audibleLocaleLbl.TabIndex = 4;
this.audibleLocaleLbl.Text = "Audible Locale";
//
// audibleLocaleCb
//
this.audibleLocaleCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.audibleLocaleCb.FormattingEnabled = true;
this.audibleLocaleCb.Items.AddRange(new object[] {
"us",
"uk",
"germany",
"france",
"canada"});
this.audibleLocaleCb.Location = new System.Drawing.Point(95, 53);
this.audibleLocaleCb.Name = "audibleLocaleCb";
this.audibleLocaleCb.Size = new System.Drawing.Size(53, 21);
this.audibleLocaleCb.TabIndex = 5;
//
// groupBox1
//
this.groupBox1.Controls.Add(this.decryptKeyTb);
this.groupBox1.Controls.Add(this.decryptKeyLbl);
this.groupBox1.Controls.Add(this.decryptKeyDescLbl);
this.groupBox1.Controls.Add(this.downloadsInProgressGb);
this.groupBox1.Controls.Add(this.decryptInProgressGb);
this.groupBox1.Location = new System.Drawing.Point(15, 90);
this.groupBox1.Location = new System.Drawing.Point(15, 53);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(773, 308);
this.groupBox1.Size = new System.Drawing.Size(773, 269);
this.groupBox1.TabIndex = 6;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "Advanced settings for control freaks";
@@ -267,10 +210,8 @@
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(800, 439);
this.ClientSize = new System.Drawing.Size(800, 363);
this.Controls.Add(this.groupBox1);
this.Controls.Add(this.audibleLocaleCb);
this.Controls.Add(this.audibleLocaleLbl);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn);
this.Controls.Add(this.booksLocationDescLbl);
@@ -286,19 +227,15 @@
this.decryptInProgressGb.ResumeLayout(false);
this.decryptInProgressGb.PerformLayout();
this.groupBox1.ResumeLayout(false);
this.groupBox1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label decryptKeyLbl;
private System.Windows.Forms.TextBox decryptKeyTb;
private System.Windows.Forms.Label booksLocationLbl;
private System.Windows.Forms.TextBox booksLocationTb;
private System.Windows.Forms.Button booksLocationSearchBtn;
private System.Windows.Forms.Label decryptKeyDescLbl;
private System.Windows.Forms.Label booksLocationDescLbl;
private System.Windows.Forms.GroupBox downloadsInProgressGb;
private System.Windows.Forms.Label downloadsInProgressDescLbl;
@@ -310,8 +247,6 @@
private System.Windows.Forms.RadioButton decryptInProgressWinTempRb;
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.Label audibleLocaleLbl;
private System.Windows.Forms.ComboBox audibleLocaleCb;
private System.Windows.Forms.GroupBox groupBox1;
}
}

View File

@@ -1,33 +1,33 @@
namespace WinFormsDesigner
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.gridPanel = new System.Windows.Forms.Panel();
this.filterHelpBtn = new System.Windows.Forms.Button();
@@ -35,15 +35,20 @@
this.filterSearchTb = new System.Windows.Forms.TextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.advancedSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
@@ -52,6 +57,7 @@
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.addFilterBtn = new System.Windows.Forms.Button();
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
this.SuspendLayout();
@@ -99,6 +105,7 @@
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.importToolStripMenuItem,
this.liberateToolStripMenuItem,
this.exportToolStripMenuItem,
this.quickFiltersToolStripMenuItem,
this.settingsToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
@@ -110,17 +117,38 @@
// importToolStripMenuItem
//
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.scanLibraryToolStripMenuItem});
this.noAccountsYetAddAccountToolStripMenuItem,
this.scanLibraryToolStripMenuItem,
this.scanLibraryOfAllAccountsToolStripMenuItem,
this.scanLibraryOfSomeAccountsToolStripMenuItem});
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
this.importToolStripMenuItem.Text = "&Import";
//
// noAccountsYetAddAccountToolStripMenuItem
//
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
//
// scanLibraryToolStripMenuItem
//
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(138, 22);
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
//
// scanLibraryOfAllAccountsToolStripMenuItem
//
this.scanLibraryOfAllAccountsToolStripMenuItem.Name = "scanLibraryOfAllAccountsToolStripMenuItem";
this.scanLibraryOfAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfAllAccountsToolStripMenuItem.Text = "Scan Library of &All Accounts";
//
// scanLibraryOfSomeAccountsToolStripMenuItem
//
this.scanLibraryOfSomeAccountsToolStripMenuItem.Name = "scanLibraryOfSomeAccountsToolStripMenuItem";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts...";
//
// liberateToolStripMenuItem
//
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
@@ -142,6 +170,14 @@
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(248, 22);
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
//
// exportToolStripMenuItem
//
this.exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.exportLibraryToolStripMenuItem});
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
this.exportToolStripMenuItem.Size = new System.Drawing.Size(53, 20);
this.exportToolStripMenuItem.Text = "E&xport";
//
// quickFiltersToolStripMenuItem
//
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
@@ -162,7 +198,7 @@
//
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters";
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters...";
//
// toolStripSeparator1
//
@@ -172,23 +208,30 @@
// settingsToolStripMenuItem
//
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.accountsToolStripMenuItem,
this.basicSettingsToolStripMenuItem,
this.advancedSettingsToolStripMenuItem});
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.settingsToolStripMenuItem.Text = "&Settings";
//
// accountsToolStripMenuItem
//
this.accountsToolStripMenuItem.Name = "accountsToolStripMenuItem";
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
this.accountsToolStripMenuItem.Text = "&Accounts...";
//
// basicSettingsToolStripMenuItem
//
this.basicSettingsToolStripMenuItem.Name = "basicSettingsToolStripMenuItem";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.basicSettingsToolStripMenuItem.Text = "&Basic Settings";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
this.basicSettingsToolStripMenuItem.Text = "&Basic Settings...";
//
// advancedSettingsToolStripMenuItem
//
this.advancedSettingsToolStripMenuItem.Name = "advancedSettingsToolStripMenuItem";
this.advancedSettingsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.advancedSettingsToolStripMenuItem.Text = "&Advanced Settings";
this.advancedSettingsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
this.advancedSettingsToolStripMenuItem.Text = "Ad&vanced Settings...";
//
// statusStrip1
//
@@ -212,7 +255,7 @@
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(232, 17);
this.springLbl.Size = new System.Drawing.Size(233, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl
@@ -224,7 +267,7 @@
// pdfsCountsLbl
//
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
this.pdfsCountsLbl.Size = new System.Drawing.Size(219, 17);
this.pdfsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.pdfsCountsLbl.Text = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
//
// addFilterBtn
@@ -236,6 +279,12 @@
this.addFilterBtn.Text = "Add To Quick Filters";
this.addFilterBtn.UseVisualStyleBackColor = true;
//
// exportLibraryToolStripMenuItem
//
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -259,32 +308,38 @@
this.ResumeLayout(false);
this.PerformLayout();
}
}
#endregion
private System.Windows.Forms.Panel gridPanel;
private System.Windows.Forms.MenuStrip menuStrip1;
private System.Windows.Forms.ToolStripMenuItem importToolStripMenuItem;
private System.Windows.Forms.StatusStrip statusStrip1;
private System.Windows.Forms.ToolStripStatusLabel springLbl;
private System.Windows.Forms.ToolStripStatusLabel visibleCountLbl;
private System.Windows.Forms.ToolStripMenuItem liberateToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel backupsCountsLbl;
private System.Windows.Forms.ToolStripMenuItem beginBookBackupsToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel pdfsCountsLbl;
private System.Windows.Forms.ToolStripMenuItem beginPdfBackupsToolStripMenuItem;
private System.Windows.Forms.TextBox filterSearchTb;
private System.Windows.Forms.Button filterBtn;
private System.Windows.Forms.Button filterHelpBtn;
private System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem quickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem firstFilterIsDefaultToolStripMenuItem;
private System.Windows.Forms.Button addFilterBtn;
private System.Windows.Forms.ToolStripMenuItem editQuickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
#endregion
private System.Windows.Forms.Panel gridPanel;
private System.Windows.Forms.MenuStrip menuStrip1;
private System.Windows.Forms.ToolStripMenuItem importToolStripMenuItem;
private System.Windows.Forms.StatusStrip statusStrip1;
private System.Windows.Forms.ToolStripStatusLabel springLbl;
private System.Windows.Forms.ToolStripStatusLabel visibleCountLbl;
private System.Windows.Forms.ToolStripMenuItem liberateToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel backupsCountsLbl;
private System.Windows.Forms.ToolStripMenuItem beginBookBackupsToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel pdfsCountsLbl;
private System.Windows.Forms.ToolStripMenuItem beginPdfBackupsToolStripMenuItem;
private System.Windows.Forms.TextBox filterSearchTb;
private System.Windows.Forms.Button filterBtn;
private System.Windows.Forms.Button filterHelpBtn;
private System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem quickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem firstFilterIsDefaultToolStripMenuItem;
private System.Windows.Forms.Button addFilterBtn;
private System.Windows.Forms.ToolStripMenuItem editQuickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem basicSettingsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem advancedSettingsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem accountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryOfAllAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryOfSomeAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem noAccountsYetAddAccountToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exportToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exportLibraryToolStripMenuItem;
}
}

View File

@@ -12,5 +12,5 @@ namespace WinFormsDesigner
{
InitializeComponent();
}
}
}
}

View File

@@ -65,6 +65,12 @@
<Compile Include="BookLiberation\DecryptForm.Designer.cs">
<DependentUpon>DecryptForm.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\AccountsDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\AccountsDialog.Designer.cs">
<DependentUpon>AccountsDialog.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\EditQuickFilters.cs">
<SubType>Form</SubType>
</Compile>
@@ -83,6 +89,12 @@
<Compile Include="Dialogs\LibationFilesDialog.Designer.cs">
<DependentUpon>LibationFilesDialog.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Login\ApprovalNeededDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\Login\ApprovalNeededDialog.Designer.cs">
<DependentUpon>ApprovalNeededDialog.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\Login\AudibleLoginDialog.cs">
<SubType>Form</SubType>
</Compile>
@@ -107,6 +119,12 @@
<Compile Include="Dialogs\EditTagsDialog.Designer.cs">
<DependentUpon>EditTagsDialog.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\ScanAccountsDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\ScanAccountsDialog.Designer.cs">
<DependentUpon>ScanAccountsDialog.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\SettingsDialog.cs">
<SubType>Form</SubType>
</Compile>
@@ -148,8 +166,12 @@
<EmbeddedResource Include="BookLiberation\DecryptForm.resx">
<DependentUpon>DecryptForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialogs\AccountsDialog.resx">
<DependentUpon>AccountsDialog.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialogs\EditQuickFilters.resx">
<DependentUpon>EditQuickFilters.cs</DependentUpon>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Dialogs\EditTagsDialog.resx">
<DependentUpon>EditTagsDialog.cs</DependentUpon>
@@ -160,14 +182,8 @@
<EmbeddedResource Include="Dialogs\LibationFilesDialog.resx">
<DependentUpon>LibationFilesDialog.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialogs\Login\AudibleLoginDialog.resx">
<DependentUpon>AudibleLoginDialog.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialogs\Login\CaptchaDialog.resx">
<DependentUpon>CaptchaDialog.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialogs\Login\_2faCodeDialog.resx">
<DependentUpon>_2faCodeDialog.cs</DependentUpon>
<EmbeddedResource Include="Dialogs\ScanAccountsDialog.resx">
<DependentUpon>ScanAccountsDialog.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialogs\SettingsDialog.resx">
<DependentUpon>SettingsDialog.cs</DependentUpon>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon />
<StartupObject />

View File

@@ -25,9 +25,9 @@ View > Other Windows > Package Manager Console
Default project: DataLayer
Startup project: DataLayer
since we have mult contexts, must use -context:
Add-Migration MyComment -context LibationContext
Update-Database -context LibationContext
Startup project: reset to prev. eg: LibationLauncher
ERROR

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<RootNamespace>ffmpeg_decrypt</RootNamespace>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>

View File

@@ -0,0 +1,696 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi.Authorization;
using Dinah.Core;
using FluentAssertions;
using FluentAssertions.Common;
using InternalUtilities;
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using TestAudibleApiCommon;
using TestCommon;
using static AuthorizationShared.Shared;
using static AuthorizationShared.Shared.AccessTokenTemporality;
using static TestAudibleApiCommon.ComputedTestValues;
namespace AccountsTests
{
public class AccountsTestBase
{
protected const string EMPTY_FILE = "{\r\n \"Accounts\": []\r\n}";
protected string TestFile;
protected Locale usLocale => Localization.Get("us");
protected Locale ukLocale => Localization.Get("uk");
protected void WriteToTestFile(string contents)
=> File.WriteAllText(TestFile, contents);
[TestInitialize]
public void TestInit()
=> TestFile = Guid.NewGuid() + ".txt";
[TestCleanup]
public void TestCleanup()
{
if (File.Exists(TestFile))
File.Delete(TestFile);
}
}
[TestClass]
public class FromJson : AccountsTestBase
{
[TestMethod]
public void _0_accounts()
{
var accountsSettings = AccountsSettings.FromJson(EMPTY_FILE);
accountsSettings.Accounts.Count.Should().Be(0);
}
[TestMethod]
public void _1_account_new()
{
var json = @"
{
""Accounts"": [
{
""AccountId"": ""cng"",
""AccountName"": ""my main login"",
""DecryptKey"": ""asdfasdf"",
""IdentityTokens"": null
}
]
}
".Trim();
var accountsSettings = AccountsSettings.FromJson(json);
accountsSettings.Accounts.Count.Should().Be(1);
accountsSettings.Accounts[0].AccountId.Should().Be("cng");
accountsSettings.Accounts[0].IdentityTokens.Should().BeNull();
}
[TestMethod]
public void _1_account_populated()
{
var id = GetIdentityJson(Future);
var json = $@"
{{
""Accounts"": [
{{
""AccountId"": ""cng"",
""AccountName"": ""my main login"",
""DecryptKey"": ""asdfasdf"",
""IdentityTokens"": {id}
}}
]
}}
".Trim();
var accountsSettings = AccountsSettings.FromJson(json);
accountsSettings.Accounts.Count.Should().Be(1);
accountsSettings.Accounts[0].AccountId.Should().Be("cng");
accountsSettings.Accounts[0].IdentityTokens.Should().NotBeNull();
accountsSettings.Accounts[0].IdentityTokens.ExistingAccessToken.TokenValue.Should().Be(AccessTokenValue);
}
}
[TestClass]
public class ToJson
{
[TestMethod]
public void serialize()
{
var id = JsonConvert.SerializeObject(Identity.Empty, Identity.GetJsonSerializerSettings());
var jsonIn = $@"
{{
""Accounts"": [
{{
""AccountId"": ""cng"",
""AccountName"": ""my main login"",
""DecryptKey"": ""asdfasdf"",
""IdentityTokens"": {id}
}}
]
}}
".Trim();
var accountsSettings = AccountsSettings.FromJson(jsonIn);
var jsonOut = accountsSettings.ToJson();
jsonOut.Should().Be(@"
{
""Accounts"": [
{
""AccountId"": ""cng"",
""AccountName"": ""my main login"",
""LibraryScan"": true,
""DecryptKey"": ""asdfasdf"",
""IdentityTokens"": {
""LocaleName"": ""[empty]"",
""ExistingAccessToken"": {
""TokenValue"": ""Atna|"",
""Expires"": ""0001-01-01T00:00:00""
},
""PrivateKey"": null,
""AdpToken"": null,
""RefreshToken"": null,
""Cookies"": []
}
}
]
}
".Trim());
}
}
[TestClass]
public class ctor : AccountsTestBase
{
[TestMethod]
public void create_file()
{
File.Exists(TestFile).Should().BeFalse();
var accountsSettings = new AccountsSettings();
_ = new AccountsSettingsPersister(accountsSettings, TestFile);
File.Exists(TestFile).Should().BeTrue();
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
}
[TestMethod]
public void overwrite_existing_file()
{
File.Exists(TestFile).Should().BeFalse();
WriteToTestFile("foo");
File.Exists(TestFile).Should().BeTrue();
var accountsSettings = new AccountsSettings();
_ = new AccountsSettingsPersister(accountsSettings, TestFile);
File.Exists(TestFile).Should().BeTrue();
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
}
[TestMethod]
public void save_multiple_children()
{
var accountsSettings = new AccountsSettings();
accountsSettings.Add(new Account("a0") { AccountName = "n0" });
accountsSettings.Add(new Account("a1") { AccountName = "n1" });
// dispose to cease auto-updates
using (var p = new AccountsSettingsPersister(accountsSettings, TestFile)) { }
var persister = new AccountsSettingsPersister(TestFile);
persister.AccountsSettings.Accounts.Count.Should().Be(2);
persister.AccountsSettings.Accounts[1].AccountName.Should().Be("n1");
}
[TestMethod]
public void save_with_identity()
{
var id = new Identity(usLocale);
var idJson = JsonConvert.SerializeObject(id, Identity.GetJsonSerializerSettings());
var accountsSettings = new AccountsSettings();
accountsSettings.Add(new Account("a0") { AccountName = "n0", IdentityTokens = id });
// dispose to cease auto-updates
using (var p = new AccountsSettingsPersister(accountsSettings, TestFile)) { }
var persister = new AccountsSettingsPersister(TestFile);
var acct = persister.AccountsSettings.Accounts[0];
acct.AccountName.Should().Be("n0");
acct.Locale.CountryCode.Should().Be("us");
}
}
[TestClass]
public class save : AccountsTestBase
{
// add/save account after file creation
[TestMethod]
public void save_1_account()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create account
using (var p = new AccountsSettingsPersister(TestFile))
{
var idIn = new Identity(usLocale);
var acctIn = new Account("a0") { AccountName = "n0", IdentityTokens = idIn };
p.AccountsSettings.Add(acctIn);
}
// re-load file. ensure account still exists
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(1);
var acct0 = p.AccountsSettings.Accounts[0];
acct0.AccountName.Should().Be("n0");
acct0.Locale.CountryCode.Should().Be("us");
}
}
// add/save mult accounts after file creation
// separately create 2 accounts. ensure both still exist in the end
[TestMethod]
public void save_2_accounts()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create account 0
using (var p = new AccountsSettingsPersister(TestFile))
{
var idIn = new Identity(usLocale);
var acctIn = new Account("a0") { AccountName = "n0", IdentityTokens = idIn };
p.AccountsSettings.Add(acctIn);
}
// re-load file. ensure account still exists
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(1);
var acct0 = p.AccountsSettings.Accounts[0];
acct0.AccountName.Should().Be("n0");
acct0.Locale.CountryCode.Should().Be("us");
}
// load file. create account 1
using (var p = new AccountsSettingsPersister(TestFile))
{
var idIn = new Identity(ukLocale);
var acctIn = new Account("a1") { AccountName = "n1", IdentityTokens = idIn };
p.AccountsSettings.Add(acctIn);
}
// re-load file. ensure both accounts still exist
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(2);
var acct0 = p.AccountsSettings.Accounts[0];
acct0.AccountName.Should().Be("n0");
acct0.Locale.CountryCode.Should().Be("us");
var acct1 = p.AccountsSettings.Accounts[1];
acct1.AccountName.Should().Be("n1");
acct1.Locale.CountryCode.Should().Be("uk");
}
}
[TestMethod]
public void update_Account_field_just_added()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create 2 accounts
using (var p = new AccountsSettingsPersister(TestFile))
{
var id1 = new Identity(usLocale);
var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 };
p.AccountsSettings.Add(acct1);
// update just-added item. note: this is different than the subscription which happens on initial collection load. ensure this works also
acct1.AccountName = "new";
}
// verify save property
using (var p = new AccountsSettingsPersister(TestFile))
{
var acct0 = p.AccountsSettings.Accounts[0];
acct0.AccountName.Should().Be("new");
}
}
// update Account property. must be non-destructive to all other data
[TestMethod]
public void update_Account_field()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create 2 accounts
using (var p = new AccountsSettingsPersister(TestFile))
{
var id1 = new Identity(usLocale);
var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 };
p.AccountsSettings.Add(acct1);
var id2 = new Identity(ukLocale);
var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 };
p.AccountsSettings.Add(acct2);
}
// update AccountName on existing file
using (var p = new AccountsSettingsPersister(TestFile))
{
var acct0 = p.AccountsSettings.Accounts[0];
acct0.AccountName = "new";
}
// re-load file. ensure both accounts still exist
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(2);
var acct0 = p.AccountsSettings.Accounts[0];
// new
acct0.AccountName.Should().Be("new");
// still here
acct0.Locale.CountryCode.Should().Be("us");
var acct1 = p.AccountsSettings.Accounts[1];
acct1.AccountName.Should().Be("n1");
acct1.Locale.CountryCode.Should().Be("uk");
}
}
// update identity. must be non-destructive to all other data
[TestMethod]
public void replace_identity()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create 2 accounts
using (var p = new AccountsSettingsPersister(TestFile))
{
var id1 = new Identity(usLocale);
var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 };
p.AccountsSettings.Add(acct1);
var id2 = new Identity(ukLocale);
var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 };
p.AccountsSettings.Add(acct2);
}
// update identity on existing file
using (var p = new AccountsSettingsPersister(TestFile))
{
var id = new Identity(ukLocale);
var acct0 = p.AccountsSettings.Accounts[0];
acct0.IdentityTokens = id;
}
// re-load file. ensure both accounts still exist
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(2);
var acct0 = p.AccountsSettings.Accounts[0];
// new
acct0.Locale.CountryCode.Should().Be("uk");
// still here
acct0.AccountName.Should().Be("n0");
var acct1 = p.AccountsSettings.Accounts[1];
acct1.AccountName.Should().Be("n1");
acct1.Locale.CountryCode.Should().Be("uk");
}
}
// multi-level subscribe => update
// edit field of existing identity. must be non-destructive to all other data
[TestMethod]
public void update_identity_field()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create 2 accounts
using (var p = new AccountsSettingsPersister(TestFile))
{
var id1 = new Identity(usLocale);
var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 };
p.AccountsSettings.Add(acct1);
var id2 = new Identity(ukLocale);
var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 };
p.AccountsSettings.Add(acct2);
}
// update identity on existing file
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts[0]
.IdentityTokens
.Update(new AccessToken("Atna|_NEW_", DateTime.Now.AddDays(1)));
}
// re-load file. ensure both accounts still exist
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(2);
var acct0 = p.AccountsSettings.Accounts[0];
// new
acct0.IdentityTokens.ExistingAccessToken.TokenValue.Should().Be("Atna|_NEW_");
// still here
acct0.AccountName.Should().Be("n0");
acct0.Locale.CountryCode.Should().Be("us");
var acct1 = p.AccountsSettings.Accounts[1];
acct1.AccountName.Should().Be("n1");
acct1.Locale.CountryCode.Should().Be("uk");
}
}
}
[TestClass]
public class retrieve : AccountsTestBase
{
[TestMethod]
public void get_where()
{
var idUs = new Identity(usLocale);
var acct1 = new Account("cng") { IdentityTokens = idUs, AccountName = "foo" };
var idUk = new Identity(ukLocale);
var acct2 = new Account("cng") { IdentityTokens = idUk, AccountName = "bar" };
var accountsSettings = new AccountsSettings();
accountsSettings.Add(acct1);
accountsSettings.Add(acct2);
accountsSettings.GetAccount("cng", "uk").AccountName.Should().Be("bar");
}
}
[TestClass]
public class upsert : AccountsTestBase
{
[TestMethod]
public void upsert_new()
{
var accountsSettings = new AccountsSettings();
accountsSettings.Accounts.Count.Should().Be(0);
accountsSettings.Upsert("cng", "us");
accountsSettings.Accounts.Count.Should().Be(1);
accountsSettings.GetAccount("cng", "us").AccountId.Should().Be("cng");
}
[TestMethod]
public void upsert_exists()
{
var accountsSettings = new AccountsSettings();
var orig = accountsSettings.Upsert("cng", "us");
orig.AccountName = "foo";
var exists = accountsSettings.Upsert("cng", "us");
exists.AccountName.Should().Be("foo");
orig.Should().IsSameOrEqualTo(exists);
}
}
[TestClass]
public class delete : AccountsTestBase
{
[TestMethod]
public void delete_account()
{
var accountsSettings = new AccountsSettings();
var acct = accountsSettings.Upsert("cng", "us");
accountsSettings.Accounts.Count.Should().Be(1);
var removed = accountsSettings.Delete(acct);
removed.Should().BeTrue();
accountsSettings.Accounts.Count.Should().Be(0);
}
[TestMethod]
public void delete_where()
{
var accountsSettings = new AccountsSettings();
_ = accountsSettings.Upsert("cng", "us");
accountsSettings.Accounts.Count.Should().Be(1);
accountsSettings.Delete("baz", "baz").Should().BeFalse();
accountsSettings.Accounts.Count.Should().Be(1);
accountsSettings.Delete("cng", "us").Should().BeTrue();
accountsSettings.Accounts.Count.Should().Be(0);
}
[TestMethod]
public void delete_updates()
{
var i = 0;
void update(object sender, EventArgs e) => i++;
var accountsSettings = new AccountsSettings();
accountsSettings.Updated += update;
accountsSettings.Accounts.Count.Should().Be(0);
i.Should().Be(0);
_ = accountsSettings.Upsert("cng", "us");
accountsSettings.Accounts.Count.Should().Be(1);
i.Should().Be(1);
accountsSettings.Delete("baz", "baz").Should().BeFalse();
accountsSettings.Accounts.Count.Should().Be(1);
i.Should().Be(1);
accountsSettings.Delete("cng", "us").Should().BeTrue();
accountsSettings.Accounts.Count.Should().Be(0);
i.Should().Be(2); // <== this is the one being tested
}
[TestMethod]
public void deleted_account_should_not_persist_file()
{
Account acct;
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile))
{
acct = p.AccountsSettings.Upsert("foo", "us");
p.AccountsSettings.Accounts.Count.Should().Be(1);
acct.AccountName = "old";
}
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile))
{
p.AccountsSettings.Delete(acct);
p.AccountsSettings.Accounts.Count.Should().Be(0);
}
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile))
{
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
acct.AccountName = "new";
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
}
}
}
// account.Id + Locale.Name -- must be unique
[TestClass]
public class validate : AccountsTestBase
{
[TestMethod]
public void violate_validation()
{
var accountsSettings = new AccountsSettings();
var idIn = new Identity(usLocale);
var a1 = new Account("a") { AccountName = "one", IdentityTokens = idIn };
accountsSettings.Add(a1);
var a2 = new Account("a") { AccountName = "two", IdentityTokens = idIn };
// violation: validate()
Assert.ThrowsException<InvalidOperationException>(() => accountsSettings.Add(a2));
}
[TestMethod]
public void identity_violate_validation()
{
var accountsSettings = new AccountsSettings();
var idIn = new Identity(usLocale);
var a1 = new Account("a") { AccountName = "one", IdentityTokens = idIn };
accountsSettings.Add(a1);
var a2 = new Account("a") { AccountName = "two" };
accountsSettings.Add(a2);
// violation: GetAccount.SingleOrDefault
Assert.ThrowsException<InvalidOperationException>(() => a2.IdentityTokens = idIn);
}
}
[TestClass]
public class transactions : AccountsTestBase
{
[TestMethod]
public void atomic_update_at_end()
{
var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile);
p.BeginTransation();
// upserted account will not persist until CommitTransation
var acct = p.AccountsSettings.Upsert("cng", "us");
acct.AccountName = "foo";
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
p.IsInTransaction.Should().BeTrue();
p.CommitTransation();
p.IsInTransaction.Should().BeFalse();
var jsonOut = File.ReadAllText(TestFile);//.Should().Be(EMPTY_FILE);
jsonOut.Should().Be(@"
{
""Accounts"": [
{
""AccountId"": ""cng"",
""AccountName"": ""foo"",
""LibraryScan"": true,
""DecryptKey"": """",
""IdentityTokens"": {
""LocaleName"": ""us"",
""ExistingAccessToken"": {
""TokenValue"": ""Atna|"",
""Expires"": ""0001-01-01T00:00:00""
},
""PrivateKey"": null,
""AdpToken"": null,
""RefreshToken"": null,
""Cookies"": []
}
}
]
}
".Trim());
}
[TestMethod]
public void abandoned_transaction()
{
var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile);
try
{
p.BeginTransation();
var acct = p.AccountsSettings.Upsert("cng", "us");
acct.AccountName = "foo";
throw new Exception();
}
catch { }
finally
{
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
p.IsInTransaction.Should().BeTrue();
}
}
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
<PackageReference Include="coverlet.collector" Version="1.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\audible api\AudibleApi\_Tests\AudibleApi.Tests\AudibleApi.Tests.csproj" />
<ProjectReference Include="..\..\..\Dinah.Core\_Tests\TestCommon\TestCommon.csproj" />
<ProjectReference Include="..\..\InternalUtilities\InternalUtilities.csproj" />
</ItemGroup>
</Project>

BIN
images/Export.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
images/v40_accounts.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
images/v40_import.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
images/v40_locales.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB