Compare commits

...

8 Commits

Author SHA1 Message Date
Robert McRackan
437e85fd12 mp3 bugfix 2021-09-03 18:22:31 -04:00
Robert McRackan
de34e5c795 import speed improvements 2021-09-03 16:35:31 -04:00
Robert McRackan
8ffcefd6ae massive speed-up for import contributors 2021-09-03 15:30:17 -04:00
Robert McRackan
e59ab9b483 remove v3=>4 migration work-arounds 2021-09-03 13:42:58 -04:00
Robert McRackan
57fa1bd763 cross thread issue. add temp time logging in ImportAccountAsync 2021-09-03 11:36:55 -04:00
rmcrackan
dccb2d73d6 Merge pull request #101 from Mbucari/master
Fixed cross thread bug and Enumerable change bug.
2021-09-03 08:15:37 -04:00
Mbucari
77fc865636 Fixed crossthread. 2021-09-02 21:21:36 -06:00
Mbucari
1040a347c6 Fixed cross thread bug and Enumerable change bug. 2021-09-02 21:04:16 -06:00
19 changed files with 147 additions and 121 deletions

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean" Version="0.1.1" />
<PackageReference Include="AAXClean" Version="0.1.8" />
<PackageReference Include="Dinah.Core" Version="1.0.5.2" />
</ItemGroup>

View File

@@ -8,6 +8,7 @@ using Dinah.Core;
using DtoImporterService;
using InternalUtilities;
using Serilog;
using static DtoImporterService.PerfLogger;
namespace ApplicationServices
{
@@ -16,12 +17,12 @@ namespace ApplicationServices
private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, ILoginCallback> loginCallbackFactoryFunc, List<LibraryBook> existingLibrary, params Account[] accounts)
{
{
//These are the minimum response groups required for the
//library scanner to pass all validation and filtering.
LibraryResponseGroups =
LibraryResponseGroups =
LibraryOptions.ResponseGroupOptions.ProductAttrs |
LibraryOptions.ResponseGroupOptions.ProductDesc |
LibraryOptions.ResponseGroupOptions.ProductDesc |
LibraryOptions.ResponseGroupOptions.Relationships;
if (accounts is null || accounts.Length == 0)
@@ -59,26 +60,32 @@ namespace ApplicationServices
Log.Logger.Error(ex, "Error importing library");
throw;
}
finally
{
finally
{
LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
}
}
}
#region FULL LIBRARY scan and import
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, params Account[] accounts)
{
logRestart();
if (accounts is null || accounts.Length == 0)
return (0, 0);
try
{
logTime($"pre {nameof(scanAccountsAsync)} all");
var importItems = await scanAccountsAsync(loginCallbackFactoryFunc, accounts);
logTime($"post {nameof(scanAccountsAsync)} all");
var totalCount = importItems.Count;
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
logTime($"pre {nameof(importIntoDbAsync)}");
var newCount = await importIntoDbAsync(importItems);
logTime($"post {nameof(importIntoDbAsync)}");
Log.Logger.Information($"Import: New count {newCount}");
return (totalCount, newCount);
@@ -106,6 +113,11 @@ namespace ApplicationServices
Log.Logger.Error(ex, "Error importing library");
throw;
}
finally
{
stop();
var putBreakPointHere = logOutput;
}
}
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, Account[] accounts)
@@ -137,19 +149,28 @@ namespace ApplicationServices
Account = account?.MaskedLogEntry ?? "[null]"
});
logTime($"pre scanAccountAsync {account.AccountName}");
var dtoItems = await AudibleApiActions.GetLibraryValidatedAsync(api, LibraryResponseGroups);
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
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)
{
logTime("importIntoDbAsync -- pre db");
using var context = DbContexts.GetContext();
var libraryImporter = new LibraryImporter(context);
var libraryImporter = new LibraryBookImporter(context);
var newCount = await Task.Run(() => libraryImporter.Import(importItems));
logTime("importIntoDbAsync -- post Import()");
var qtyChanges = context.SaveChanges();
logTime("importIntoDbAsync -- post SaveChanges");
if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange());
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
return newCount;
}
@@ -215,15 +236,14 @@ namespace ApplicationServices
}
#endregion
// must be here instead of in db layer due to AaxcExists
public static LiberatedStatus Liberated_Status(Book book)
=> book.Audio_Exists ? LiberatedStatus.Liberated
=> book.Audio_Exists ? book.UserDefinedItem.BookStatus
: FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
: LiberatedStatus.NotLiberated;
public static LiberatedStatus? Pdf_Status(Book book)
=> !book.HasPdf ? null
: book.PDF_Exists ? LiberatedStatus.Liberated
: LiberatedStatus.NotLiberated;
// exists here for feature predictability. It makes sense for this to be where Liberated_Status is
public static LiberatedStatus? Pdf_Status(Book book) => book.UserDefinedItem.PdfStatus;
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...

