From 30e6deeeaaf0a8dae444a50d5272fdb0d7dac89a Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Tue, 7 Jun 2022 15:25:52 -0600 Subject: [PATCH 01/18] Add migration to cleans DB of 7.10.1 hack --- Source/AppScaffolding/LibationScaffolding.cs | 24 +++++++++++++++++++ Source/ApplicationServices/LibraryCommands.cs | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 20af1da9..e16f52f0 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -59,6 +59,7 @@ namespace AppScaffolding // Migrations.migrate_to_v6_6_9(config); + Migrations.migrate_from_7_10_1(config); } public static void PopulateMissingConfigValues(Configuration config) @@ -401,5 +402,28 @@ namespace AppScaffolding UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails"); } } + + public static void migrate_from_7_10_1(Configuration config) + { + //This migration removes books and series with SERIES_ prefix that were created + //as a hack workaround in 7.10.1. Said workaround was removed in 7.10.2 + + var migrated = config.GetNonString(nameof(migrate_from_7_10_1)); + + if (migrated) return; + + using var context = DbContexts.GetContext(); + + var booksToRemove = context.Books.Where(b => b.AudibleProductId.StartsWith("SERIES_")).ToArray(); + var seriesToRemove = context.Series.Where(s => s.AudibleSeriesId.StartsWith("SERIES_")).ToArray(); + var lbToRemove = context.LibraryBooks.Where(lb => booksToRemove.Any(b => b == lb.Book)).ToArray(); + + context.LibraryBooks.RemoveRange(lbToRemove); + context.Books.RemoveRange(booksToRemove); + context.Series.RemoveRange(seriesToRemove); + + LibraryCommands.SaveContext(context); + config.SetObject(nameof(migrate_from_7_10_1), true); + } } } diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index 745e8a8f..66ee71e1 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -200,7 +200,7 @@ namespace ApplicationServices var libraryBookImporter = new LibraryBookImporter(context); var newCount = await Task.Run(() => libraryBookImporter.Import(importItems)); logTime("importIntoDbAsync -- post Import()"); - int qtyChanges = saveChanges(context); + int qtyChanges = SaveContext(context); logTime("importIntoDbAsync -- post SaveChanges"); // this is any changes at all to the database, not just new books @@ -211,7 +211,7 @@ namespace ApplicationServices return newCount; } - private static int saveChanges(LibationContext context) + public static int SaveContext(LibationContext context) { try { From c48eacd9af90fd500d275dc96f6d4eb487e2648b Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Tue, 7 Jun 2022 15:27:18 -0600 Subject: [PATCH 02/18] Add ContentType.Parent Import Series parent when only individual episodes are in library --- Source/AudibleUtilities/ApiExtended.cs | 79 ++++++++++++++++++----- Source/DataLayer/EfClasses/Book.cs | 10 ++- Source/DtoImporterService/BookImporter.cs | 12 +++- Source/FileLiberator/Processable.cs | 3 +- 4 files changed, 83 insertions(+), 21 deletions(-) diff --git a/Source/AudibleUtilities/ApiExtended.cs b/Source/AudibleUtilities/ApiExtended.cs index 2a14de01..ff96a9ab 100644 --- a/Source/AudibleUtilities/ApiExtended.cs +++ b/Source/AudibleUtilities/ApiExtended.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using AudibleApi; using AudibleApi.Common; using Dinah.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Polly; using Polly.Retry; @@ -129,7 +132,7 @@ namespace AudibleUtilities await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions)) { - if (item.IsEpisodes && importEpisodes) + if ((item.IsEpisodes || item.IsSeriesParent) && importEpisodes) { //Get child episodes asynchronously and await all at the end getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item)); @@ -173,16 +176,65 @@ namespace AudibleUtilities { Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent); - var children = await getEpisodeChildrenAsync(parent); + List children; - if (!children.Any()) + if (parent.IsEpisodes) { - //The parent is the only episode in the podcase series, - //so the parent is its own child. - parent.Series = new Series[] { new Series { Asin = parent.Asin, Sequence = RelationshipToProduct.Parent, Title = parent.TitleWithSubtitle } }; - children.Add(parent); - return children; + //The 'parent' is a single episode that was added to the library. + //Get the episode's parent and add it to the database. + + Serilog.Log.Logger.Debug("Supplied Parent is an episode. Beginning parent scan for {parent}", parent); + + children = new() { parent }; + + var parentAsins = parent.Relationships + .Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent) + .Select(p => p.Asin); + + var seriesParents = await Api.GetCatalogProductsAsync(parentAsins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS); + + int numSeriesParents = seriesParents.Count(p => p.IsSeriesParent); + if (numSeriesParents != 1) + { + //There should only ever be 1 top-level parent per episode. If not, log + //and throw so we can figure out what to do about those special cases. + JsonSerializerSettings Settings = new() + { + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + DateParseHandling = DateParseHandling.None, + Converters = + { + new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } + }, + }; + var ex = new ApplicationException($"Found {numSeriesParents} parents for {parent.Asin}"); + Serilog.Log.Logger.Error(ex, $"Episode Product:\r\n{JsonConvert.SerializeObject(parent, Formatting.None, Settings)}"); + throw ex; + } + + var realParent = seriesParents.Single(p => p.IsSeriesParent); + realParent.PurchaseDate = parent.PurchaseDate; + + Serilog.Log.Logger.Debug("Completed parent scan for {parent}", parent); + parent = realParent; } + else + { + children = await getEpisodeChildrenAsync(parent); + if (!children.Any()) + return new(); + } + + //A series parent will always have exactly 1 Series + parent.Series = new Series[] + { + new Series + { + Asin = parent.Asin, + Sequence = "-1", + Title = parent.TitleWithSubtitle + } + }; foreach (var child in children) { @@ -199,17 +251,10 @@ namespace AudibleUtilities Title = parent.TitleWithSubtitle } }; - // overload (read: abuse) IsEpisodes flag - child.Relationships = new Relationship[] - { - new Relationship - { - RelationshipToProduct = RelationshipToProduct.Child, - RelationshipType = RelationshipType.Episode - } - }; } + children.Add(parent); + Serilog.Log.Logger.Debug("Completed episode scan for {parent}", parent); return children; diff --git a/Source/DataLayer/EfClasses/Book.cs b/Source/DataLayer/EfClasses/Book.cs index 804df40a..ef072484 100644 --- a/Source/DataLayer/EfClasses/Book.cs +++ b/Source/DataLayer/EfClasses/Book.cs @@ -16,8 +16,14 @@ namespace DataLayer } } - // enum will be easier than bool to extend later - public enum ContentType { Unknown = 0, Product = 1, Episode = 2 } + // enum will be easier than bool to extend later. + public enum ContentType + { + Unknown = 0, + Product = 1, + Episode = 2, + Parent = 4, + } public class Book { diff --git a/Source/DtoImporterService/BookImporter.cs b/Source/DtoImporterService/BookImporter.cs index bc69f382..297ae659 100644 --- a/Source/DtoImporterService/BookImporter.cs +++ b/Source/DtoImporterService/BookImporter.cs @@ -75,7 +75,7 @@ namespace DtoImporterService { var item = importItem.DtoItem; - var contentType = item.IsEpisodes ? DataLayer.ContentType.Episode : DataLayer.ContentType.Product; + var contentType = GetContentType(item); // absence of authors is very rare, but possible if (!item.Authors?.Any() ?? true) @@ -184,5 +184,15 @@ namespace DtoImporterService } } } + + private static DataLayer.ContentType GetContentType(Item item) + { + if (item.IsEpisodes) + return DataLayer.ContentType.Episode; + else if (item.IsSeriesParent) + return DataLayer.ContentType.Parent; + else + return DataLayer.ContentType.Product; + } } } diff --git a/Source/FileLiberator/Processable.cs b/Source/FileLiberator/Processable.cs index b8b984ff..ef1bb139 100644 --- a/Source/FileLiberator/Processable.cs +++ b/Source/FileLiberator/Processable.cs @@ -29,7 +29,8 @@ namespace FileLiberator public IEnumerable GetValidLibraryBooks(IEnumerable library) => library.Where(libraryBook => Validate(libraryBook) - && (libraryBook.Book.ContentType != ContentType.Episode || LibationFileManager.Configuration.Instance.DownloadEpisodes) + && libraryBook.Book.ContentType != ContentType.Parent + && (libraryBook.Book.ContentType != ContentType.Episode || Configuration.Instance.DownloadEpisodes) ); public async Task ProcessSingleAsync(LibraryBook libraryBook, bool validate) From 920f4df213c0b3a11deda3fff9d93277f652bf54 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Tue, 7 Jun 2022 15:28:16 -0600 Subject: [PATCH 03/18] Use new ContentType.Parent to add series info to grid display --- Source/LibationWinForms/GridView/GridEntry.cs | 120 +++++++---- .../GridView/GridEntryBindingList.cs | 14 +- .../GridView/LibraryBookEntry.cs | 89 +-------- .../GridView/ProductsDisplay.Designer.cs | 8 +- .../GridView/ProductsDisplay.cs | 5 +- .../LibationWinForms/GridView/ProductsGrid.cs | 188 +++++++++++------- .../GridView/QueryExtensions.cs | 58 ++++++ .../LibationWinForms/GridView/SeriesEntry.cs | 106 ++++------ 8 files changed, 313 insertions(+), 275 deletions(-) create mode 100644 Source/LibationWinForms/GridView/QueryExtensions.cs diff --git a/Source/LibationWinForms/GridView/GridEntry.cs b/Source/LibationWinForms/GridView/GridEntry.cs index 4b1a614c..b2e2ee21 100644 --- a/Source/LibationWinForms/GridView/GridEntry.cs +++ b/Source/LibationWinForms/GridView/GridEntry.cs @@ -1,4 +1,5 @@ using DataLayer; +using Dinah.Core; using Dinah.Core.DataBinding; using Dinah.Core.Drawing; using LibationFileManager; @@ -10,11 +11,14 @@ using System.Linq; namespace LibationWinForms.GridView { + /// The View Model base for the DataGridView public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable { - protected abstract Book Book { get; } - + public string AudibleProductId => Book.AudibleProductId; + public LibraryBook LibraryBook { get; protected set; } + protected Book Book => LibraryBook.Book; private Image _cover; + #region Model properties exposed to the view public Image Cover { @@ -25,20 +29,20 @@ namespace LibationWinForms.GridView NotifyPropertyChanged(); } } - public new bool InvokeRequired => base.InvokeRequired; + public float SeriesIndex { get; protected set; } + public string ProductRating { get; protected set; } + public string PurchaseDate { get; protected set; } + public string MyRating { get; protected set; } + public string Series { get; protected set; } + public string Title { get; protected set; } + public string Length { get; protected set; } + public string Authors { get; protected set; } + public string Narrators { get; protected set; } + public string Category { get; protected set; } + public string Misc { get; protected set; } + public string Description { get; protected set; } + public string LongDescription { get; protected set; } public abstract DateTime DateAdded { get; } - public abstract float SeriesIndex { get; } - public abstract string ProductRating { get; protected set; } - public abstract string PurchaseDate { get; protected set; } - public abstract string MyRating { get; protected set; } - public abstract string Series { get; protected set; } - public abstract string Title { get; protected set; } - public abstract string Length { get; protected set; } - public abstract string Authors { get; protected set; } - public abstract string Narrators { get; protected set; } - public abstract string Category { get; protected set; } - public abstract string Misc { get; protected set; } - public abstract string Description { get; protected set; } public abstract string DisplayTags { get; } public abstract LiberateButtonStatus Liberate { get; } #endregion @@ -54,6 +58,17 @@ namespace LibationWinForms.GridView public virtual object GetMemberValue(string memberName) => _memberValues[memberName](); public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType]; + // Instantiate comparers for every exposed member object type. + private static readonly Dictionary _memberTypeComparers = new() + { + { typeof(string), new ObjectComparer() }, + { typeof(int), new ObjectComparer() }, + { typeof(float), new ObjectComparer() }, + { typeof(bool), new ObjectComparer() }, + { typeof(DateTime), new ObjectComparer() }, + { typeof(LiberateButtonStatus), new ObjectComparer() }, + }; + #endregion protected void LoadCover() @@ -79,36 +94,61 @@ namespace LibationWinForms.GridView } } - // Instantiate comparers for every exposed member object type. - private static readonly Dictionary _memberTypeComparers = new() + #region Static library display functions + + /// + /// This information should not change during lifetime, so call only once. + /// + protected static string GetDescriptionDisplay(Book book) { - { typeof(string), new ObjectComparer() }, - { typeof(int), new ObjectComparer() }, - { typeof(float), new ObjectComparer() }, - { typeof(bool), new ObjectComparer() }, - { typeof(DateTime), new ObjectComparer() }, - { typeof(LiberateButtonStatus), new ObjectComparer() }, - }; + var doc = new HtmlAgilityPack.HtmlDocument(); + doc.LoadHtml(book?.Description?.Replace("

