Compare commits

...

4 Commits

Author SHA1 Message Date
Robert McRackan
f3128b562d Fix performance issues, esp regarding saving tags 2019-11-18 14:37:17 -05:00
Robert McRackan
6734dec55c remove TODO from git 2019-11-16 21:30:52 -05:00
Robert McRackan
b9314ac678 Added validation and error handling
BETA READY
2019-11-15 22:43:04 -05:00
Robert McRackan
e319326c30 Switch to SQLite 2019-11-15 16:34:16 -05:00
22 changed files with 212 additions and 403 deletions

4
.gitignore vendored
View File

@@ -328,3 +328,7 @@ ASALocalRun/
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# manually ignored files
/__TODO.txt

View File

@@ -23,14 +23,12 @@ namespace ApplicationServices
return (totalCount, newCount);
}
public static int IndexChangedTags(Book book)
public static int UpdateTags(this LibationContext context, Book book, string newTags)
{
// update disconnected entity
using var context = LibationContext.Create();
context.Update(book);
book.UserDefinedItem.Tags = newTags;
var qtyChanges = context.SaveChanges();
// this part is tags-specific
if (qtyChanges > 0)
SearchEngineCommands.UpdateBookTags(book);

View File

@@ -1,5 +1,4 @@
using System.Threading.Tasks;
using DataLayer;
using DataLayer;
using LibationSearchEngine;
namespace ApplicationServices

View File

@@ -61,12 +61,17 @@ namespace DataLayer
string title,
string description,
int lengthInMinutes,
IEnumerable<Contributor> authors)
IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators)
{
// validate
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
var productId = audibleProductId.Id;
ArgumentValidator.EnsureNotNullOrWhiteSpace(productId, nameof(productId));
// assign as soon as possible. stuff below relies on this
AudibleProductId = productId;
ArgumentValidator.EnsureNotNullOrWhiteSpace(title, nameof(title));
// non-ef-ctor init.s
@@ -79,19 +84,13 @@ namespace DataLayer
CategoryId = Category.GetEmpty().CategoryId;
// simple assigns
AudibleProductId = productId;
Title = title;
Description = description;
LengthInMinutes = lengthInMinutes;
// assigns with biz logic
ReplaceAuthors(authors);
//ReplaceNarrators(narrators);
// import previously saved tags
// do this immediately. any save occurs before reloading tags will overwrite persistent tags with new blank entries; all old persisted tags will be lost
// if refactoring, DO NOT use "ProductId" before it's assigned to. to be safe, just use "productId"
UserDefinedItem = new UserDefinedItem(this) { Tags = FileManager.TagsPersistence.GetTags(productId) };
ReplaceNarrators(narrators);
}
#region contributors, authors, narrators

View File

@@ -13,29 +13,36 @@ namespace DataLayer
private UserDefinedItem() { }
internal UserDefinedItem(Book book)
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
Book = book;
}
// import previously saved tags
ArgumentValidator.EnsureNotNullOrWhiteSpace(book.AudibleProductId, nameof(book.AudibleProductId));
Tags = FileManager.TagsPersistence.GetTags(book.AudibleProductId);
}
private string _tags = "";
public string Tags
{
get => _tags;
set => _tags = sanitize(value);
}
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
// only legal chars are letters numbers underscores and separating whitespace
//
// technically, the only char.s which aren't easily supported are \ [ ]
// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
// it's easy to expand whitelist as needed
// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
//
// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
// full list of characters which must be escaped:
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
static Regex regex = new Regex(@"[^\w\d\s_]", RegexOptions.Compiled);
}
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
// only legal chars are letters numbers underscores and separating whitespace
//
// technically, the only char.s which aren't easily supported are \ [ ]
// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
// it's easy to expand whitelist as needed
// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
//
// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
// full list of characters which must be escaped:
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
static Regex regex { get; } = new Regex(@"[^\w\d\s_]", RegexOptions.Compiled);
private static string sanitize(string input)
{
if (string.IsNullOrWhiteSpace(input))
@@ -63,8 +70,6 @@ namespace DataLayer
return string.Join(" ", unique);
}
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
#endregion
// owned: not an optional one-to-one

View File