View File

@@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.EntityFrameworkCore" Version="1.0.2.2" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="1.0.5.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -30,8 +30,6 @@ namespace DataLayer
public string Description { get; private set; }
public int LengthInMinutes { get; private set; }
public ContentType ContentType { get; private set; }
// immutable-ish. should be immutable. mutability is necessary for v3 => v4 upgrades
public string Locale { get; private set; }
// mutable
@@ -57,22 +55,10 @@ namespace DataLayer
public UserDefinedItem UserDefinedItem { get; private set; }
// UserDefinedItem convenience properties
public bool Audio_Exists
{
get
{
var status = UserDefinedItem?.BookStatus;
return status.HasValue && status.Value != LiberatedStatus.NotLiberated;
}
}
public bool PDF_Exists
{
get
{
var status = UserDefinedItem?.PdfStatus;
return (status.HasValue && status.Value == LiberatedStatus.Liberated);
}
}
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
public bool Audio_Exists => UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated;
/// <summary>True if exists and IsLiberated. Else false</summary>
public bool PDF_Exists => UserDefinedItem.PdfStatus == LiberatedStatus.Liberated;
// is owned, not optional 1:1
/// <summary>The product's aggregate community rating</summary>
@@ -90,7 +76,8 @@ namespace DataLayer
ContentType contentType,
IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators,
Category category, string localeName)
Category category,
string localeName)
{
// validate
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
@@ -272,10 +259,6 @@ namespace DataLayer
Category = category;
}
// needed for v3 => v4 upgrade
public void UpdateLocale(string localeName)
=> Locale ??= localeName;
public override string ToString() => $"[{AudibleProductId}] {Title}";
}
}

View File

@@ -9,8 +9,6 @@ namespace DataLayer
public Book Book { get; private set; }
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() { }
@@ -24,10 +22,6 @@ namespace DataLayer
Account = account;
}
// needed for v3 => v4 upgrade
public void UpdateAccount(string account)
=> Account ??= account;
public override string ToString() => $"{DateAdded:d} {Book}";
}
}

View File

@@ -1,19 +0,0 @@
using System;
using System.Linq;
namespace DataLayer
{
public static class GenericPaging
{
public static IQueryable<T> Page<T>(this IQueryable<T> query, int pageNumZeroStart, int pageSize)
{
if (pageSize < 1)
throw new ArgumentOutOfRangeException(nameof(pageSize), "pageSize must be at least 1");
if (pageNumZeroStart > 0)
query = query.Skip(pageNumZeroStart * pageSize);
return query.Take(pageSize);
}
}
}

View File