", "\r\n\r\n

") ?? ""); + return doc.DocumentNode.InnerText.Trim(); + } + + protected static string TrimTextToWord(string text, int maxLength) + { + return + text.Length <= maxLength ? + text : + text.Substring(0, maxLength - 3) + "..."; + } + + + /// + /// This information should not change during lifetime, so call only once. + /// Maximum of 5 text rows will fit in 80-pixel row height. + /// + protected static string GetMiscDisplay(LibraryBook libraryBook) + { + var details = new List(); + + var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]"); + var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]"); + + details.Add($"Account: {locale} - {acct}"); + + if (libraryBook.Book.HasPdf()) + details.Add("Has PDF"); + if (libraryBook.Book.IsAbridged) + details.Add("Abridged"); + if (libraryBook.Book.DatePublished.HasValue) + details.Add($"Date pub'd: {libraryBook.Book.DatePublished.Value:MM/dd/yyyy}"); + // this goes last since it's most likely to have a line-break + if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher)) + details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}"); + + if (!details.Any()) + return "[details not imported]"; + + return string.Join("\r\n", details); + } + + #endregion ~GridEntry() { PictureStorage.PictureCached -= PictureStorage_PictureCached; } } - - internal static class GridEntryExtensions - { -#nullable enable - public static IEnumerable Series(this IEnumerable gridEntries) - => gridEntries.OfType(); - public static IEnumerable LibraryBooks(this IEnumerable gridEntries) - => gridEntries.OfType(); - public static LibraryBookEntry? FindBookByAsin(this IEnumerable gridEntries, string audibleProductID) - => gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID); - public static SeriesEntry? FindBookSeriesEntry(this IEnumerable gridEntries, IEnumerable matchSeries) - => gridEntries.Series().FirstOrDefault(i => matchSeries.Any(s => s.Series.Name == i.Series)); - public static IEnumerable EmptySeries(this IEnumerable gridEntries) - => gridEntries.Series().Where(i => i.Children.Count == 0); - public static bool IsEpisodeChild(this LibraryBook lb) => lb.Book.ContentType == ContentType.Episode; - } } diff --git a/Source/LibationWinForms/GridView/GridEntryBindingList.cs b/Source/LibationWinForms/GridView/GridEntryBindingList.cs index 58be850c..e2b45c61 100644 --- a/Source/LibationWinForms/GridView/GridEntryBindingList.cs +++ b/Source/LibationWinForms/GridView/GridEntryBindingList.cs @@ -73,10 +73,10 @@ namespace LibationWinForms.GridView FilterString = filterString; SearchResults = SearchEngineCommands.Search(filterString); - var booksFilteredIn = Items.LibraryBooks().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe); + var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe); //Find all series containing children that match the search criteria - var seriesFilteredIn = Items.Series().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any()); + var seriesFilteredIn = Items.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any()); var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList(); @@ -89,19 +89,19 @@ namespace LibationWinForms.GridView public void CollapseAll() { - foreach (var series in Items.Series().ToList()) + foreach (var series in Items.SeriesEntries().ToList()) CollapseItem(series); } public void ExpandAll() { - foreach (var series in Items.Series().ToList()) + foreach (var series in Items.SeriesEntries().ToList()) ExpandItem(series); } public void CollapseItem(SeriesEntry sEntry) { - foreach (var episode in Items.LibraryBooks().Where(b => b.Parent == sEntry).ToList()) + foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).ToList()) { FilterRemoved.Add(episode); base.Remove(episode); @@ -114,7 +114,7 @@ namespace LibationWinForms.GridView { var sindex = Items.IndexOf(sEntry); - foreach (var episode in FilterRemoved.LibraryBooks().Where(b => b.Parent == sEntry).ToList()) + foreach (var episode in FilterRemoved.BookEntries().Where(b => b.Parent == sEntry).ToList()) { if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId)) { @@ -174,7 +174,7 @@ namespace LibationWinForms.GridView { var itemsList = (List)Items; - var children = itemsList.LibraryBooks().Where(i => i.Parent is not null).ToList(); + var children = itemsList.BookEntries().Where(i => i.Parent is not null).ToList(); var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList(); diff --git a/Source/LibationWinForms/GridView/LibraryBookEntry.cs b/Source/LibationWinForms/GridView/LibraryBookEntry.cs index be6836a8..cddde1f5 100644 --- a/Source/LibationWinForms/GridView/LibraryBookEntry.cs +++ b/Source/LibationWinForms/GridView/LibraryBookEntry.cs @@ -3,29 +3,13 @@ using DataLayer; using Dinah.Core; using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; namespace LibationWinForms.GridView { - /// - /// The View Model for a LibraryBook - /// + /// The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode public class LibraryBookEntry : GridEntry { - #region implementation properties NOT exposed to the view - // hide from public fields from Data Source GUI with [Browsable(false)] - - [Browsable(false)] - public string AudibleProductId => Book.AudibleProductId; - [Browsable(false)] - public LibraryBook LibraryBook { get; private set; } - [Browsable(false)] - public string LongDescription { get; private set; } - #endregion - - protected override Book Book => LibraryBook.Book; - #region Model properties exposed to the view private DateTime lastStatusUpdate = default; @@ -33,21 +17,8 @@ namespace LibationWinForms.GridView private LiberatedStatus? _pdfStatus; public override DateTime DateAdded => LibraryBook.DateAdded; - public override float SeriesIndex => Book.SeriesLink.FirstOrDefault()?.Index ?? 0; - public override string ProductRating { get; protected set; } - public override string PurchaseDate { get; protected set; } - public override string MyRating { get; protected set; } - public override string Series { get; protected set; } - public override string Title { get; protected set; } - public override string Length { get; protected set; } - public override string Authors { get; protected set; } - public override string Narrators { get; protected set; } - public override string Category { get; protected set; } - public override string Misc { get; protected set; } - public override string Description { get; protected set; } public override string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated); - // these 2 values being in 1 field is the trick behind getting the liberated+pdf 'stoplight' icon to draw. See: LiberateDataGridViewImageButtonCell.Paint public override LiberateButtonStatus Liberate { get @@ -62,15 +33,17 @@ namespace LibationWinForms.GridView return new LiberateButtonStatus { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false }; } } + #endregion + public SeriesEntry Parent { get; init; } + public LibraryBookEntry(LibraryBook libraryBook) { setLibraryBook(libraryBook); LoadCover(); } - public SeriesEntry Parent { get; init; } public void UpdateLibraryBook(LibraryBook libraryBook) { if (AudibleProductId != libraryBook.Book.AudibleProductId) @@ -100,12 +73,12 @@ namespace LibationWinForms.GridView Misc = GetMiscDisplay(libraryBook); LongDescription = GetDescriptionDisplay(Book); Description = TrimTextToWord(LongDescription, 62); + SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0; } UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged; } - #region detect changes to the model, update the view, and save to database. /// @@ -169,58 +142,6 @@ namespace LibationWinForms.GridView { nameof(DateAdded), () => DateAdded }, }; - - #endregion - - #region Static library display functions - - /// - /// This information should not change during lifetime, so call only once. - /// - private static string GetDescriptionDisplay(Book book) - { - var doc = new HtmlAgilityPack.HtmlDocument(); - doc.LoadHtml(book?.Description?.Replace("

", "\r\n\r\n