@@ -6,6 +6,9 @@ namespace DataLayer
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
{
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlServer(connectionString);
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder
//.UseSqlServer
.UseSqlite
(connectionString);
}
}

View File

@@ -5,13 +5,19 @@ using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
public static class LibraryQueries
{
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
{
public static List<LibraryBook> GetLibrary_Flat_WithTracking(this LibationContext context)
=> context
.Library
.GetLibrary()
.ToList();
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
{
using var context = LibationContext.Create();
return context
.Library
//.AsNoTracking()
.AsNoTracking()
.GetLibrary()
.ToList();
}
@@ -21,7 +27,7 @@ namespace DataLayer
using var context = LibationContext.Create();
return context
.Library
//.AsNoTracking()
.AsNoTracking()
.GetLibraryBook(productId);
}

View File

@@ -4,57 +4,35 @@ using System.Linq;
using Dinah.Core.Collections.Generic;
using Dinah.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace DataLayer
{
internal class TagPersistenceInterceptor : IDbInterceptor
{
public void Executing(DbContext context)
{
doWork__EFCore(context);
}
public void Executed(DbContext context) { }
static void doWork__EFCore(DbContext context)
{
// persist tags:
var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State.In(EntityState.Modified, EntityState.Added)).ToList();
var tagSets = modifiedEntities.Select(e => e.Entity as UserDefinedItem).Where(a => a != null).ToList();
foreach (var t in tagSets)
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
}
public void Executing(DbContext context)
{
// persist tags:
var modifiedEntities = context
.ChangeTracker
.Entries()
.Where(p => p.State.In(EntityState.Modified, EntityState.Added))
.ToList();
#region // notes: working with proxies, esp EF 6
// EF 6: entities are proxied with lazy loading when collections are virtual
// EF Core: lazy loading is supported in 2.1 (there is a version of lazy loading with proxy-wrapping and a proxy-less version with DI) but not on by default and are not supported here
persistTags(modifiedEntities);
}
//static void doWork_EF6(DbContext context)
//{
// var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State == EntityState.Modified).ToList();
// var unproxiedEntities = modifiedEntities.Select(me => UnProxy(context, me.Entity)).ToList();
// // persist tags
// var tagSets = unproxiedEntities.Select(ue => ue as UserDefinedItem).Where(a => a != null).ToList();
// foreach (var t in tagSets)
// FileManager.TagsPersistence.Save(t.ProductId, t.TagsRaw);
//}
//// https://stackoverflow.com/a/25774651
//private static T UnProxy<T>(DbContext context, T proxyObject) where T : class
//{
// // alternative: https://docs.microsoft.com/en-us/ef/ef6/fundamentals/proxies
// var proxyCreationEnabled = context.Configuration.ProxyCreationEnabled;
// try
// {
// context.Configuration.ProxyCreationEnabled = false;
// return context.Entry(proxyObject).CurrentValues.ToObject() as T;
// }
// finally
// {
// context.Configuration.ProxyCreationEnabled = proxyCreationEnabled;
// }
//}
#endregion
}
private static void persistTags(List<EntityEntry> modifiedEntities)
{
var tagSets = modifiedEntities
.Select(e => e.Entity as UserDefinedItem)
// filter by null but NOT by blank. blank is the valid way to show the absence of tags
.Where(a => a != null)
.ToList();
foreach (var t in tagSets)
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace DataLayer.Utilities
{
public static class LocalDatabaseInfo
{
public static List<string> GetLocalDBInstances()
{
// Start the child process.
using var p = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
UseShellExecute = false,
RedirectStandardOutput = true,
FileName = "cmd.exe",
Arguments = "/C sqllocaldb info",
CreateNoWindow = true,
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
}
};
p.Start();
var output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
// if LocalDb is not installed then it will return that 'sqllocaldb' is not recognized as an internal or external command operable program or batch file
return string.IsNullOrWhiteSpace(output) || output.Contains("not recognized")
? new List<string>()
: output
.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)
.Select(i => i.Trim())
.Where(i => !string.IsNullOrEmpty(i))
.ToList();
}
}
}

View File