@@ -19,13 +19,13 @@ namespace DtoImporterService
new ContributorImporter(DbContext).Import(importItems);
new SeriesImporter(DbContext).Import(importItems);
new CategoryImporter(DbContext).Import(importItems);
// get distinct
var productIds = importItems.Select(i => i.DtoItem.ProductId).ToList();
var productIds = importItems.Select(i => i.DtoItem.ProductId).Distinct().ToList();
// load db existing => .Local
loadLocal_books(productIds);
// upsert
var qtyNew = upsertBooks(importItems);
return qtyNew;
@@ -33,12 +33,30 @@ namespace DtoImporterService
private void loadLocal_books(List<string> productIds)
{
var localProductIds = DbContext.Books.Local.Select(b => b.AudibleProductId);
// if this context has already loaded books, don't need to reload them. vestige from when context was long-lived. in practice, we now typically use a fresh context. this is quick though so no harm in leaving it.
var localProductIds = DbContext.Books.Local.Select(b => b.AudibleProductId).ToList();
var remainingProductIds = productIds
.Distinct()
.Except(localProductIds)
.ToList();
#region // explanation of DbContext.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
/*
articles suggest loading to Local with
context.Books.Load();
we want Books and associated fields
context.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
this is emulating Load() but with also getting associated fields
from: Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
// Summary:
// Enumerates the query. When using Entity Framework, this causes the results of
// the query to be loaded into the associated context. This is equivalent to calling
// ToList and then throwing away the list (without the overhead of actually creating
// the list).
public static void Load<TSource>([NotNullAttribute] this IQueryable<TSource> source);
*/
#endregion
// GetBooks() eager loads Series, category, et al
if (remainingProductIds.Any())
DbContext.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
@@ -137,9 +155,6 @@ namespace DtoImporterService
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);

View File

@@ -35,7 +35,7 @@ namespace DtoImporterService
private void loadLocal_categories(List<string> categoryIds)
{
var localIds = DbContext.Categories.Local.Select(c => c.AudibleCategoryId);
var localIds = DbContext.Categories.Local.Select(c => c.AudibleCategoryId).ToList();
var remainingCategoryIds = categoryIds
.Distinct()
.Except(localIds)

View File

@@ -47,57 +47,61 @@ namespace DtoImporterService
private void loadLocal_contributors(List<string> contributorNames)
{
// must include default/empty/missing
contributorNames.Add(Contributor.GetEmpty().Name);
//// BAD: very inefficient
// var x = context.Contributors.Local.Where(c => !contribNames.Contains(c.Name));
// GOOD: Except() is efficient. Due to hashing, it's close to O(n)
var localNames = DbContext.Contributors.Local.Select(c => c.Name);
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
var remainingContribNames = contributorNames
.Distinct()
.Except(localNames)
.ToList();
// load existing => local
// remember to include default/empty/missing
var emptyName = Contributor.GetEmpty().Name;
if (remainingContribNames.Any())
DbContext.Contributors.Where(c => remainingContribNames.Contains(c.Name) || c.Name == emptyName).ToList();
DbContext.Contributors.Where(c => remainingContribNames.Contains(c.Name)).ToList();
}
// only use after loading contributors => local
private int upsertPeople(List<Person> people)
{
var qtyNew = 0;
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
var newPeople = people
.Select(p => p.Name)
.Distinct()
.Except(localNames)
.ToList();
foreach (var p in people)
var groupby = people.GroupBy(
p => p.Name,
p => p,
(key, g) => new { Name = key, People = g.ToList() }
);
foreach (var name in newPeople)
{
// Should be 'Single' not 'First'. A user had a duplicate get in somehow though so I'm now using 'First' defensively
var person = DbContext.Contributors.Local.FirstOrDefault(c => c.Name == p.Name);
if (person == null)
{
person = DbContext.Contributors.Add(new Contributor(p.Name, p.Asin)).Entity;
qtyNew++;
}
var p = groupby.Single(g => g.Name == name).People.First();
DbContext.Contributors.Add(new Contributor(p.Name, p.Asin));
}
return qtyNew;
return newPeople.Count;
}
// only use after loading contributors => local
private int upsertPublishers(List<string> publishers)
{
var qtyNew = 0;
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
var newPublishers = publishers
.Distinct()
.Except(localNames)
.ToList();
foreach (var publisherName in publishers)
{
if (DbContext.Contributors.Local.SingleOrDefault(c => c.Name == publisherName) == null)
{
DbContext.Contributors.Add(new Contributor(publisherName));
qtyNew++;
}
}
foreach (var pub in newPublishers)
DbContext.Contributors.Add(new Contributor(pub));
return qtyNew;
return newPublishers.Count;
}
}
}