") ?? ""); - return doc.DocumentNode.InnerText.Trim(); - } - - private static string TrimTextToWord(string text, int maxLength) - { - return - text.Length <= maxLength ? - text : - text.Substring(0, maxLength - 3) + "..."; - } - - /// - /// This information should not change during lifetime, so call only once. - /// Maximum of 5 text rows will fit in 80-pixel row height. - /// - private static string GetMiscDisplay(LibraryBook libraryBook) - { - var details = new List(); - - var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]"); - var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]"); - - details.Add($"Account: {locale} - {acct}"); - - if (libraryBook.Book.HasPdf()) - details.Add("Has PDF"); - if (libraryBook.Book.IsAbridged) - details.Add("Abridged"); - if (libraryBook.Book.DatePublished.HasValue) - details.Add($"Date pub'd: {libraryBook.Book.DatePublished.Value:MM/dd/yyyy}"); - // this goes last since it's most likely to have a line-break - if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher)) - details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}"); - - if (!details.Any()) - return "[details not imported]"; - - return string.Join("\r\n", details); - } - #endregion ~LibraryBookEntry() diff --git a/Source/LibationWinForms/GridView/ProductsDisplay.Designer.cs b/Source/LibationWinForms/GridView/ProductsDisplay.Designer.cs index c4b1a543..db9ca6a5 100644 --- a/Source/LibationWinForms/GridView/ProductsDisplay.Designer.cs +++ b/Source/LibationWinForms/GridView/ProductsDisplay.Designer.cs @@ -39,10 +39,10 @@ this.productsGrid.Name = "productsGrid"; this.productsGrid.Size = new System.Drawing.Size(1510, 380); this.productsGrid.TabIndex = 0; - this.productsGrid.LiberateClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked); - this.productsGrid.CoverClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_CoverClicked); - this.productsGrid.DetailsClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked); - this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked); + this.productsGrid.LiberateClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked); + this.productsGrid.CoverClicked += new LibationWinForms.GridView.GridEntryClickedEventHandler(this.productsGrid_CoverClicked); + this.productsGrid.DetailsClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked); + this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.GridEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked); this.productsGrid.VisibleCountChanged += new System.EventHandler(this.productsGrid_VisibleCountChanged); // // ProductsDisplay diff --git a/Source/LibationWinForms/GridView/ProductsDisplay.cs b/Source/LibationWinForms/GridView/ProductsDisplay.cs index 7c411f3f..5f91a581 100644 --- a/Source/LibationWinForms/GridView/ProductsDisplay.cs +++ b/Source/LibationWinForms/GridView/ProductsDisplay.cs @@ -29,7 +29,7 @@ namespace LibationWinForms.GridView #region Button controls private ImageDisplay imageDisplay; - private async void productsGrid_CoverClicked(LibraryBookEntry liveGridEntry) + private async void productsGrid_CoverClicked(GridEntry liveGridEntry) { var picDefinition = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native); var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition)); @@ -52,7 +52,7 @@ namespace LibationWinForms.GridView imageDisplay.CoverPicture = await picDlTask; } - private void productsGrid_DescriptionClicked(LibraryBookEntry liveGridEntry, Rectangle cellRectangle) + private void productsGrid_DescriptionClicked(GridEntry liveGridEntry, Rectangle cellRectangle) { var displayWindow = new DescriptionDisplay { @@ -103,7 +103,6 @@ namespace LibationWinForms.GridView { Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay)); } - } #endregion diff --git a/Source/LibationWinForms/GridView/ProductsGrid.cs b/Source/LibationWinForms/GridView/ProductsGrid.cs index 24dafc73..b762a046 100644 --- a/Source/LibationWinForms/GridView/ProductsGrid.cs +++ b/Source/LibationWinForms/GridView/ProductsGrid.cs @@ -10,23 +10,24 @@ using System.Windows.Forms; namespace LibationWinForms.GridView { + public delegate void GridEntryClickedEventHandler(GridEntry liveGridEntry); + public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry); + public delegate void GridEntryRectangleClickedEventHandler(GridEntry liveGridEntry, Rectangle cellRectangle); + public partial class ProductsGrid : UserControl { - public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry); - public delegate void LibraryBookEntryRectangleClickedEventHandler(LibraryBookEntry liveGridEntry, Rectangle cellRectangle); - /// Number of visible rows has changed public event EventHandler VisibleCountChanged; public event LibraryBookEntryClickedEventHandler LiberateClicked; - public event LibraryBookEntryClickedEventHandler CoverClicked; + public event GridEntryClickedEventHandler CoverClicked; public event LibraryBookEntryClickedEventHandler DetailsClicked; - public event LibraryBookEntryRectangleClickedEventHandler DescriptionClicked; + public event GridEntryRectangleClickedEventHandler DescriptionClicked; public new event EventHandler Scroll; private GridEntryBindingList bindingList; internal IEnumerable GetVisible() => bindingList - .LibraryBooks(); + .BookEntries(); public ProductsGrid() { @@ -61,16 +62,23 @@ namespace LibationWinForms.GridView else if (e.ColumnIndex == coverGVColumn.Index) CoverClicked?.Invoke(lbEntry); } - else if (entry is SeriesEntry sEntry && e.ColumnIndex == liberateGVColumn.Index) + else if (entry is SeriesEntry sEntry) { - if (sEntry.Liberate.Expanded) - bindingList.CollapseItem(sEntry); - else - bindingList.ExpandItem(sEntry); + if (e.ColumnIndex == liberateGVColumn.Index) + { + if (sEntry.Liberate.Expanded) + bindingList.CollapseItem(sEntry); + else + bindingList.ExpandItem(sEntry); - sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate)); + sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate)); - VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count()); + VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count()); + } + else if (e.ColumnIndex == descriptionGVColumn.Index) + DescriptionClicked?.Invoke(sEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false)); + else if (e.ColumnIndex == coverGVColumn.Index) + CoverClicked?.Invoke(sEntry); } } @@ -82,14 +90,18 @@ namespace LibationWinForms.GridView internal void BindToGrid(List dbBooks) { - var geList = dbBooks.Where(b => b.Book.ContentType is not ContentType.Episode).Select(b => new LibraryBookEntry(b)).Cast().ToList(); + var geList = dbBooks.Where(lb => lb.IsProduct()).Select(b => new LibraryBookEntry(b)).Cast().ToList(); - var episodes = dbBooks.Where(b => b.IsEpisodeChild()).ToList(); - - var allSeries = episodes.SelectMany(lb => lb.Book.SeriesLink.Where(s => !s.Series.AudibleSeriesId.StartsWith("SERIES_"))).DistinctBy(s => s.Series).ToList(); - foreach (var series in allSeries) + var parents = dbBooks.Where(lb => lb.IsEpisodeParent()); + var episodes = dbBooks.Where(lb => lb.IsEpisodeChild()); + + foreach (var parent in parents) { - var seriesEntry = new SeriesEntry(series, episodes.Where(lb => lb.Book.SeriesLink.Any(s => s.Series == series.Series))); + var seriesEpisodes = episodes.Where(lb => lb.Book.SeriesLink?.Any(s => s.Series.AudibleSeriesId == parent.Book.AudibleProductId) == true).ToList(); + + if (!seriesEpisodes.Any()) continue; + + var seriesEntry = new SeriesEntry(parent, seriesEpisodes); geList.Add(seriesEntry); geList.AddRange(seriesEntry.Children); @@ -98,79 +110,47 @@ namespace LibationWinForms.GridView bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded)); bindingList.CollapseAll(); syncBindingSource.DataSource = bindingList; - VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count()); - } + VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count()); + } internal void UpdateGrid(List dbBooks) { + #region Add new or update existing grid entries + + //Remove filter prior to adding/updating boooks string existingFilter = syncBindingSource.Filter; Filter(null); bindingList.SuspendFilteringOnUpdate = true; - //Add absent books to grid, or update current books + //Add absent entries to grid, or update existing entry - var allItmes = bindingList.AllItems().LibraryBooks(); - foreach (var libraryBook in dbBooks) + var allEntries = bindingList.AllItems().BookEntries(); + var seriesEntries = bindingList.AllItems().SeriesEntries().ToList(); + + foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded)) { - var existingItem = allItmes.FindBookByAsin(libraryBook.Book.AudibleProductId); + var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId); - // add new to top - if (existingItem is null) - { - if (libraryBook.IsEpisodeChild()) - { - LibraryBookEntry lbe; - //Find the series that libraryBook belongs to, if it exists - var series = bindingList.AllItems().FindBookSeriesEntry(libraryBook.Book.SeriesLink); - - if (series is null) - { - //Series doesn't exist yet, so create and add it - var newSeries = new SeriesEntry(libraryBook.Book.SeriesLink.First(), libraryBook); - lbe = newSeries.Children[0]; - newSeries.Liberate.Expanded = true; - bindingList.Insert(0, newSeries); - series = newSeries; - } - else - { - lbe = new(libraryBook) { Parent = series }; - series.Children.Add(lbe); - } - //Add episode beneath the parent - int seriesIndex = bindingList.IndexOf(series); - bindingList.Insert(seriesIndex + 1, lbe); - - if (series.Liberate.Expanded) - bindingList.ExpandItem(series); - else - bindingList.CollapseItem(series); - - series.NotifyPropertyChanged(); - } - else if (libraryBook.Book.ContentType is not ContentType.Episode) - //Add the new product - bindingList.Insert(0, new LibraryBookEntry(libraryBook)); - } - // update existing - else - { - existingItem.UpdateLibraryBook(libraryBook); - } - } + if (libraryBook.IsEpisodeChild()) + AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks); + else if (libraryBook.IsProduct()) + AddOrUpdateBook(libraryBook, existingEntry); + } bindingList.SuspendFilteringOnUpdate = false; - //Re-filter after updating existing / adding new books to capture any changes + //Re-apply filter after adding new/updating existing books to capture any changes Filter(existingFilter); + #endregion + // remove deleted from grid. // note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this var removedBooks = bindingList .AllItems() - .LibraryBooks() + .BookEntries() .ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId); //Remove books in series from their parents' Children list @@ -190,7 +170,69 @@ namespace LibationWinForms.GridView //no need to re-filter for removed books bindingList.Remove(removed); - VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count()); + VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count()); + } + + private void AddOrUpdateBook(LibraryBook book, LibraryBookEntry existingBookEntry) + { + if (existingBookEntry is null) + // Add the new product to top + bindingList.Insert(0, new LibraryBookEntry(book)); + else + // update existing + existingBookEntry.UpdateLibraryBook(book); + } + + private void AddOrUpdateEpisode(LibraryBook episodeBook, LibraryBookEntry existingEpisodeEntry, List seriesEntries, IEnumerable dbBooks) + { + if (existingEpisodeEntry is null) + { + LibraryBookEntry episodeEntry; + var seriesEntry = seriesEntries.FindSeriesParent(episodeBook); + + if (seriesEntry is null) + { + //Series doesn't exist yet, so create and add it + var seriesBook = dbBooks.FindSeriesParent(episodeBook); + + if (seriesBook is null) + { + var ex = new ApplicationException($"Episode's series parent not found in database."); + var seriesLinks = string.Join("\r\n", episodeBook.Book.SeriesLink?.Select(sb => $"{nameof(sb.Series.Name)}={sb.Series.Name}, {nameof(sb.Series.AudibleSeriesId)}={sb.Series.AudibleSeriesId}")); + Serilog.Log.Logger.Error(ex, "Episode={episodeBook}, Series: {seriesLinks}", episodeBook, seriesLinks); + throw ex; + } + + seriesEntry = new SeriesEntry(seriesBook, episodeBook); + seriesEntries.Add(seriesEntry); + + episodeEntry = seriesEntry.Children[0]; + seriesEntry.Liberate.Expanded = true; + bindingList.Insert(0, seriesEntry); + } + else + { + //Series exists. Create and add episode child then update the SeriesEntry + episodeEntry = new(episodeBook) { Parent = seriesEntry }; + seriesEntry.Children.Add(episodeEntry); + var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId); + seriesEntry.UpdateSeries(seriesBook); + } + + //Add episode to the grid beneath the parent + int seriesIndex = bindingList.IndexOf(seriesEntry); + bindingList.Insert(seriesIndex + 1, episodeEntry); + + if (seriesEntry.Liberate.Expanded) + bindingList.ExpandItem(seriesEntry); + else + bindingList.CollapseItem(seriesEntry); + + seriesEntry.NotifyPropertyChanged(); + + } + else + existingEpisodeEntry.UpdateLibraryBook(episodeBook); } #endregion @@ -207,7 +249,7 @@ namespace LibationWinForms.GridView syncBindingSource.Filter = searchString; if (visibleCount != bindingList.Count) - VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count()); + VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count()); } diff --git a/Source/LibationWinForms/GridView/QueryExtensions.cs b/Source/LibationWinForms/GridView/QueryExtensions.cs new file mode 100644 index 00000000..e6df05cb --- /dev/null +++ b/Source/LibationWinForms/GridView/QueryExtensions.cs @@ -0,0 +1,58 @@ +using DataLayer; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace LibationWinForms.GridView +{ +#nullable enable + internal static class QueryExtensions + { + public static IEnumerable BookEntries(this IEnumerable gridEntries) + => gridEntries.OfType(); + + public static IEnumerable SeriesEntries(this IEnumerable gridEntries) + => gridEntries.OfType(); + + public static T? FindByAsin(this IEnumerable gridEntries, string audibleProductID) where T : GridEntry + => gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID); + + public static IEnumerable EmptySeries(this IEnumerable gridEntries) + => gridEntries.SeriesEntries().Where(i => i.Children.Count == 0); + + public static bool IsProduct(this LibraryBook lb) + => lb.Book.ContentType is not ContentType.Episode and not ContentType.Parent; + + public static bool IsEpisodeChild(this LibraryBook lb) + => lb.Book.ContentType is ContentType.Episode; + + public static bool IsEpisodeParent(this LibraryBook lb) + => lb.Book.ContentType is ContentType.Parent; + + public static SeriesEntry? FindSeriesParent(this IEnumerable gridEntries, LibraryBook seriesEpisode) + { + if (seriesEpisode.Book.SeriesLink is null) return null; + + //Parent books will always have exactly 1 SeriesBook due to how + //they are imported in ApiExtended.getChildEpisodesAsync() + return gridEntries.SeriesEntries().FirstOrDefault( + lb => + seriesEpisode.Book.SeriesLink.Any( + s => s.Series.AudibleSeriesId == lb.LibraryBook.Book.SeriesLink.Single().Series.AudibleSeriesId)); + } + + public static LibraryBook? FindSeriesParent(this IEnumerable libraryBooks, LibraryBook seriesEpisode) + { + if (seriesEpisode.Book.SeriesLink is null) return null; + + //Parent books will always have exactly 1 SeriesBook due to how + //they are imported in ApiExtended.getChildEpisodesAsync() + return libraryBooks.FirstOrDefault( + lb => + lb.IsEpisodeParent() && + seriesEpisode.Book.SeriesLink.Any( + s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId)); + } + } +#nullable disable +} diff --git a/Source/LibationWinForms/GridView/SeriesEntry.cs b/Source/LibationWinForms/GridView/SeriesEntry.cs index 96d40f49..781695f0 100644 --- a/Source/LibationWinForms/GridView/SeriesEntry.cs +++ b/Source/LibationWinForms/GridView/SeriesEntry.cs @@ -6,101 +6,79 @@ using System.Linq; namespace LibationWinForms.GridView { + /// The View Model for a LibraryBook that is ContentType.Parent public class SeriesEntry : GridEntry { - public List Children { get; init; } + public List Children { get; } = new(); public override DateTime DateAdded => Children.Max(c => c.DateAdded); - public override float SeriesIndex { get; } - public override string ProductRating - { - get - { - var productAverageRating = new Rating(Children.Average(c => c.LibraryBook.Book.Rating.OverallRating), Children.Average(c => c.LibraryBook.Book.Rating.PerformanceRating), Children.Average(c => c.LibraryBook.Book.Rating.StoryRating)); - return productAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace(""); - } - protected set => throw new NotImplementedException(); - } - public override string PurchaseDate { get; protected set; } - public override string MyRating - { - get - { - var myAverageRating = new Rating(Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.OverallRating), Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.PerformanceRating), Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.StoryRating)); - return myAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace(""); - } - protected set => throw new NotImplementedException(); - } - public override string Series { get; protected set; } - public override string Title { get; protected set; } - public override string Length - { - get - { - int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes); - return bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min"; - } - protected set => throw new NotImplementedException(); - } - public override string Authors { get; protected set; } - public override string Narrators { get; protected set; } - public override string Category { get; protected set; } - public override string Misc { get; protected set; } = string.Empty; - public override string Description { get; protected set; } = string.Empty; public override string DisplayTags { get; } = string.Empty; - public override LiberateButtonStatus Liberate { get; } - protected override Book Book => SeriesBook.Book; - - private SeriesBook SeriesBook { get; set; } - - private SeriesEntry(SeriesBook seriesBook) + private SeriesEntry(LibraryBook parent) { + LibraryBook = parent; Liberate = new LiberateButtonStatus { IsSeries = true }; - SeriesIndex = seriesBook.Index; + SeriesIndex = -1; } - public SeriesEntry(SeriesBook seriesBook, IEnumerable children) : this(seriesBook) + + public SeriesEntry(LibraryBook parent, IEnumerable children) : this(parent) { - Children = children.Select(c => new LibraryBookEntry(c) { Parent = this }).OrderBy(c => c.SeriesIndex).ToList(); - SetSeriesBook(seriesBook); + Children = children + .Select(c => new LibraryBookEntry(c) { Parent = this }) + .OrderBy(c => c.SeriesIndex) + .ToList(); + + UpdateSeries(parent); + LoadCover(); } - public SeriesEntry(SeriesBook seriesBook, LibraryBook child) : this(seriesBook) + + public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent) { Children = new() { new LibraryBookEntry(child) { Parent = this } }; - SetSeriesBook(seriesBook); + + UpdateSeries(parent); + LoadCover(); } - private void SetSeriesBook(SeriesBook seriesBook) + public void UpdateSeries(LibraryBook libraryBook) { - SeriesBook = seriesBook; - LoadCover(); + LibraryBook = libraryBook; // Immutable properties { - Title = SeriesBook.Series.Name; - Series = SeriesBook.Series.Name; + Title = Book.Title; + Series = Book.SeriesNames(); + MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d"); + ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); Authors = Book.AuthorNames(); Narrators = Book.NarratorNames(); Category = string.Join(" > ", Book.CategoriesNames()); - } - } + Misc = GetMiscDisplay(libraryBook); + LongDescription = GetDescriptionDisplay(Book); + Description = TrimTextToWord(LongDescription, 62); + int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes); + Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min"; + } + + NotifyPropertyChanged(); + } /// Create getters for all member object values by name protected override Dictionary> CreateMemberValueDictionary() => new() { - { nameof(Title), () => Book.SeriesSortable() }, + { nameof(Title), () => Book.TitleSortable() }, { nameof(Series), () => Book.SeriesSortable() }, { nameof(Length), () => Children.Sum(c => c.LibraryBook.Book.LengthInMinutes) }, - { nameof(MyRating), () => Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.FirstScore()) }, + { nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore() }, { nameof(PurchaseDate), () => Children.Min(c => c.LibraryBook.DateAdded) }, - { nameof(ProductRating), () => Children.Average(c => c.LibraryBook.Book.Rating.FirstScore()) }, - { nameof(Authors), () => string.Empty }, - { nameof(Narrators), () => string.Empty }, - { nameof(Description), () => string.Empty }, - { nameof(Category), () => string.Empty }, - { nameof(Misc), () => string.Empty }, + { nameof(ProductRating), () => Book.Rating.FirstScore() }, + { nameof(Authors), () => Authors }, + { nameof(Narrators), () => Narrators }, + { nameof(Description), () => Description }, + { nameof(Category), () => Category }, + { nameof(Misc), () => Misc }, { nameof(DisplayTags), () => string.Empty }, { nameof(Liberate), () => Liberate }, { nameof(DateAdded), () => DateAdded }, From 5cbe72863107c164c168675cbdcc0ae9baabfed8 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Tue, 7 Jun 2022 15:32:49 -0600 Subject: [PATCH 04/18] Don't add series parents to list --- Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs b/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs index 84e5ff51..bd6f1fc0 100644 --- a/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs +++ b/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs @@ -39,6 +39,7 @@ namespace LibationWinForms.Dialogs _dataGridView.BindingContextChanged += _dataGridView_BindingContextChanged; var orderedGridEntries = _libraryBooks + .Where(lb => lb.Book.ContentType is not ContentType.Parent) .Select(lb => new RemovableGridEntry(lb)) .OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate))) .ToList(); From 0729e4ab0921a907a11d8bc0b900fec7aec052e7 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Tue, 7 Jun 2022 15:38:11 -0600 Subject: [PATCH 05/18] Minor refactor --- Source/LibationWinForms/GridView/SeriesEntry.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Source/LibationWinForms/GridView/SeriesEntry.cs b/Source/LibationWinForms/GridView/SeriesEntry.cs index 781695f0..fd8411b8 100644 --- a/Source/LibationWinForms/GridView/SeriesEntry.cs +++ b/Source/LibationWinForms/GridView/SeriesEntry.cs @@ -9,19 +9,18 @@ namespace LibationWinForms.GridView /// The View Model for a LibraryBook that is ContentType.Parent public class SeriesEntry : GridEntry { - public List Children { get; } = new(); + public List Children { get; } public override DateTime DateAdded => Children.Max(c => c.DateAdded); public override string DisplayTags { get; } = string.Empty; public override LiberateButtonStatus Liberate { get; } - private SeriesEntry(LibraryBook parent) + private SeriesEntry() { - LibraryBook = parent; Liberate = new LiberateButtonStatus { IsSeries = true }; SeriesIndex = -1; } - public SeriesEntry(LibraryBook parent, IEnumerable children) : this(parent) + public SeriesEntry(LibraryBook parent, IEnumerable children) : this() { Children = children .Select(c => new LibraryBookEntry(c) { Parent = this }) @@ -32,7 +31,7 @@ namespace LibationWinForms.GridView LoadCover(); } - public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent) + public SeriesEntry(LibraryBook parent, LibraryBook child) : this() { Children = new() { new LibraryBookEntry(child) { Parent = this } }; From 9c6211e8e056715c043f356de5c218bf878ae765 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 08:39:17 -0600 Subject: [PATCH 06/18] Improve UI speed when adding many books to queue at once. --- .../ProcessQueue/ProcessQueueControl.cs | 60 +++++++++++++++++-- .../ProcessQueue/TrackedQueue[T].cs | 9 +++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs index d6b9d322..7da03da5 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs @@ -77,27 +77,64 @@ namespace LibationWinForms.ProcessQueue CompletedCount = 0; } + private bool isBookInQueue(DataLayer.LibraryBook libraryBook) + => Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId); + public void AddDownloadPdf(IEnumerable entries) { + List procs = new(); foreach (var entry in entries) - AddDownloadPdf(entry); + { + if (isBookInQueue(entry)) + continue; + + ProcessBook pbook = new(entry, Logger); + pbook.PropertyChanged += Pbook_DataAvailable; + pbook.AddDownloadPdf(); + procs.Add(pbook); + } + + AddToQueue(procs); } public void AddDownloadDecrypt(IEnumerable entries) { + List procs = new(); foreach (var entry in entries) - AddDownloadDecrypt(entry); + { + if (isBookInQueue(entry)) + continue; + + ProcessBook pbook = new(entry, Logger); + pbook.PropertyChanged += Pbook_DataAvailable; + pbook.AddDownloadDecryptBook(); + pbook.AddDownloadPdf(); + procs.Add(pbook); + } + + AddToQueue(procs); } public void AddConvertMp3(IEnumerable entries) { + List procs = new(); foreach (var entry in entries) - AddConvertMp3(entry); + { + if (isBookInQueue(entry)) + continue; + + ProcessBook pbook = new(entry, Logger); + pbook.PropertyChanged += Pbook_DataAvailable; + pbook.AddConvertToMp3(); + procs.Add(pbook); + } + + AddToQueue(procs); } public void AddDownloadPdf(DataLayer.LibraryBook libraryBook) { - if (Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId)) + if (isBookInQueue(libraryBook)) return; ProcessBook pbook = new(libraryBook, Logger); @@ -108,7 +145,7 @@ namespace LibationWinForms.ProcessQueue public void AddDownloadDecrypt(DataLayer.LibraryBook libraryBook) { - if (Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId)) + if (isBookInQueue(libraryBook)) return; ProcessBook pbook = new(libraryBook, Logger); @@ -120,7 +157,7 @@ namespace LibationWinForms.ProcessQueue public void AddConvertMp3(DataLayer.LibraryBook libraryBook) { - if (Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId)) + if (isBookInQueue(libraryBook)) return; ProcessBook pbook = new(libraryBook, Logger); @@ -129,6 +166,17 @@ namespace LibationWinForms.ProcessQueue AddToQueue(pbook); } + private void AddToQueue(IEnumerable pbook) + { + syncContext.Post(_ => + { + Queue.Enqueue(pbook); + if (!Running) + QueueRunner = QueueLoop(); + }, + null); + } + private void AddToQueue(ProcessBook pbook) { syncContext.Post(_ => diff --git a/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs b/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs index 20bbe7e8..43c8ce98 100644 --- a/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs +++ b/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs @@ -226,5 +226,14 @@ namespace LibationWinForms.ProcessQueue QueuededCountChanged?.Invoke(this, _queued.Count); } } + + public void Enqueue(IEnumerable item) + { + lock (lockObject) + { + _queued.AddRange(item); + QueuededCountChanged?.Invoke(this, _queued.Count); + } + } } } From ee109ba67d35ae296c3183e850a099c5bce63f74 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 08:39:59 -0600 Subject: [PATCH 07/18] Refactor --- Source/LibationWinForms/GridView/GridEntry.cs | 26 ++++++----- .../GridView/ProductsDisplay.cs | 4 +- .../LibationWinForms/GridView/ProductsGrid.cs | 5 ++- .../LibationWinForms/GridView/SeriesEntry.cs | 45 +++++++++---------- 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/Source/LibationWinForms/GridView/GridEntry.cs b/Source/LibationWinForms/GridView/GridEntry.cs index b2e2ee21..69a23e10 100644 --- a/Source/LibationWinForms/GridView/GridEntry.cs +++ b/Source/LibationWinForms/GridView/GridEntry.cs @@ -17,7 +17,6 @@ namespace LibationWinForms.GridView public string AudibleProductId => Book.AudibleProductId; public LibraryBook LibraryBook { get; protected set; } protected Book Book => LibraryBook.Book; - private Image _cover; #region Model properties exposed to the view public Image Cover @@ -50,13 +49,13 @@ namespace LibationWinForms.GridView #region Sorting public GridEntry() => _memberValues = CreateMemberValueDictionary(); - private Dictionary> _memberValues { get; set; } - protected abstract Dictionary> CreateMemberValueDictionary(); // These methods are implementation of Dinah.Core.DataBinding.IMemberComparable // Used by GridEntryBindingList for all sorting public virtual object GetMemberValue(string memberName) => _memberValues[memberName](); public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType]; + protected abstract Dictionary> CreateMemberValueDictionary(); + private Dictionary> _memberValues { get; set; } // Instantiate comparers for every exposed member object type. private static readonly Dictionary _memberTypeComparers = new() @@ -71,18 +70,19 @@ namespace LibationWinForms.GridView #endregion + #region Cover Art + + private Image _cover; protected void LoadCover() { // Get cover art. If it's default, subscribe to PictureCached - { - (bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80)); + (bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80)); - if (isDefault) - PictureStorage.PictureCached += PictureStorage_PictureCached; + if (isDefault) + PictureStorage.PictureCached += PictureStorage_PictureCached; - // Mutable property. Set the field so PropertyChanged isn't fired. - _cover = ImageReader.ToImage(picture); - } + // Mutable property. Set the field so PropertyChanged isn't fired. + _cover = ImageReader.ToImage(picture); } private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e) @@ -94,10 +94,12 @@ namespace LibationWinForms.GridView } } + #endregion + #region Static library display functions /// - /// This information should not change during lifetime, so call only once. + /// This information should not change during lifetime, so call only once. /// protected static string GetDescriptionDisplay(Book book) { @@ -116,7 +118,7 @@ namespace LibationWinForms.GridView /// - /// This information should not change during lifetime, so call only once. + /// This information should not change during lifetime, so call only once. /// Maximum of 5 text rows will fit in 80-pixel row height. /// protected static string GetMiscDisplay(LibraryBook libraryBook) diff --git a/Source/LibationWinForms/GridView/ProductsDisplay.cs b/Source/LibationWinForms/GridView/ProductsDisplay.cs index 5f91a581..7eb55455 100644 --- a/Source/LibationWinForms/GridView/ProductsDisplay.cs +++ b/Source/LibationWinForms/GridView/ProductsDisplay.cs @@ -87,7 +87,7 @@ namespace LibationWinForms.GridView try { // don't return early if lib size == 0. this will not update correctly if all books are removed - var lib = DbContexts.GetLibrary_Flat_NoTracking(); + var lib = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true); if (!hasBeenDisplayed) { @@ -114,7 +114,7 @@ namespace LibationWinForms.GridView #endregion - internal List GetVisible() => productsGrid.GetVisible().Select(v => v.LibraryBook).ToList(); + internal List GetVisible() => productsGrid.GetVisibleBooks().ToList(); private void productsGrid_VisibleCountChanged(object sender, int count) { diff --git a/Source/LibationWinForms/GridView/ProductsGrid.cs b/Source/LibationWinForms/GridView/ProductsGrid.cs index b762a046..6305722f 100644 --- a/Source/LibationWinForms/GridView/ProductsGrid.cs +++ b/Source/LibationWinForms/GridView/ProductsGrid.cs @@ -25,9 +25,10 @@ namespace LibationWinForms.GridView public new event EventHandler Scroll; private GridEntryBindingList bindingList; - internal IEnumerable GetVisible() + internal IEnumerable GetVisibleBooks() => bindingList - .BookEntries(); + .BookEntries() + .Select(lbe => lbe.LibraryBook); public ProductsGrid() { diff --git a/Source/LibationWinForms/GridView/SeriesEntry.cs b/Source/LibationWinForms/GridView/SeriesEntry.cs index fd8411b8..e6882d77 100644 --- a/Source/LibationWinForms/GridView/SeriesEntry.cs +++ b/Source/LibationWinForms/GridView/SeriesEntry.cs @@ -14,52 +14,47 @@ namespace LibationWinForms.GridView public override string DisplayTags { get; } = string.Empty; public override LiberateButtonStatus Liberate { get; } - private SeriesEntry() + private SeriesEntry(LibraryBook parent) { Liberate = new LiberateButtonStatus { IsSeries = true }; SeriesIndex = -1; + LibraryBook = parent; + LoadCover(); } - public SeriesEntry(LibraryBook parent, IEnumerable children) : this() + public SeriesEntry(LibraryBook parent, IEnumerable children) : this(parent) { Children = children .Select(c => new LibraryBookEntry(c) { Parent = this }) .OrderBy(c => c.SeriesIndex) .ToList(); - UpdateSeries(parent); - LoadCover(); } - public SeriesEntry(LibraryBook parent, LibraryBook child) : this() + public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent) { Children = new() { new LibraryBookEntry(child) { Parent = this } }; - UpdateSeries(parent); - LoadCover(); } - public void UpdateSeries(LibraryBook libraryBook) + public void UpdateSeries(LibraryBook parent) { - LibraryBook = libraryBook; + LibraryBook = parent; - // Immutable properties - { - Title = Book.Title; - Series = Book.SeriesNames(); - MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); - PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d"); - ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); - Authors = Book.AuthorNames(); - Narrators = Book.NarratorNames(); - Category = string.Join(" > ", Book.CategoriesNames()); - Misc = GetMiscDisplay(libraryBook); - LongDescription = GetDescriptionDisplay(Book); - Description = TrimTextToWord(LongDescription, 62); + Title = Book.Title; + Series = Book.SeriesNames(); + MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); + PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d"); + ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); + Authors = Book.AuthorNames(); + Narrators = Book.NarratorNames(); + Category = string.Join(" > ", Book.CategoriesNames()); + Misc = GetMiscDisplay(LibraryBook); + LongDescription = GetDescriptionDisplay(Book); + Description = TrimTextToWord(LongDescription, 62); - int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes); - Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min"; - } + int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes); + Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min"; NotifyPropertyChanged(); } From cf1bc1c252729728cc7ed01d552deb332fe70c11 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 08:40:25 -0600 Subject: [PATCH 08/18] By defauly, only get actual books and not parents from DB --- Source/ApplicationServices/DbContexts.cs | 4 ++-- Source/DataLayer/QueryObjects/LibraryBookQueries.cs | 3 ++- Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/ApplicationServices/DbContexts.cs b/Source/ApplicationServices/DbContexts.cs index eb79a34d..d8f4b487 100644 --- a/Source/ApplicationServices/DbContexts.cs +++ b/Source/ApplicationServices/DbContexts.cs @@ -12,10 +12,10 @@ namespace ApplicationServices => LibationContext.Create(SqliteStorage.ConnectionString); /// Use for full library querying. No lazy loading - public static List GetLibrary_Flat_NoTracking() + public static List GetLibrary_Flat_NoTracking(bool includeParents = false) { using var context = GetContext(); - return context.GetLibrary_Flat_NoTracking(); + return context.GetLibrary_Flat_NoTracking(includeParents); } } } diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs index 41016e52..969aa702 100644 --- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs +++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs @@ -15,11 +15,12 @@ namespace DataLayer // .GetLibrary() // .ToList(); - public static List GetLibrary_Flat_NoTracking(this LibationContext context) + public static List GetLibrary_Flat_NoTracking(this LibationContext context, bool includeParents = false) => context .LibraryBooks .AsNoTrackingWithIdentityResolution() .GetLibrary() + .Where(lb => lb.Book.ContentType != ContentType.Parent || includeParents) .ToList(); public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId) diff --git a/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs b/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs index bd6f1fc0..84e5ff51 100644 --- a/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs +++ b/Source/LibationWinForms/Dialogs/RemoveBooksDialog.cs @@ -39,7 +39,6 @@ namespace LibationWinForms.Dialogs _dataGridView.BindingContextChanged += _dataGridView_BindingContextChanged; var orderedGridEntries = _libraryBooks - .Where(lb => lb.Book.ContentType is not ContentType.Parent) .Select(lb => new RemovableGridEntry(lb)) .OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate))) .ToList(); From 30ba69eca7ffe562249cf88b7b7cce291fb47e1c Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 08:52:25 -0600 Subject: [PATCH 09/18] Minor refactoring. --- Source/FileLiberator/Processable.cs | 3 +-- Source/LibationWinForms/GridView/ProductsGrid.cs | 14 +++++++------- .../LibationWinForms/GridView/QueryExtensions.cs | 3 +++ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Source/FileLiberator/Processable.cs b/Source/FileLiberator/Processable.cs index ef1bb139..b8b984ff 100644 --- a/Source/FileLiberator/Processable.cs +++ b/Source/FileLiberator/Processable.cs @@ -29,8 +29,7 @@ namespace FileLiberator public IEnumerable GetValidLibraryBooks(IEnumerable library) => library.Where(libraryBook => Validate(libraryBook) - && libraryBook.Book.ContentType != ContentType.Parent - && (libraryBook.Book.ContentType != ContentType.Episode || Configuration.Instance.DownloadEpisodes) + && (libraryBook.Book.ContentType != ContentType.Episode || LibationFileManager.Configuration.Instance.DownloadEpisodes) ); public async Task ProcessSingleAsync(LibraryBook libraryBook, bool validate) diff --git a/Source/LibationWinForms/GridView/ProductsGrid.cs b/Source/LibationWinForms/GridView/ProductsGrid.cs index 6305722f..ff058f92 100644 --- a/Source/LibationWinForms/GridView/ProductsGrid.cs +++ b/Source/LibationWinForms/GridView/ProductsGrid.cs @@ -93,12 +93,11 @@ namespace LibationWinForms.GridView { var geList = dbBooks.Where(lb => lb.IsProduct()).Select(b => new LibraryBookEntry(b)).Cast().ToList(); - var parents = dbBooks.Where(lb => lb.IsEpisodeParent()); var episodes = dbBooks.Where(lb => lb.IsEpisodeChild()); - foreach (var parent in parents) + foreach (var parent in dbBooks.Where(lb => lb.IsEpisodeParent())) { - var seriesEpisodes = episodes.Where(lb => lb.Book.SeriesLink?.Any(s => s.Series.AudibleSeriesId == parent.Book.AudibleProductId) == true).ToList(); + var seriesEpisodes = episodes.FindChildren(parent).ToList(); if (!seriesEpisodes.Any()) continue; @@ -112,7 +111,7 @@ namespace LibationWinForms.GridView bindingList.CollapseAll(); syncBindingSource.DataSource = bindingList; VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count()); - } + } internal void UpdateGrid(List dbBooks) { @@ -133,10 +132,10 @@ namespace LibationWinForms.GridView { var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId); - if (libraryBook.IsEpisodeChild()) - AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks); - else if (libraryBook.IsProduct()) + if (libraryBook.IsProduct()) AddOrUpdateBook(libraryBook, existingEntry); + else if(libraryBook.IsEpisodeChild()) + AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks); } bindingList.SuspendFilteringOnUpdate = false; @@ -198,6 +197,7 @@ namespace LibationWinForms.GridView if (seriesBook is null) { + //This should be impossible because the importer ensures every episode has a parent. var ex = new ApplicationException($"Episode's series parent not found in database."); var seriesLinks = string.Join("\r\n", episodeBook.Book.SeriesLink?.Select(sb => $"{nameof(sb.Series.Name)}={sb.Series.Name}, {nameof(sb.Series.AudibleSeriesId)}={sb.Series.AudibleSeriesId}")); Serilog.Log.Logger.Error(ex, "Episode={episodeBook}, Series: {seriesLinks}", episodeBook, seriesLinks); diff --git a/Source/LibationWinForms/GridView/QueryExtensions.cs b/Source/LibationWinForms/GridView/QueryExtensions.cs index e6df05cb..c6257014 100644 --- a/Source/LibationWinForms/GridView/QueryExtensions.cs +++ b/Source/LibationWinForms/GridView/QueryExtensions.cs @@ -8,6 +8,9 @@ namespace LibationWinForms.GridView #nullable enable internal static class QueryExtensions { + public static IEnumerable FindChildren(this IEnumerable bookList, LibraryBook parent) + => bookList.Where(lb => lb.Book.SeriesLink?.Any(s => s.Series.AudibleSeriesId == parent.Book.AudibleProductId) == true); + public static IEnumerable BookEntries(this IEnumerable gridEntries) => gridEntries.OfType(); From 31812bc2d9518196b0f1ba902a653ab2085d2484 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 09:24:06 -0600 Subject: [PATCH 10/18] Refactoring --- Source/LibationWinForms/GridView/GridEntry.cs | 25 ++++---- .../GridView/LibraryBookEntry.cs | 11 ++-- .../LibationWinForms/GridView/SeriesEntry.cs | 12 +++- .../ProcessQueue/ProcessQueueControl.cs | 59 ++++--------------- 4 files changed, 40 insertions(+), 67 deletions(-) diff --git a/Source/LibationWinForms/GridView/GridEntry.cs b/Source/LibationWinForms/GridView/GridEntry.cs index 69a23e10..8e5702e1 100644 --- a/Source/LibationWinForms/GridView/GridEntry.cs +++ b/Source/LibationWinForms/GridView/GridEntry.cs @@ -6,6 +6,7 @@ using LibationFileManager; using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Drawing; using System.Linq; @@ -14,11 +15,16 @@ namespace LibationWinForms.GridView /// The View Model base for the DataGridView public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable { - public string AudibleProductId => Book.AudibleProductId; - public LibraryBook LibraryBook { get; protected set; } - protected Book Book => LibraryBook.Book; + [Browsable(false)] public string AudibleProductId => Book.AudibleProductId; + [Browsable(false)] public LibraryBook LibraryBook { get; protected set; } + [Browsable(false)] public float SeriesIndex { get; protected set; } + [Browsable(false)] public string LongDescription { get; protected set; } + [Browsable(false)] public abstract DateTime DateAdded { get; } + [Browsable(false)] protected Book Book => LibraryBook.Book; #region Model properties exposed to the view + + public abstract LiberateButtonStatus Liberate { get; } public Image Cover { get => _cover; @@ -28,10 +34,7 @@ namespace LibationWinForms.GridView NotifyPropertyChanged(); } } - public float SeriesIndex { get; protected set; } - public string ProductRating { get; protected set; } public string PurchaseDate { get; protected set; } - public string MyRating { get; protected set; } public string Series { get; protected set; } public string Title { get; protected set; } public string Length { get; protected set; } @@ -40,10 +43,10 @@ namespace LibationWinForms.GridView public string Category { get; protected set; } public string Misc { get; protected set; } public string Description { get; protected set; } - public string LongDescription { get; protected set; } - public abstract DateTime DateAdded { get; } + public string ProductRating { get; protected set; } + public string MyRating { get; protected set; } public abstract string DisplayTags { get; } - public abstract LiberateButtonStatus Liberate { get; } + #endregion #region Sorting @@ -98,9 +101,7 @@ namespace LibationWinForms.GridView #region Static library display functions - /// - /// This information should not change during lifetime, so call only once. - /// + /// This information should not change during lifetime, so call only once. protected static string GetDescriptionDisplay(Book book) { var doc = new HtmlAgilityPack.HtmlDocument(); diff --git a/Source/LibationWinForms/GridView/LibraryBookEntry.cs b/Source/LibationWinForms/GridView/LibraryBookEntry.cs index cddde1f5..5cda59ea 100644 --- a/Source/LibationWinForms/GridView/LibraryBookEntry.cs +++ b/Source/LibationWinForms/GridView/LibraryBookEntry.cs @@ -3,6 +3,7 @@ using DataLayer; using Dinah.Core; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; namespace LibationWinForms.GridView @@ -10,15 +11,16 @@ namespace LibationWinForms.GridView /// The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode public class LibraryBookEntry : GridEntry { + + [Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded; + [Browsable(false)] public SeriesEntry Parent { get; init; } + #region Model properties exposed to the view private DateTime lastStatusUpdate = default; private LiberatedStatus _bookStatus; private LiberatedStatus? _pdfStatus; - public override DateTime DateAdded => LibraryBook.DateAdded; - public override string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated); - public override LiberateButtonStatus Liberate { get @@ -33,11 +35,10 @@ namespace LibationWinForms.GridView return new LiberateButtonStatus { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false }; } } + public override string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated); #endregion - public SeriesEntry Parent { get; init; } - public LibraryBookEntry(LibraryBook libraryBook) { setLibraryBook(libraryBook); diff --git a/Source/LibationWinForms/GridView/SeriesEntry.cs b/Source/LibationWinForms/GridView/SeriesEntry.cs index e6882d77..c22368cb 100644 --- a/Source/LibationWinForms/GridView/SeriesEntry.cs +++ b/Source/LibationWinForms/GridView/SeriesEntry.cs @@ -2,6 +2,7 @@ using Dinah.Core; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; namespace LibationWinForms.GridView @@ -9,10 +10,15 @@ namespace LibationWinForms.GridView /// The View Model for a LibraryBook that is ContentType.Parent public class SeriesEntry : GridEntry { - public List Children { get; } - public override DateTime DateAdded => Children.Max(c => c.DateAdded); - public override string DisplayTags { get; } = string.Empty; + [Browsable(false)] public List Children { get; } + [Browsable(false)] public override DateTime DateAdded => Children.Max(c => c.DateAdded); + + #region Model properties exposed to the view + public override LiberateButtonStatus Liberate { get; } + public override string DisplayTags { get; } = string.Empty; + + #endregion private SeriesEntry(LibraryBook parent) { diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs index 7da03da5..f698e29b 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs @@ -80,6 +80,15 @@ namespace LibationWinForms.ProcessQueue private bool isBookInQueue(DataLayer.LibraryBook libraryBook) => Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId); + public void AddDownloadPdf(DataLayer.LibraryBook libraryBook) + => AddDownloadPdf(new List() { libraryBook }); + + public void AddDownloadDecrypt(DataLayer.LibraryBook libraryBook) + => AddDownloadDecrypt(new List() { libraryBook }); + + public void AddConvertMp3(DataLayer.LibraryBook libraryBook) + => AddConvertMp3(new List() { libraryBook }); + public void AddDownloadPdf(IEnumerable entries) { List procs = new(); @@ -132,40 +141,6 @@ namespace LibationWinForms.ProcessQueue AddToQueue(procs); } - public void AddDownloadPdf(DataLayer.LibraryBook libraryBook) - { - if (isBookInQueue(libraryBook)) - return; - - ProcessBook pbook = new(libraryBook, Logger); - pbook.PropertyChanged += Pbook_DataAvailable; - pbook.AddDownloadPdf(); - AddToQueue(pbook); - } - - public void AddDownloadDecrypt(DataLayer.LibraryBook libraryBook) - { - if (isBookInQueue(libraryBook)) - return; - - ProcessBook pbook = new(libraryBook, Logger); - pbook.PropertyChanged += Pbook_DataAvailable; - pbook.AddDownloadDecryptBook(); - pbook.AddDownloadPdf(); - AddToQueue(pbook); - } - - public void AddConvertMp3(DataLayer.LibraryBook libraryBook) - { - if (isBookInQueue(libraryBook)) - return; - - ProcessBook pbook = new(libraryBook, Logger); - pbook.PropertyChanged += Pbook_DataAvailable; - pbook.AddConvertToMp3(); - AddToQueue(pbook); - } - private void AddToQueue(IEnumerable pbook) { syncContext.Post(_ => @@ -177,21 +152,11 @@ namespace LibationWinForms.ProcessQueue null); } - private void AddToQueue(ProcessBook pbook) - { - syncContext.Post(_ => - { - Queue.Enqueue(pbook); - if (!Running) - QueueRunner = QueueLoop(); - }, - null); - } - DateTime StartintTime; + DateTime StartingTime; private async Task QueueLoop() { - StartintTime = DateTime.Now; + StartingTime = DateTime.Now; counterTimer.Start(); while (Queue.MoveNext()) @@ -273,7 +238,7 @@ namespace LibationWinForms.ProcessQueue } if (Running) - runningTimeLbl.Text = timeToStr(DateTime.Now - StartintTime); + runningTimeLbl.Text = timeToStr(DateTime.Now - StartingTime); } private void clearLogBtn_Click(object sender, EventArgs e) From a476d5986df0d9d399487ccdea56b01c37f47854 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 09:44:06 -0600 Subject: [PATCH 11/18] Update dependency --- Source/AudibleUtilities/AudibleUtilities.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/AudibleUtilities/AudibleUtilities.csproj b/Source/AudibleUtilities/AudibleUtilities.csproj index b6f7774f..69ecdf6c 100644 --- a/Source/AudibleUtilities/AudibleUtilities.csproj +++ b/Source/AudibleUtilities/AudibleUtilities.csproj @@ -5,7 +5,7 @@ - + From 859a8e933ceff625fb041c66f3e9d7ce0a81c385 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 09:46:11 -0600 Subject: [PATCH 12/18] Formatting --- .../GridView/LibraryBookEntry.cs | 30 ++++++++----------- .../LibationWinForms/GridView/SeriesEntry.cs | 4 +++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Source/LibationWinForms/GridView/LibraryBookEntry.cs b/Source/LibationWinForms/GridView/LibraryBookEntry.cs index 5cda59ea..861b9587 100644 --- a/Source/LibationWinForms/GridView/LibraryBookEntry.cs +++ b/Source/LibationWinForms/GridView/LibraryBookEntry.cs @@ -11,7 +11,6 @@ namespace LibationWinForms.GridView /// The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode public class LibraryBookEntry : GridEntry { - [Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded; [Browsable(false)] public SeriesEntry Parent { get; init; } @@ -60,22 +59,19 @@ namespace LibationWinForms.GridView { LibraryBook = libraryBook; - // Immutable properties - { - Title = Book.Title; - Series = Book.SeriesNames(); - Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min"; - MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); - PurchaseDate = libraryBook.DateAdded.ToString("d"); - ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); - Authors = Book.AuthorNames(); - Narrators = Book.NarratorNames(); - Category = string.Join(" > ", Book.CategoriesNames()); - Misc = GetMiscDisplay(libraryBook); - LongDescription = GetDescriptionDisplay(Book); - Description = TrimTextToWord(LongDescription, 62); - SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0; - } + Title = Book.Title; + Series = Book.SeriesNames(); + Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min"; + MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); + PurchaseDate = libraryBook.DateAdded.ToString("d"); + ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); + Authors = Book.AuthorNames(); + Narrators = Book.NarratorNames(); + Category = string.Join(" > ", Book.CategoriesNames()); + Misc = GetMiscDisplay(libraryBook); + LongDescription = GetDescriptionDisplay(Book); + Description = TrimTextToWord(LongDescription, 62); + SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0; UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged; } diff --git a/Source/LibationWinForms/GridView/SeriesEntry.cs b/Source/LibationWinForms/GridView/SeriesEntry.cs index c22368cb..ca422029 100644 --- a/Source/LibationWinForms/GridView/SeriesEntry.cs +++ b/Source/LibationWinForms/GridView/SeriesEntry.cs @@ -65,6 +65,8 @@ namespace LibationWinForms.GridView NotifyPropertyChanged(); } + #region Data Sorting + /// Create getters for all member object values by name protected override Dictionary> CreateMemberValueDictionary() => new() { @@ -83,5 +85,7 @@ namespace LibationWinForms.GridView { nameof(Liberate), () => Liberate }, { nameof(DateAdded), () => DateAdded }, }; + + #endregion } } From 7d28681b235aa07ef5086b3b3e7a71b42f90434f Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 10:08:18 -0600 Subject: [PATCH 13/18] Move queries into DataLayer --- Source/DataLayer/QueryObjects/BookQueries.cs | 10 ++++++++ .../QueryObjects/LibraryBookQueries.cs | 18 +++++++++++++ .../LibationWinForms/GridView/ProductsGrid.cs | 12 ++++----- .../GridView/QueryExtensions.cs | 25 ------------------- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/Source/DataLayer/QueryObjects/BookQueries.cs b/Source/DataLayer/QueryObjects/BookQueries.cs index e54bb42f..efcbdc36 100644 --- a/Source/DataLayer/QueryObjects/BookQueries.cs +++ b/Source/DataLayer/QueryObjects/BookQueries.cs @@ -35,5 +35,15 @@ namespace DataLayer .Include(b => b.SeriesLink).ThenInclude(sb => sb.Series) .Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor) .Include(b => b.Category).ThenInclude(c => c.ParentCategory); + + public static bool IsProduct(this Book book) + => book.ContentType is not ContentType.Episode and not ContentType.Parent; + + public static bool IsEpisodeChild(this Book book) + => book.ContentType is ContentType.Episode; + + public static bool IsEpisodeParent(this Book book) + => book.ContentType is ContentType.Parent; + } } diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs index 969aa702..810488cc 100644 --- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs +++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs @@ -41,5 +41,23 @@ namespace DataLayer .Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series) .Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor) .Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory); + +#nullable enable + public static LibraryBook? FindSeriesParent(this IEnumerable libraryBooks, LibraryBook seriesEpisode) + { + if (seriesEpisode.Book.SeriesLink is null) return null; + + //Parent books will always have exactly 1 SeriesBook due to how + //they are imported in ApiExtended.getChildEpisodesAsync() + return libraryBooks.FirstOrDefault( + lb => + lb.Book.IsEpisodeParent() && + seriesEpisode.Book.SeriesLink.Any( + s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId)); + } +#nullable disable + + public static IEnumerable FindChildren(this IEnumerable bookList, LibraryBook parent) + => bookList.Where(lb => lb.Book.SeriesLink?.Any(s => s.Series.AudibleSeriesId == parent.Book.AudibleProductId) == true).ToList(); } } diff --git a/Source/LibationWinForms/GridView/ProductsGrid.cs b/Source/LibationWinForms/GridView/ProductsGrid.cs index ff058f92..c13a2d12 100644 --- a/Source/LibationWinForms/GridView/ProductsGrid.cs +++ b/Source/LibationWinForms/GridView/ProductsGrid.cs @@ -91,13 +91,13 @@ namespace LibationWinForms.GridView internal void BindToGrid(List dbBooks) { - var geList = dbBooks.Where(lb => lb.IsProduct()).Select(b => new LibraryBookEntry(b)).Cast().ToList(); + var geList = dbBooks.Where(lb => lb.Book.IsProduct()).Select(b => new LibraryBookEntry(b)).Cast().ToList(); - var episodes = dbBooks.Where(lb => lb.IsEpisodeChild()); + var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild()); - foreach (var parent in dbBooks.Where(lb => lb.IsEpisodeParent())) + foreach (var parent in dbBooks.Where(lb => lb.Book.IsEpisodeParent())) { - var seriesEpisodes = episodes.FindChildren(parent).ToList(); + var seriesEpisodes = episodes.FindChildren(parent); if (!seriesEpisodes.Any()) continue; @@ -132,9 +132,9 @@ namespace LibationWinForms.GridView { var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId); - if (libraryBook.IsProduct()) + if (libraryBook.Book.IsProduct()) AddOrUpdateBook(libraryBook, existingEntry); - else if(libraryBook.IsEpisodeChild()) + else if(libraryBook.Book.IsEpisodeChild()) AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks); } diff --git a/Source/LibationWinForms/GridView/QueryExtensions.cs b/Source/LibationWinForms/GridView/QueryExtensions.cs index c6257014..d084aec7 100644 --- a/Source/LibationWinForms/GridView/QueryExtensions.cs +++ b/Source/LibationWinForms/GridView/QueryExtensions.cs @@ -8,9 +8,6 @@ namespace LibationWinForms.GridView #nullable enable internal static class QueryExtensions { - public static IEnumerable FindChildren(this IEnumerable bookList, LibraryBook parent) - => bookList.Where(lb => lb.Book.SeriesLink?.Any(s => s.Series.AudibleSeriesId == parent.Book.AudibleProductId) == true); - public static IEnumerable BookEntries(this IEnumerable gridEntries) => gridEntries.OfType(); @@ -23,15 +20,6 @@ namespace LibationWinForms.GridView public static IEnumerable EmptySeries(this IEnumerable gridEntries) => gridEntries.SeriesEntries().Where(i => i.Children.Count == 0); - public static bool IsProduct(this LibraryBook lb) - => lb.Book.ContentType is not ContentType.Episode and not ContentType.Parent; - - public static bool IsEpisodeChild(this LibraryBook lb) - => lb.Book.ContentType is ContentType.Episode; - - public static bool IsEpisodeParent(this LibraryBook lb) - => lb.Book.ContentType is ContentType.Parent; - public static SeriesEntry? FindSeriesParent(this IEnumerable gridEntries, LibraryBook seriesEpisode) { if (seriesEpisode.Book.SeriesLink is null) return null; @@ -43,19 +31,6 @@ namespace LibationWinForms.GridView seriesEpisode.Book.SeriesLink.Any( s => s.Series.AudibleSeriesId == lb.LibraryBook.Book.SeriesLink.Single().Series.AudibleSeriesId)); } - - public static LibraryBook? FindSeriesParent(this IEnumerable libraryBooks, LibraryBook seriesEpisode) - { - if (seriesEpisode.Book.SeriesLink is null) return null; - - //Parent books will always have exactly 1 SeriesBook due to how - //they are imported in ApiExtended.getChildEpisodesAsync() - return libraryBooks.FirstOrDefault( - lb => - lb.IsEpisodeParent() && - seriesEpisode.Book.SeriesLink.Any( - s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId)); - } } #nullable disable } From 6cb98f99c57e902bbadffba7552bd989d3eb569f Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 10:34:05 -0600 Subject: [PATCH 14/18] Use new content type queries --- Source/DataLayer/QueryObjects/LibraryBookQueries.cs | 3 ++- Source/FileLiberator/Processable.cs | 2 +- Source/LibationSearchEngine/SearchEngine.cs | 12 ++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs index 810488cc..18ddce76 100644 --- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs +++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs @@ -20,7 +20,8 @@ namespace DataLayer .LibraryBooks .AsNoTrackingWithIdentityResolution() .GetLibrary() - .Where(lb => lb.Book.ContentType != ContentType.Parent || includeParents) + .AsEnumerable() + .Where(lb => !lb.Book.IsEpisodeParent() || includeParents) .ToList(); public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId) diff --git a/Source/FileLiberator/Processable.cs b/Source/FileLiberator/Processable.cs index b8b984ff..f56c67f1 100644 --- a/Source/FileLiberator/Processable.cs +++ b/Source/FileLiberator/Processable.cs @@ -29,7 +29,7 @@ namespace FileLiberator public IEnumerable GetValidLibraryBooks(IEnumerable library) => library.Where(libraryBook => Validate(libraryBook) - && (libraryBook.Book.ContentType != ContentType.Episode || LibationFileManager.Configuration.Instance.DownloadEpisodes) + && (!libraryBook.Book.IsEpisodeChild() || Configuration.Instance.DownloadEpisodes) ); public async Task ProcessSingleAsync(LibraryBook libraryBook, bool validate) diff --git a/Source/LibationSearchEngine/SearchEngine.cs b/Source/LibationSearchEngine/SearchEngine.cs index a3c86867..e3e733d2 100644 --- a/Source/LibationSearchEngine/SearchEngine.cs +++ b/Source/LibationSearchEngine/SearchEngine.cs @@ -121,12 +121,12 @@ namespace LibationSearchEngine ["Liberated"] = lb => isLiberated(lb.Book), ["LiberatedError"] = lb => liberatedError(lb.Book), - ["Podcast"] = lb => lb.Book.ContentType == ContentType.Episode, - ["Podcasts"] = lb => lb.Book.ContentType == ContentType.Episode, - ["IsPodcast"] = lb => lb.Book.ContentType == ContentType.Episode, - ["Episode"] = lb => lb.Book.ContentType == ContentType.Episode, - ["Episodes"] = lb => lb.Book.ContentType == ContentType.Episode, - ["IsEpisode"] = lb => lb.Book.ContentType == ContentType.Episode, + ["Podcast"] = lb => lb.Book.IsEpisodeChild(), + ["Podcasts"] = lb => lb.Book.IsEpisodeChild(), + ["IsPodcast"] = lb => lb.Book.IsEpisodeChild(), + ["Episode"] = lb => lb.Book.IsEpisodeChild(), + ["Episodes"] = lb => lb.Book.IsEpisodeChild(), + ["IsEpisode"] = lb => lb.Book.IsEpisodeChild(), } ); From 074d647d1901f39ea6c86b72bdd35f5a97811609 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 10:36:06 -0600 Subject: [PATCH 15/18] Improve Query --- Source/DataLayer/QueryObjects/LibraryBookQueries.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs index 18ddce76..9c60f3fb 100644 --- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs +++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs @@ -59,6 +59,15 @@ namespace DataLayer #nullable disable public static IEnumerable FindChildren(this IEnumerable bookList, LibraryBook parent) - => bookList.Where(lb => lb.Book.SeriesLink?.Any(s => s.Series.AudibleSeriesId == parent.Book.AudibleProductId) == true).ToList(); + => bookList + .Where( + lb => + lb.Book.IsEpisodeChild() && + lb.Book.SeriesLink? + .Any( + s => + s.Series.AudibleSeriesId == parent.Book.AudibleProductId + ) == true + ).ToList(); } } From 5a093a9a046f3171f502e76c9a22c086a4d65c18 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 10:53:45 -0600 Subject: [PATCH 16/18] add event keyword --- Source/ApplicationServices/SearchEngineCommands.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ApplicationServices/SearchEngineCommands.cs b/Source/ApplicationServices/SearchEngineCommands.cs index 7a0e2219..efc21c7f 100644 --- a/Source/ApplicationServices/SearchEngineCommands.cs +++ b/Source/ApplicationServices/SearchEngineCommands.cs @@ -29,7 +29,7 @@ namespace ApplicationServices } #endregion - public static EventHandler SearchEngineUpdated; + public static event EventHandler SearchEngineUpdated; #region Update private static bool isUpdating; From 508e03114312f2345f118ae5d458aa9be76cbe02 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 12:08:15 -0600 Subject: [PATCH 17/18] Move all event invocations outside locks --- .../ProcessQueue/TrackedQueue[T].cs | 85 +++++++++++-------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs b/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs index 43c8ce98..53710fb9 100644 --- a/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs +++ b/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs @@ -85,13 +85,18 @@ namespace LibationWinForms.ProcessQueue public bool RemoveQueued(T item) { + bool itemsRemoved; + int queuedCount; + lock (lockObject) { - bool removed = _queued.Remove(item); - if (removed) - QueuededCountChanged?.Invoke(this, _queued.Count); - return removed; + itemsRemoved = _queued.Remove(item); + queuedCount = _queued.Count; } + + if (itemsRemoved) + QueuededCountChanged?.Invoke(this, queuedCount); + return itemsRemoved; } public void ClearCurrent() @@ -102,31 +107,32 @@ namespace LibationWinForms.ProcessQueue public bool RemoveCompleted(T item) { + bool itemsRemoved; + int completedCount; + lock (lockObject) { - bool removed = _completed.Remove(item); - if (removed) - CompletedCountChanged?.Invoke(this, _completed.Count); - return removed; + itemsRemoved = _completed.Remove(item); + completedCount = _completed.Count; } + + if (itemsRemoved) + CompletedCountChanged?.Invoke(this, completedCount); + return itemsRemoved; } public void ClearQueue() { lock (lockObject) - { _queued.Clear(); - QueuededCountChanged?.Invoke(this, 0); - } + QueuededCountChanged?.Invoke(this, 0); } public void ClearCompleted() { lock (lockObject) - { _completed.Clear(); - CompletedCountChanged?.Invoke(this, 0); - } + CompletedCountChanged?.Invoke(this, 0); } public bool Any(Func predicate) @@ -175,23 +181,35 @@ namespace LibationWinForms.ProcessQueue public bool MoveNext() { - lock (lockObject) + int queuedCount, completedCount = 0; + bool completedChanged = false; + try { - if (Current != null) + lock (lockObject) { - _completed.Add(Current); - CompletedCountChanged?.Invoke(this, _completed.Count); - } - if (_queued.Count == 0) - { - Current = null; - return false; - } - Current = _queued[0]; - _queued.RemoveAt(0); + if (Current != null) + { + _completed.Add(Current); + completedCount = _completed.Count; + completedChanged = true; + } + if (_queued.Count == 0) + { + Current = null; + return false; + } + Current = _queued[0]; + _queued.RemoveAt(0); + queuedCount = _queued.Count; + return true; + } + } + finally + { + if (completedChanged) + CompletedCountChanged?.Invoke(this, _completed.Count); QueuededCountChanged?.Invoke(this, _queued.Count); - return true; } } @@ -218,22 +236,15 @@ namespace LibationWinForms.ProcessQueue } } - public void Enqueue(T item) - { - lock (lockObject) - { - _queued.Add(item); - QueuededCountChanged?.Invoke(this, _queued.Count); - } - } - public void Enqueue(IEnumerable item) { + int queueCount; lock (lockObject) { _queued.AddRange(item); - QueuededCountChanged?.Invoke(this, _queued.Count); + queueCount = _queued.Count; } + QueuededCountChanged?.Invoke(this, queueCount); } } } From cc1d2b423f69d77d9eb5a1ad2c8535164bc75a16 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 8 Jun 2022 12:15:21 -0600 Subject: [PATCH 18/18] Fix an oopsie --- Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs b/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs index 53710fb9..80991f34 100644 --- a/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs +++ b/Source/LibationWinForms/ProcessQueue/TrackedQueue[T].cs @@ -181,7 +181,7 @@ namespace LibationWinForms.ProcessQueue public bool MoveNext() { - int queuedCount, completedCount = 0; + int completedCount = 0, queuedCount = 0; bool completedChanged = false; try { @@ -208,8 +208,8 @@ namespace LibationWinForms.ProcessQueue finally { if (completedChanged) - CompletedCountChanged?.Invoke(this, _completed.Count); - QueuededCountChanged?.Invoke(this, _queued.Count); + CompletedCountChanged?.Invoke(this, completedCount); + QueuededCountChanged?.Invoke(this, queuedCount); } }