@@ -1,6 +1,7 @@
{
"ConnectionStrings": {
"LibationContext": "Server=(LocalDb)\\MSSQLLocalDB;Database=DataLayer.LibationContext;Integrated Security=true;",
"LibationContext_sqlserver": "Server=(LocalDb)\\MSSQLLocalDB;Database=DataLayer.LibationContext;Integrated Security=true;",
"LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;",
"// on windows sqlite paths accept windows and/or unix slashes": "",
"MyTestContext": "Data Source=%DESKTOP%/sample.db"

View File

@@ -57,38 +57,40 @@ namespace DtoImporterService
.Select(a => context.Contributors.Local.Single(c => a.Name == c.Name))
.ToList();
// if no narrators listed, author is the narrator
if (item.Narrators is null || !item.Narrators.Any())
item.Narrators = item.Authors;
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
var narrators = item
.Narrators
.Select(n => context.Contributors.Local.Single(c => n.Name == c.Name))
.ToList();
book = context.Books.Add(new Book(
new AudibleProductId(item.ProductId), item.Title, item.Description, item.LengthInMinutes, authors))
.Entity;
new AudibleProductId(item.ProductId),
item.Title,
item.Description,
item.LengthInMinutes,
authors,
narrators)
).Entity;
var publisherName = item.Publisher;
if (!string.IsNullOrWhiteSpace(publisherName))
{
var publisher = context.Contributors.Local.Single(c => publisherName == c.Name);
book.ReplacePublisher(publisher);
}
qtyNew++;
}
// if no narrators listed, author is the narrator
if (item.Narrators is null || !item.Narrators.Any())
item.Narrators = item.Authors;
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
var narrators = item
.Narrators
.Select(n => context.Contributors.Local.Single(c => n.Name == c.Name))
.ToList();
// not all books have narrators. these will already be using author as narrator. don't undo this
if (narrators.Any())
book.ReplaceNarrators(narrators);
// set/update book-specific info which may have changed
book.PictureId = item.PictureId;
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
if (!string.IsNullOrWhiteSpace(item.SupplementUrl))
book.AddSupplementDownloadUrl(item.SupplementUrl);
var publisherName = item.Publisher;
if (!string.IsNullOrWhiteSpace(publisherName))
{
var publisher = context.Contributors.Local.Single(c => publisherName == c.Name);
book.ReplacePublisher(publisher);
}
// 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);

View File

@@ -43,19 +43,19 @@ namespace FileLiberator
try
{
{
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.AAX, DownloadBook);
var statusHandler = await DownloadBook.TryProcessAsync(libraryBook);
if (statusHandler.HasErrors)
return statusHandler;
}
{
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.Audio, DecryptBook);
var statusHandler = await DecryptBook.TryProcessAsync(libraryBook);
if (statusHandler.HasErrors)
return statusHandler;
}
{
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.PDF, DownloadPdf);
var statusHandler = await DownloadPdf.TryProcessAsync(libraryBook);
if (statusHandler.HasErrors)
return statusHandler;
}
@@ -67,10 +67,5 @@ namespace FileLiberator
Completed?.Invoke(this, displayMessage);
}
}
private static async Task<StatusHandler> processAsync(LibraryBook libraryBook, AudibleFileStorage afs, IProcessable processable)
=> !await afs.ExistsAsync(libraryBook.Book.AudibleProductId)
? await processable.ProcessAsync(libraryBook)
: new StatusHandler();
}
}

View File

@@ -13,12 +13,8 @@ namespace FileLiberator
public class DownloadPdf : DownloadableBase
{
public override async Task<bool> ValidateAsync(LibraryBook libraryBook)
{
if (string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook)))
return false;
return !await AudibleFileStorage.PDF.ExistsAsync(libraryBook.Book.AudibleProductId);
}
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
&& !await AudibleFileStorage.PDF.ExistsAsync(libraryBook.Book.AudibleProductId);
private static string getdownloadUrl(LibraryBook libraryBook)
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;

View File

@@ -40,5 +40,10 @@ namespace FileLiberator
return null;
}
}
public static async Task<StatusHandler> TryProcessAsync(this IProcessable processable, LibraryBook libraryBook)
=> await processable.ValidateAsync(libraryBook)
? await processable.ProcessAsync(libraryBook)
: new StatusHandler();
}
}