View File

@@ -1,15 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{
public class LibraryImporter : ItemsImporterBase
public class LibraryBookImporter : ItemsImporterBase
{
public LibraryImporter(LibationContext context) : base(context) { }
public LibraryBookImporter(LibationContext context) : base(context) { }
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new LibraryValidator().Validate(importItems.Select(i => i.DtoItem));
@@ -28,7 +27,8 @@ namespace DtoImporterService
// - 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
// sqlite cannot alter pk. the work around is an extensive headache
// - update: now possible in .net5/efcore5
//
// currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region.
//
@@ -46,15 +46,6 @@ namespace DtoImporterService
DbContext.LibraryBooks.Add(libraryBook);
}
// needed for v3 => v4 upgrade
var toUpdate = DbContext.LibraryBooks.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

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace DtoImporterService
{
public record timeLogEntry(string msg, long totalElapsed, long delta);
public static class PerfLogger
{
private static Stopwatch sw = new Stopwatch();
private static List<timeLogEntry> __log { get; } = new List<timeLogEntry>();
public static void logTime(string s)
{
var totalElapsed = sw.ElapsedMilliseconds;
var prev = __log.Last().totalElapsed;
var delta = totalElapsed - prev;
__log.Add(new(s, totalElapsed, delta));
}
public static void logRestart()
{
__log.Clear();
__log.Add(new("begin", 0, 0));
sw.Restart();
}
public static void stop() => sw.Stop();
public static string logOutput =>
$"{nameof(timeLogEntry.msg)}\t{nameof(timeLogEntry.totalElapsed)}\t{nameof(timeLogEntry.delta)}\r\n"
+ __log.Select(t => $"{t.msg}\t{t.totalElapsed}\t{t.delta}").Aggregate((a, b) => $"{a}\r\n{b}");
}
}

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="1.1.1.1" />
<PackageReference Include="AudibleApi" Version="1.1.5.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>5.6.3.1</Version>
<Version>5.6.5.1</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -30,7 +30,8 @@ namespace LibationWinForms.Dialogs
InitializeComponent();
_labelFormat = label1.Text;
_dataGridView.CellContentClick += (s, e) => _dataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
_dataGridView.CellContentClick += (_, _) => _dataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
_dataGridView.CellValueChanged += (_, _) => UpdateSelection();
_dataGridView.BindingContextChanged += _dataGridView_BindingContextChanged;
var orderedGridEntries = _libraryBooks
@@ -58,7 +59,7 @@ namespace LibationWinForms.Dialogs
{
var removedBooks = await LibraryCommands.FindInactiveBooks((account) => new WinformResponder(account), _libraryBooks, _accounts);
var removable = _removableGridEntries.Where(rge => removedBooks.Any(rb => rb.Book.AudibleProductId == rge.AudibleProductId));
var removable = _removableGridEntries.Where(rge => removedBooks.Any(rb => rb.Book.AudibleProductId == rge.AudibleProductId)).ToList();
if (!removable.Any())
return;

View File

@@ -134,11 +134,11 @@ namespace LibationWinForms
// suppressed filter while init'ing UI
var prev_isProcessingGridSelect = isProcessingGridSelect;
isProcessingGridSelect = true;
setGrid();
this.UIThread(() => setGrid());
isProcessingGridSelect = prev_isProcessingGridSelect;
// UI init complete. now we can apply filter
doFilter(lastGoodFilter);
this.UIThread(() => doFilter(lastGoodFilter));
setBackupCounts(null, null);
}

View File

@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="1.0.2.1" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="1.0.5.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -80,7 +80,10 @@ namespace LibationWinForms
{
var filePath = FileManager.AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!Go.To.File(filePath))
MessageBox.Show($"File not found:\r\n{filePath}");
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
MessageBox.Show($"File not found" + suffix);
}
return;
}

View File

@@ -10,8 +10,8 @@
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.6" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.6" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -10,8 +10,8 @@
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.6" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.6" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>