From fcd79c55610b0a28fa450f23cf3186fca85056f3 Mon Sep 17 00:00:00 2001 From: delebash Date: Mon, 20 Oct 2025 12:55:48 -0400 Subject: [PATCH] InludedUntil fixes by Mbucari --- Source/ApplicationServices/LibraryCommands.cs | 2 +- Source/DataLayer/EfClasses/LibraryBook.cs | 11 +- .../DtoImporterService/LibraryBookImporter.cs | 224 +++++++++--------- .../Controls/ThemePreviewControl.axaml.cs | 12 +- .../Views/ProductsDisplay.axaml | 4 +- .../Configuration.PersistentSettings.cs | 3 +- Source/LibationUiBase/GridView/GridEntry.cs | 2 +- 7 files changed, 127 insertions(+), 131 deletions(-) diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index 0647f79a..f2e2a7cc 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -211,7 +211,7 @@ namespace ApplicationServices if (book is null) { - book = new LibraryBook(bookImporter.Cache[importItem.DtoItem.ProductId], importItem.DtoItem.DateAdded, importItem.AccountId,DateTime.Now); + book = new LibraryBook(bookImporter.Cache[importItem.DtoItem.ProductId], importItem.DtoItem.DateAdded, importItem.AccountId); context.LibraryBooks.Add(book); } else diff --git a/Source/DataLayer/EfClasses/LibraryBook.cs b/Source/DataLayer/EfClasses/LibraryBook.cs index ad650ab5..9c57fe0a 100644 --- a/Source/DataLayer/EfClasses/LibraryBook.cs +++ b/Source/DataLayer/EfClasses/LibraryBook.cs @@ -16,7 +16,7 @@ namespace DataLayer public DateTime? IncludedUntil { get; private set; } private LibraryBook() { } - public LibraryBook(Book book, DateTime dateAdded, string account, DateTime? includedUntil) + public LibraryBook(Book book, DateTime dateAdded, string account) { ArgumentValidator.EnsureNotNull(book, nameof(book)); ArgumentValidator.EnsureNotNull(account, nameof(account)); @@ -24,11 +24,10 @@ namespace DataLayer Book = book; DateAdded = dateAdded; Account = account; - IncludedUntil = includedUntil; } public void SetAccount(string account) => Account = account; - - public override string ToString() => $"{DateAdded:d} {Book}"; - } -} + public void SetIncludedUntil(DateTime? includedUntil) => IncludedUntil = includedUntil; + public override string ToString() => $"{DateAdded:d} {Book}"; + } +} \ No newline at end of file diff --git a/Source/DtoImporterService/LibraryBookImporter.cs b/Source/DtoImporterService/LibraryBookImporter.cs index 8b694b7b..689a1cc7 100644 --- a/Source/DtoImporterService/LibraryBookImporter.cs +++ b/Source/DtoImporterService/LibraryBookImporter.cs @@ -8,140 +8,136 @@ using Dinah.Core.Collections.Generic; namespace DtoImporterService { - public class LibraryBookImporter : ItemsImporterBase - { - protected override IValidator Validator => new LibraryValidator(); + public class LibraryBookImporter : ItemsImporterBase + { + protected override IValidator Validator => new LibraryValidator(); - private BookImporter bookImporter { get; } + private BookImporter bookImporter { get; } - public LibraryBookImporter(LibationContext context) : base(context) - { - bookImporter = new BookImporter(DbContext); - } + public LibraryBookImporter(LibationContext context) : base(context) + { + bookImporter = new BookImporter(DbContext); + } - protected override int DoImport(IEnumerable importItems) - { - bookImporter.Import(importItems); + protected override int DoImport(IEnumerable importItems) + { + bookImporter.Import(importItems); - var qtyNew = upsertLibraryBooks(importItems); - return qtyNew; - } + var qtyNew = upsertLibraryBooks(importItems); + return qtyNew; + } - private int upsertLibraryBooks(IEnumerable importItems) - { - // technically, we should be able to have duplicate books from separate accounts. - // this would violate the current pk and would be difficult to deal with elsewhere: - // - what to show in the grid - // - which to consider liberated - // - // sqlite cannot alter pk. the work around is an extensive headache - // - update: now possible in .net5/efcore5 - // - // currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region. - // - // CURRENT SOLUTION: don't re-insert + private int upsertLibraryBooks(IEnumerable importItems) + { + // technically, we should be able to have duplicate books from separate accounts. + // this would violate the current pk and would be difficult to deal with elsewhere: + // - what to show in the grid + // - which to consider liberated + // + // sqlite cannot alter pk. the work around is an extensive headache + // - update: now possible in .net5/efcore5 + // + // currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region. + // + // CURRENT SOLUTION: don't re-insert - //When Books are upserted during the BookImporter run, they are linked to their LibraryBook in the DbContext - //instance. If a LibraryBook has a null book here, that means it's Book was not imported during by BookImporter. - //There should never be duplicates, but this is defensive. - var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionarySafe(l => l.Book.AudibleProductId); + //When Books are upserted during the BookImporter run, they are linked to their LibraryBook in the DbContext + //instance. If a LibraryBook has a null book here, that means it's Book was not imported during by BookImporter. + //There should never be duplicates, but this is defensive. + var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionarySafe(l => l.Book.AudibleProductId); - //If importItems are contains duplicates by asin, keep the Item that's "available" - var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak); + //If importItems are contains duplicates by asin, keep the Item that's "available" + var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak); - int qtyNew = 0; - - - + int qtyNew = 0; - foreach (var item in uniqueImportItems.Values) - { - if (existingEntries.TryGetValue(item.DtoItem.ProductId, out LibraryBook existing)) - { - if (existing.Account != item.AccountId) - { - //Book is absent from the existing LibraryBook's account. Use the alternate account. - existing.SetAccount(item.AccountId); - } + foreach (var item in uniqueImportItems.Values) + { + if (existingEntries.TryGetValue(item.DtoItem.ProductId, out LibraryBook existing)) + { + if (existing.Account != item.AccountId) + { + //Book is absent from the existing LibraryBook's account. Use the alternate account. + existing.SetAccount(item.AccountId); + } - existing.AbsentFromLastScan = isUnavailable(item); - } - else - { - - //Used to determine when your audible plus or free book will expire from your library - //plan.IsAyce from underlying AudibleApi project determines the plans to look at, first plan found is used. - DateTime? includedUntil = null; - if (item.DtoItem.Plans is not null) - { - foreach (var plan in item.DtoItem.Plans) - { - if (plan.IsAyce && plan.EndDate.Value.Year != 2099 && plan.EndDate.Value.Year != 9999 && plan.EndDate.HasValue) - { - includedUntil = plan.EndDate.Value.LocalDateTime; - } - } - } - - var libraryBook = new LibraryBook( - bookImporter.Cache[item.DtoItem.ProductId], - item.DtoItem.DateAdded, - item.AccountId, - includedUntil) - { - AbsentFromLastScan = isUnavailable(item) - }; + existing.AbsentFromLastScan = isUnavailable(item); + } + else + { + existing = new LibraryBook( + bookImporter.Cache[item.DtoItem.ProductId], + item.DtoItem.DateAdded, + item.AccountId) + { + AbsentFromLastScan = isUnavailable(item) + }; - try - { - DbContext.LibraryBooks.Add(libraryBook); - qtyNew++; - } - catch (Exception ex) - { - Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { libraryBook.Book, libraryBook.Account }); - } - } - } + try + { + DbContext.LibraryBooks.Add(existing); + qtyNew++; + } + catch (Exception ex) + { + Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { existing.Book, existing.Account }); + } + } + + existing.SetIncludedUntil(GetExpirationDate(item)); + } - var scannedAccounts = importItems.Select(i => i.AccountId).Distinct().ToList(); + var scannedAccounts = importItems.Select(i => i.AccountId).Distinct().ToList(); - //If an existing Book wasn't found in the import, the owning LibraryBook's Book will be null. - //Only change AbsentFromLastScan for LibraryBooks of accounts that were scanned. - foreach (var nullBook in DbContext.LibraryBooks.AsEnumerable().Where(lb => lb.Book is null && lb.Account.In(scannedAccounts))) - nullBook.AbsentFromLastScan = true; + //If an existing Book wasn't found in the import, the owning LibraryBook's Book will be null. + //Only change AbsentFromLastScan for LibraryBooks of accounts that were scanned. + foreach (var nullBook in DbContext.LibraryBooks.AsEnumerable().Where(lb => lb.Book is null && lb.Account.In(scannedAccounts))) + nullBook.AbsentFromLastScan = true; - return qtyNew; - } + return qtyNew; + } - private static Dictionary ToDictionarySafe(IEnumerable source, Func keySelector, Func tieBreaker) - { - var dictionary = new Dictionary(); + private static Dictionary ToDictionarySafe(IEnumerable source, Func keySelector, Func tieBreaker) + { + var dictionary = new Dictionary(); - foreach (TSource newItem in source) - { - TKey key = keySelector(newItem); + foreach (TSource newItem in source) + { + TKey key = keySelector(newItem); - dictionary[key] - = dictionary.TryGetValue(key, out TSource existingItem) - ? tieBreaker(existingItem, newItem) - : newItem; - } - return dictionary; - } + dictionary[key] + = dictionary.TryGetValue(key, out TSource existingItem) + ? tieBreaker(existingItem, newItem) + : newItem; + } + return dictionary; + } - private static ImportItem tieBreak(ImportItem item1, ImportItem item2) - => isUnavailable(item1) && !isUnavailable(item2) ? item2 : item1; + private static ImportItem tieBreak(ImportItem item1, ImportItem item2) + => isUnavailable(item1) && !isUnavailable(item2) ? item2 : item1; - private static bool isUnavailable(ImportItem item) - => isFutureRelease(item) || isPlusTitleUnavailable(item); + private static bool isUnavailable(ImportItem item) + => isFutureRelease(item) || isPlusTitleUnavailable(item); private static bool isFutureRelease(ImportItem item) - => item.DtoItem.IssueDate is DateTimeOffset dt && dt > DateTimeOffset.UtcNow; + => item.DtoItem.IssueDate is DateTimeOffset dt && dt > DateTimeOffset.UtcNow; - private static bool isPlusTitleUnavailable(ImportItem item) - => item.DtoItem.ContentType is null - || (item.DtoItem.IsAyce is true && item.DtoItem.Plans?.Any(p => p.IsAyce) is not true); - } -} + private static bool isPlusTitleUnavailable(ImportItem item) + => item.DtoItem.ContentType is null + || (item.DtoItem.IsAyce is true && item.DtoItem.Plans?.Any(p => p.IsAyce) is not true); + + /// + /// Determines when your audible plus or free book will expire from your library + /// plan.IsAyce from underlying AudibleApi project determines the plans to look at, first plan found is used. + /// In some cases current date is later than end date so exclude. + /// + /// The DateTime that this title will become unavailable, otherwise null + private static DateTime? GetExpirationDate(ImportItem item) + => item.DtoItem.Plans + ?.Where(p => p.IsAyce) + .Select(p => p.EndDate) + .FirstOrDefault(end => end.HasValue && end.Value.Year is not (2099 or 9999) && end.Value.LocalDateTime >= DateTime.Now) + ?.DateTime; + } +} \ No newline at end of file diff --git a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs index bd28249b..029b46f3 100644 --- a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs +++ b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs @@ -59,12 +59,12 @@ public partial class ThemePreviewControl : UserControl var author = new Contributor("Some Author", "asin_contributor"); var narrator = new Contributor("Some Narrator", "asin_narrator"); - var book1 = new Book(new AudibleProductId("asin_book1"), "Some Book 1", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us", null); - var book2 = new Book(new AudibleProductId("asin_book2"), "Some Book 2", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us", null); - var book3 = new Book(new AudibleProductId("asin_book3"), "Some Book 3", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us", null); - var book4 = new Book(new AudibleProductId("asin_book4"), "Some Book 4", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us", null); - var seriesParent = new Book(new AudibleProductId("asin_series"), "Some Series", "", "Demo Series Entry", 0, ContentType.Parent, [author], [narrator], "us",null); - var episode = new Book(new AudibleProductId("asin_episode"), "Some Episode", "Episode 1", "Demo Episode Entry", 56, ContentType.Episode, [author], [narrator], "us",null); + var book1 = new Book(new AudibleProductId("asin_book1"), "Some Book 1", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us"); + var book2 = new Book(new AudibleProductId("asin_book2"), "Some Book 2", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us"); + var book3 = new Book(new AudibleProductId("asin_book3"), "Some Book 3", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us"); + var book4 = new Book(new AudibleProductId("asin_book4"), "Some Book 4", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us"); + var seriesParent = new Book(new AudibleProductId("asin_series"), "Some Series", "", "Demo Series Entry", 0, ContentType.Parent, [author], [narrator], "us"); + var episode = new Book(new AudibleProductId("asin_episode"), "Some Episode", "Episode 1", "Demo Episode Entry", 56, ContentType.Episode, [author], [narrator], "us"); var series = new Series(new AudibleSeriesId(seriesParent.AudibleProductId), seriesParent.Title); diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml b/Source/LibationAvalonia/Views/ProductsDisplay.axaml index 17cc6516..287d2da0 100644 --- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml +++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml @@ -213,11 +213,11 @@ - + - + diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs index 392c5ecf..e5f56e99 100644 --- a/Source/LibationFileManager/Configuration.PersistentSettings.cs +++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs @@ -209,7 +209,8 @@ namespace LibationFileManager private static readonly EquatableDictionary DefaultColumns = new([ new ("SeriesOrder", false), new ("LastDownload", false), - new ("IsSpatial", false) + new ("IsSpatial", false), + new ("IncludedUntil", false), ]); public bool GetColumnVisibility(string columnName) => GridColumnsVisibilities.TryGetValue(columnName, out var isVisible) ? isVisible diff --git a/Source/LibationUiBase/GridView/GridEntry.cs b/Source/LibationUiBase/GridView/GridEntry.cs index 46aec3cc..9c55b809 100644 --- a/Source/LibationUiBase/GridView/GridEntry.cs +++ b/Source/LibationUiBase/GridView/GridEntry.cs @@ -158,7 +158,7 @@ namespace LibationUiBase.GridView { //If UserDefinedItem was changed on a different Book instance (such as when batch liberating via menus), //Liberate.Book and LibraryBook.Book instances will not have the current DB state. - Invoke(() => UpdateLibraryBook(new LibraryBook(udi.Book, LibraryBook.DateAdded, LibraryBook.Account,LibraryBook.IncludedUntil))); + Invoke(() => UpdateLibraryBook(new LibraryBook(udi.Book, LibraryBook.DateAdded, LibraryBook.Account))); return; }