View File

@@ -4,6 +4,10 @@
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Polly" Version="7.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
</ItemGroup>

View File

@@ -3,64 +3,57 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Polly;
using Polly.Retry;
namespace FileManager
{
/// <summary>
/// Tags must also be stored in db for search performance. Stored in json file to survive a db reset.
/// json is only read when a product is first loaded
/// json is only read when a product is first loaded into the db
/// json is only written to when tags are edited
/// json access is infrequent and one-off
/// all other reads happen against db. No volitile storage
/// </summary>
public static class TagsPersistence
{
public static string TagsFile => Path.Combine(Configuration.Instance.LibationFiles, "BookTags.json");
private static string TagsFile => Path.Combine(Configuration.Instance.LibationFiles, "BookTags.json");
private static object locker { get; } = new object();
// if failed, retry only 1 time after a wait of 100 ms
// 1st save attempt sometimes fails with
// The requested operation cannot be performed on a file with a user-mapped section open.
private static RetryPolicy policy { get; }
= Policy.Handle<Exception>()
.WaitAndRetry(new[] { TimeSpan.FromMilliseconds(100) });
public static void Save(string productId, string tags)
=> System.Threading.Tasks.Task.Run(() => save_fireAndForget(productId, tags));
private static void save_fireAndForget(string productId, string tags)
{
ensureCache();
cache[productId] = tags;
lock (locker)
{
// get all
var allDictionary = retrieve();
// update/upsert tag list
allDictionary[productId] = tags;
// re-save:
// this often fails the first time with
// The requested operation cannot be performed on a file with a user-mapped section open.
// 2nd immediate attempt failing was rare. So I added sleep. We'll see...
void resave() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(allDictionary, Formatting.Indented));
try { resave(); }
catch (IOException debugEx)
{
// 1000 was always reliable but very slow. trying other values
var waitMs = 100;
System.Threading.Thread.Sleep(waitMs);
resave();
}
}
policy.Execute(() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(cache, Formatting.Indented)));
}
private static Dictionary<string, string> cache;
public static string GetTags(string productId)
{
var dic = retrieve();
return dic.ContainsKey(productId) ? dic[productId] : null;
ensureCache();
cache.TryGetValue(productId, out string value);
return value;
}
private static Dictionary<string, string> retrieve()
{
if (!FileUtility.FileExists(TagsFile))
return new Dictionary<string, string>();
lock (locker)
return JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(TagsFile));
}
}
private static void ensureCache()
{
if (cache is null)
lock (locker)
cache = !FileUtility.FileExists(TagsFile)
? new Dictionary<string, string>()
: JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(TagsFile));
}
}
}

View File

@@ -4,10 +4,6 @@
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Polly" Version="7.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />

View File

@@ -9,7 +9,7 @@
<PublishTrimmed>true</PublishTrimmed>
<PublishReadyToRun>true</PublishReadyToRun>
<PublishSingleFile>true</PublishSingleFile>
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>

View File

@@ -16,7 +16,14 @@ namespace LibationWinForm
private async void IndexLibraryDialog_Shown(object sender, System.EventArgs e)
{
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.IndexLibraryAsync(new Login.WinformResponder());
try
{
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.IndexLibraryAsync(new Login.WinformResponder());
}
catch
{
MessageBox.Show("Error importing library. Please try again. If this happens after 2 or 3 tries, contact administrator", "Error importing library", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
this.Close();
}

View File

@@ -27,10 +27,15 @@ namespace LibationWinForm
public event EventHandler<int> VisibleCountChanged;
private DataGridView dataGridView;
private LibationContext context;
public ProductsGrid() => InitializeComponent();
public ProductsGrid()
{
InitializeComponent();
Disposed += (_, __) => { if (context != null) context.Dispose(); };
}
private bool hasBeenDisplayed = false;
private bool hasBeenDisplayed = false;
public void Display()
{
if (hasBeenDisplayed)
@@ -87,10 +92,11 @@ namespace LibationWinForm
}
//
// transform into sorted GridEntry.s BEFORE binding
//
var lib = LibraryQueries.GetLibrary_Flat_NoTracking();
//
// transform into sorted GridEntry.s BEFORE binding
//
context = LibationContext.Create();
var lib = context.GetLibrary_Flat_WithTracking();
// if no data. hide all columns. return
if (!lib.Any())
@@ -174,8 +180,8 @@ namespace LibationWinForm
if (editTagsForm.ShowDialog() != DialogResult.OK)
return;
var qtyChanges = saveChangedTags(liveGridEntry.GetBook(), editTagsForm.NewTags);
if (qtyChanges == 0)
var qtyChanges = context.UpdateTags(liveGridEntry.GetBook(), editTagsForm.NewTags);
if (qtyChanges == 0)
return;
// force a re-draw, and re-apply filters
@@ -186,14 +192,6 @@ namespace LibationWinForm
filter();
}
private static int saveChangedTags(Book book, string newTags)
{
book.UserDefinedItem.Tags = newTags;
var qtyChanges = LibraryCommands.IndexChangedTags(book);
return qtyChanges;
}
#region Cell Formatting
private void replaceFormatted(object sender, DataGridViewCellFormattingEventArgs e)
{
@@ -213,22 +211,6 @@ namespace LibationWinForm
}
#endregion
public void UpdateRow(string productId)
{
for (var r = dataGridView.RowCount - 1; r >= 0; r--)
{
var gridEntry = getGridEntry(r);
if (gridEntry.GetBook().AudibleProductId == productId)
{
var libBook = LibraryQueries.GetLibraryBook_Flat_NoTracking(productId);
gridEntry.REPLACE_Library_Book(libBook);
dataGridView.InvalidateRow(r);
return;
}
}
}
#region filter
string _filterSearchString;
private void filter() => Filter(_filterSearchString);

View File

@@ -1,13 +1,18 @@
-- begin VERSIONING ---------------------------------------------------------------------------------------------------------------------
https://github.com/rmcrackan/Libation/releases
v3.1-beta.1 : RELEASE TO BETA
v3.0.3 : Switch to SQLite. No longer relies on LocalDB, which must be installed separately
v3.0.2 : Final using LocalDB
v3.0.1 : Legacy inAudible wire-up code is still present but is commented out. All future check-ins are not guaranteed to have inAudible wire-up code
v3.0 : This version is fully powered by the Audible API. Legacy scraping code is still present but is commented out. All future check-ins are not guaranteed to have any scraping code
v2 : new library page scraping. still chrome cookies. all decryption is handled natively. no inAudible dependency
v1 : old library ajax scraping. wish list scraping. chrome cookies. directly call local inAudible. .net framework
-- end VERSIONING ---------------------------------------------------------------------------------------------------------------------
-- begin HOW TO PUBLISH ---------------------------------------------------------------------------------------------------------------------
OPTION 1: UI
rt-clk project > Publish...
rt-clk project project > Publish...
click Publish
OPTION 2: cmd line

View File

@@ -1,207 +0,0 @@
-- begin BETA ---------------------------------------------------------------------------------------------------------------------
TESTING BUG
dbl clk. long pause. exception:
System.ComponentModel.Win32Exception (2): The system cannot find the file specified.
"continue" button allows me to keep using
bottom #s do not update
login succeeded. IdentityTokens.json successfully created
received above error again during scan. continue
stuck on scan. force quit from task manager
only files:
Images -- empty dir
IdentityTokens.json -- populated
no mdf, ldf
REPLACE DB
need completely need db? replace LocalDb with sqlite? embedded document nosql?
CREATE INSTALLER
see REFERENCE.txt > HOW TO PUBLISH
RELEASE TO BETA
Warn of known performance issues
- Library import
- Tag add/edit
- Grid is slow to respond loading when books aren't liberated
- get decrypt key -- unavoidable
-- end BETA ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT, IMPORT UI ---------------------------------------------------------------------------------------------------------------------
scan library in background?
can include a notice somewhere that a scan is in-process
why block the UI at all?
what to do if new books? don't want to refresh grid when user isn't expecting it
-- end ENHANCEMENT, IMPORT UI ---------------------------------------------------------------------------------------------------------------------
-- begin BUG, FILE DOWNLOAD ---------------------------------------------------------------------------------------------------------------------
reproduce: try to do the api download with a bad codec
result: DownloadsFinal dir .aax file 1 kb
this resulted from an exception. we should not be keeping a file after exception
if error: show error. DownloadBook delete bad file
-- end BUG, FILE DOWNLOAD ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT, PERFORMANCE: IMPORT ---------------------------------------------------------------------------------------------------------------------
imports are PAINFULLY slow for just a few hundred items. wtf is taking so long?
-- end ENHANCEMENT, PERFORMANCE: IMPORT ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT, PERFORMANCE: GRID ---------------------------------------------------------------------------------------------------------------------
when a book/pdf is NOT liberated, calculating the grid's [Liberated][NOT d/l'ed] label is very slow. use something similar to PictureStorage's timer to run on a separate thread
https://stackoverflow.com/a/12046333
https://codereview.stackexchange.com/a/135074
// do NOT use lock() or Monitor with async/await
private static int _lockFlag = 0; // 0 - free
if (Interlocked.CompareExchange(ref _lockFlag, 1, 0) != 0) return;
// only 1 thread will enter here without locking the object/put the other threads to sleep
try { await DoWorkAsync(); }
// free the lock
finally { Interlocked.Decrement(ref _lockFlag); }
use stop light icons for liberated state: red=none, yellow=downloaded encrypted, green=liberated
need a way to liberate ad hoc books and pdf.s
use pdf icon with and without and X over it to indicate status
-- end ENHANCEMENT, PERFORMANCE: GRID ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT, GET LIBRARY ---------------------------------------------------------------------------------------------------------------------
Audible API. GET /1.0/library , GET /1.0/library/{asin}
TONS of expensive conversion: GetLibraryAsync > string > JObject > string > LibraryDtoV10
same for GetLibraryBookAsync > ... > BookDtoV10
-- end ENHANCEMENT, GET LIBRARY ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT, DEBUGGING ---------------------------------------------------------------------------------------------------------------------
datalayer stuff (eg: Book) need better ToString
-- end ENHANCEMENT, DEBUGGING ---------------------------------------------------------------------------------------------------------------------
-- begin BUG, MOVING FILES ---------------------------------------------------------------------------------------------------------------------
with libation closed, move files
start libation
can get error below
fixed on restart
Form1_Load ... await setBackupCountsAsync();
Collection was modified; enumeration operation may not execute.
stack trace
at System.ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
at FileManager.FilePathCache.GetPath(String id, FileType type) in \Libation\FileManager\UNTESTED\FilePathCache.cs:line 33
at FileManager.AudibleFileStorage.<getAsync>d__32.MoveNext() in \Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 112
at FileManager.AudibleFileStorage.<GetAsync>d__31.MoveNext() in \Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 107
at FileManager.AudibleFileStorage.<ExistsAsync>d__30.MoveNext() in \Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 104
at LibationWinForm.Form1.<<setBookBackupCountsAsync>g__getAudioFileStateAsync|15_1>d.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 110
at LibationWinForm.Form1.<setBookBackupCountsAsync>d__15.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 117
at LibationWinForm.Form1.<setBackupCountsAsync>d__13.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 81
at LibationWinForm.Form1.<Form1_Load>d__11.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 60
-- end BUG, MOVING FILES ---------------------------------------------------------------------------------------------------------------------
-- begin CONFIG FILES ---------------------------------------------------------------------------------------------------------------------
.\appsettings.json should only be a pointer to the real settings file location: LibationSettings.json
replace complex config saving throughout with new way in my ConsoleDependencyInjection solution
all settings should be strongly typed
re-create my shortcuts and bak
for appsettings.json to get copied in the single-file release, project must incl <ExcludeFromSingleFile>true
multiple files named "appsettings.json" will overwrite each other
libraries should avoid this generic name. in general: ok for applications to use them
there are exceptions: datalayer has appsettings which is copied to winform. if winform uses appsettings also, it will override datalayer's
Audible API
\AudibleApi\_Tests\AudibleApi.Tests\bin\Debug\netcoreapp3.0\L1
\AudibleApi\_Tests\AudibleApi.Tests\bin\Debug\netcoreapp3.0\ComputedTestValues
14+ json files
these can go in a shared solution folder
BasePath => recursively search directories upward-only until fild dir with .sln
from here can set up a shared dir anywhere. use recursive upward search to find it. store shared files here. eg: identityTokens.json, ComputedTestValues
-- end CONFIG FILES ---------------------------------------------------------------------------------------------------------------------
-- begin TAGS ---------------------------------------------------------------------------------------------------------------------
pulling previous tags into new Books. think: reloading db
move out of Book and into DtoMapper?
Extract file and tag stuff from domain objects. This should exist only in data layer. If domain objects are able to call EF context, it should go through data layer
Why are tags in file AND database?
extract FileManager dependency from data layer
-- end TAGS ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT, PERFORMANCE: TAGS ---------------------------------------------------------------------------------------------------------------------
tag edits still take forever and block UI
unlikely to be an issue with file write. in fact, should probably roll back this change
also touches parts of code which: db write via a hook, search engine re-index
-- end ENHANCEMENT, PERFORMANCE: TAGS ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT, CATEGORIES ---------------------------------------------------------------------------------------------------------------------
add support for multiple categories
when i do this, learn about the different CategoryLadder.Root enums. probably only need Root.Genres
-- end ENHANCEMENT, CATEGORIES ---------------------------------------------------------------------------------------------------------------------
-- begin CLEAN UP ARCHITECTURE ---------------------------------------------------------------------------------------------------------------------
my ui sucks. it's also tightly coupled with biz logic. can't replace ui until biz logic is extracted and loosely. remove all biz logic from presentation/winforms layer
-- end CLEAN UP ARCHITECTURE ---------------------------------------------------------------------------------------------------------------------
-- begin UNIT TESTS ---------------------------------------------------------------------------------------------------------------------
all "UNTESTED" code needs unit tests
Turn into unit tests or demos
TextBoxBaseTextWriter.cs
EnumerationExamples.cs
EnumExt.cs
IEnumerable[T]Ext.cs
MultiTextWriter.cs
Selenium.Examples.cs
ScratchPad.cs
ProcessorAutomationController.Examples.cs
ScraperRules.Examples.cs
ByFactory.Example.cs
search 'example code' on: LibationWinForm\...\Form1.cs
EnumerationFlagsExtensions.EXAMPLES()
// examples
scratchpad
scratch pad
scratch_pad
-- end UNIT TESTS ---------------------------------------------------------------------------------------------------------------------
-- begin DECRYPTING ---------------------------------------------------------------------------------------------------------------------
replace inaudible/inaudible lite with pure ffmpeg
benefits of inaudible:
highly configurable
embedded cover image
chapter-ized
cue and nfo files
can hopefully get most of this with simple decrypt. possibly including the new chapter titles
better chapters in many m4b files. to see, try re-downloading. examples: bobiverse, sharp objects
raw ffmpeg decrypting is significantly faster than inAudible/AaxDecrypter method:
inAudible/AaxDecrypter
tiny file
40 sec decrypt
40 sec chapterize + tag
huge file
60 sec decrypt
120 sec chapterize + tag
directly call ffmpeg (decrypt only)
tiny file
17 sec decrypt
huge file
39 sec decrypt
-- end DECRYPTING ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT: REMOVE BOOK ---------------------------------------------------------------------------------------------------------------------
how to remove a book?
previously difficult due to implementation details regarding scraping and importing. should now be trivial
-- end ENHANCEMENT: REMOVE BOOK ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT: NEW VIEWS ---------------------------------------------------------------------------------------------------------------------
menu views. filter could work for grid display; just use the lucene query language
1) menu to show all tags and count of each. click on tag so see only those books
2) tree to show all categories and subcategories. click on category so see only those books
-- end ENHANCEMENT: NEW VIEWS ---------------------------------------------------------------------------------------------------------------------
-- begin ENHANCEMENT: LOGGING, ERROR HANDLING ---------------------------------------------------------------------------------------------------------------------
LibationWinForm and Audible API need better logging and error handling
incl log levels, db query logging
see AaxDecryptorWinForms.initLogging()
-- end ENHANCEMENT: LOGGING ---------------------------------------------------------------------------------------------------------------------