From fa8f7617719f13188d4574cf3f413cbf080e7d72 Mon Sep 17 00:00:00 2001 From: delebash Date: Fri, 17 Oct 2025 13:36:01 -0400 Subject: [PATCH 1/7] Added IncludedUntil Date --- Source/DataLayer/EfClasses/Book.cs | 9 +++++++- Source/DtoImporterService/BookImporter.cs | 21 +++++++++++++++++-- .../Controls/ThemePreviewControl.axaml.cs | 13 ++++++------ .../ViewModels/ProductsDisplayViewModel.cs | 1 + .../Views/ProductsDisplay.axaml | 10 +++++++++ Source/LibationUiBase/GridView/GridEntry.cs | 6 ++++++ .../GridView/ProductsGrid.Designer.cs | 14 ++++++++++++- 7 files changed, 64 insertions(+), 10 deletions(-) diff --git a/Source/DataLayer/EfClasses/Book.cs b/Source/DataLayer/EfClasses/Book.cs index 8de17cb8..d11fb75b 100644 --- a/Source/DataLayer/EfClasses/Book.cs +++ b/Source/DataLayer/EfClasses/Book.cs @@ -42,6 +42,7 @@ namespace DataLayer public int LengthInMinutes { get; private set; } public ContentType ContentType { get; private set; } public string Locale { get; private set; } + public DateTime? IncludedUntil { get; private set; } // mutable public string PictureId { get; set; } @@ -73,7 +74,9 @@ namespace DataLayer ContentType contentType, IEnumerable authors, IEnumerable narrators, - string localeName) + string localeName, + DateTime? includedUntil + ) { // validate ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId)); @@ -97,6 +100,7 @@ namespace DataLayer UpdateTitle(title, subtitle); Description = description?.Trim() ?? ""; LengthInMinutes = lengthInMinutes; + IncludedUntil = includedUntil; ContentType = contentType; // assigns with biz logic @@ -113,6 +117,9 @@ namespace DataLayer public void UpdateLengthInMinutes(int lengthInMinutes) => LengthInMinutes = lengthInMinutes; + + public void UpdateIncludedUntil(DateTime? includedUntil) + => IncludedUntil = includedUntil; #region contributors, authors, narrators internal HashSet ContributorsLink { get; private set; } diff --git a/Source/DtoImporterService/BookImporter.cs b/Source/DtoImporterService/BookImporter.cs index e6596d09..b310b8aa 100644 --- a/Source/DtoImporterService/BookImporter.cs +++ b/Source/DtoImporterService/BookImporter.cs @@ -98,6 +98,20 @@ namespace DtoImporterService .DistinctBy(a => a.Name) .Select(n => contributorImporter.Cache[n.Name]) .ToList(); + + //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.Plans is not null) + { + foreach (var plan in item.Plans) + { + if (plan.IsAyce && plan.EndDate.Value.Year != 2099 && plan.EndDate.HasValue) + { + includedUntil = plan.EndDate.Value.LocalDateTime; + } + } + } Book book; try @@ -111,7 +125,9 @@ namespace DtoImporterService contentType, authors, narrators, - importItem.LocaleName) + importItem.LocaleName, + includedUntil + ) ).Entity; Cache.Add(book.AudibleProductId, book); } @@ -125,7 +141,8 @@ namespace DtoImporterService contentType, QtyAuthors = authors?.Count, QtyNarrators = narrators?.Count, - importItem.LocaleName + importItem.LocaleName, + includedUntil }); throw; } diff --git a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs index ee35a855..bd28249b 100644 --- a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs +++ b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs @@ -4,6 +4,7 @@ using Dinah.Core.ErrorHandling; using LibationAvalonia.ViewModels; using LibationFileManager; using LibationUiBase.ProcessQueue; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -58,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"); - 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 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 series = new Series(new AudibleSeriesId(seriesParent.AudibleProductId), seriesParent.Title); diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs index f51a3044..473a8592 100644 --- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs @@ -477,6 +477,7 @@ namespace LibationAvalonia.ViewModels public DataGridLength PurchaseDateWidth { get => getColumnWidth("PurchaseDate", 75); set => setColumnWidth("PurchaseDate", value); } public DataGridLength MyRatingWidth { get => getColumnWidth("MyRating", 95); set => setColumnWidth("MyRating", value); } public DataGridLength MiscWidth { get => getColumnWidth("Misc", 140); set => setColumnWidth("Misc", value); } + public DataGridLength IncludedUntilWidth { get => getColumnWidth("IncludedUntil", 140); set => setColumnWidth("IncludedUntil", value); } public DataGridLength LastDownloadWidth { get => getColumnWidth("LastDownload", 100); set => setColumnWidth("LastDownload", value); } public DataGridLength BookTagsWidth { get => getColumnWidth("BookTags", 100); set => setColumnWidth("BookTags", value); } public DataGridLength IsSpatialWidth { get => getColumnWidth("IsSpatial", 100); set => setColumnWidth("IsSpatial", value); } diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml b/Source/LibationAvalonia/Views/ProductsDisplay.axaml index 8f2ec9c2..17cc6516 100644 --- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml +++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml @@ -213,6 +213,16 @@ + + + + + + + + + + diff --git a/Source/LibationUiBase/GridView/GridEntry.cs b/Source/LibationUiBase/GridView/GridEntry.cs index 665859f1..3adf66f2 100644 --- a/Source/LibationUiBase/GridView/GridEntry.cs +++ b/Source/LibationUiBase/GridView/GridEntry.cs @@ -49,6 +49,7 @@ namespace LibationUiBase.GridView private string _bookTags; private Rating _myRating; private bool _isSpatial; + private string _includedUntil; public abstract bool? Remove { get; set; } public EntryStatus Liberate { get => _liberate; private set => RaiseAndSetIfChanged(ref _liberate, value); } public string PurchaseDate { get => _purchasedate; protected set => RaiseAndSetIfChanged(ref _purchasedate, value); } @@ -66,6 +67,7 @@ namespace LibationUiBase.GridView public Rating ProductRating { get => _productrating; private set => RaiseAndSetIfChanged(ref _productrating, value); } public string BookTags { get => _bookTags; private set => RaiseAndSetIfChanged(ref _bookTags, value); } public bool IsSpatial { get => _isSpatial; protected set => RaiseAndSetIfChanged(ref _isSpatial, value); } + public string IncludedUntil { get => _includedUntil; protected set => RaiseAndSetIfChanged(ref _includedUntil, value); } public Rating MyRating { @@ -120,6 +122,8 @@ namespace LibationUiBase.GridView SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0; BookTags = GetBookTags(); IsSpatial = Book.IsSpatial; + IncludedUntil = GetIncludedUntilString(); + UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged; } @@ -128,6 +132,7 @@ namespace LibationUiBase.GridView protected virtual DateTime GetPurchaseDate() => LibraryBook.DateAdded; protected virtual int GetLengthInMinutes() => Book.LengthInMinutes; protected string GetPurchaseDateString() => GetPurchaseDate().ToString("d"); + protected string GetIncludedUntilString() => Book.IncludedUntil?.ToString("d") ?? string.Empty; protected string GetBookLengthString() { int bookLenMins = GetLengthInMinutes(); @@ -208,6 +213,7 @@ namespace LibationUiBase.GridView nameof(Liberate) => Liberate, nameof(DateAdded) => DateAdded, nameof(IsSpatial) => IsSpatial, + nameof(IncludedUntil) => IncludedUntil, _ => null }; diff --git a/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs b/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs index 3f004673..21874afa 100644 --- a/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs +++ b/Source/LibationWinForms/GridView/ProductsGrid.Designer.cs @@ -54,6 +54,7 @@ namespace LibationWinForms.GridView lastDownloadedGVColumn = new LastDownloadedGridViewColumn(); isSpatialGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn(); tagAndDetailsGVColumn = new EditTagsDataGridViewImageButtonColumn(); + includedUntilGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); ((System.ComponentModel.ISupportInitialize)gridEntryDataGridView).BeginInit(); ((System.ComponentModel.ISupportInitialize)syncBindingSource).BeginInit(); SuspendLayout(); @@ -66,7 +67,7 @@ namespace LibationWinForms.GridView gridEntryDataGridView.AllowUserToResizeRows = false; gridEntryDataGridView.AutoGenerateColumns = false; gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; - gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { removeGVColumn, liberateGVColumn, coverGVColumn, titleGVColumn, authorsGVColumn, narratorsGVColumn, lengthGVColumn, seriesGVColumn, seriesOrderGVColumn, descriptionGVColumn, categoryGVColumn, productRatingGVColumn, purchaseDateGVColumn, myRatingGVColumn, miscGVColumn, lastDownloadedGVColumn, isSpatialGVColumn, tagAndDetailsGVColumn }); + gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { removeGVColumn, liberateGVColumn, coverGVColumn, titleGVColumn, authorsGVColumn, narratorsGVColumn, lengthGVColumn, seriesGVColumn, seriesOrderGVColumn, descriptionGVColumn, categoryGVColumn, productRatingGVColumn, purchaseDateGVColumn, myRatingGVColumn, miscGVColumn, lastDownloadedGVColumn, isSpatialGVColumn, tagAndDetailsGVColumn, includedUntilGVColumn }); gridEntryDataGridView.ContextMenuStrip = showHideColumnsContextMenuStrip; gridEntryDataGridView.DataSource = syncBindingSource; dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; @@ -276,6 +277,16 @@ namespace LibationWinForms.GridView tagAndDetailsGVColumn.ScaleFactor = 0F; tagAndDetailsGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic; // + // includedUntilGVColumn + // + includedUntilGVColumn.DataPropertyName = "IncludedUntil"; + includedUntilGVColumn.HeaderText = "Included Until"; + includedUntilGVColumn.MinimumWidth = 10; + includedUntilGVColumn.Name = "includedUntilGVColumn"; + includedUntilGVColumn.ReadOnly = true; + includedUntilGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic; + includedUntilGVColumn.Width = 108; + // // ProductsGrid // AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); @@ -314,5 +325,6 @@ namespace LibationWinForms.GridView private LastDownloadedGridViewColumn lastDownloadedGVColumn; private System.Windows.Forms.DataGridViewCheckBoxColumn isSpatialGVColumn; private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn includedUntilGVColumn; } } From b07e61e6a8c8d15eb0ad9b1134fdd3e093896814 Mon Sep 17 00:00:00 2001 From: delebash Date: Fri, 17 Oct 2025 13:49:37 -0400 Subject: [PATCH 2/7] fix UntilDate --- Source/DtoImporterService/BookImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/DtoImporterService/BookImporter.cs b/Source/DtoImporterService/BookImporter.cs index b310b8aa..c67157ab 100644 --- a/Source/DtoImporterService/BookImporter.cs +++ b/Source/DtoImporterService/BookImporter.cs @@ -106,7 +106,7 @@ namespace DtoImporterService { foreach (var plan in item.Plans) { - if (plan.IsAyce && plan.EndDate.Value.Year != 2099 && plan.EndDate.HasValue) + if (plan.IsAyce && plan.EndDate.Value.Year != 2099 && plan.EndDate.Value.Year != 9999 && plan.EndDate.HasValue) { includedUntil = plan.EndDate.Value.LocalDateTime; } From ba98820989e32301e17b9e0427e995e86bc6f2c5 Mon Sep 17 00:00:00 2001 From: delebash Date: Sat, 18 Oct 2025 02:05:56 -0400 Subject: [PATCH 3/7] move code to LibraryBook --- Source/ApplicationServices/LibraryCommands.cs | 2 +- Source/DataLayer/EfClasses/Book.cs | 8 +------ Source/DataLayer/EfClasses/LibraryBook.cs | 6 ++++-- .../LibationContextModelSnapshot.cs | 5 ++++- Source/DtoImporterService/BookImporter.cs | 20 ++---------------- .../DtoImporterService/LibraryBookImporter.cs | 21 ++++++++++++++++++- Source/LibationUiBase/GridView/GridEntry.cs | 4 ++-- 7 files changed, 34 insertions(+), 32 deletions(-) diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index f2e2a7cc..0647f79a 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); + book = new LibraryBook(bookImporter.Cache[importItem.DtoItem.ProductId], importItem.DtoItem.DateAdded, importItem.AccountId,DateTime.Now); context.LibraryBooks.Add(book); } else diff --git a/Source/DataLayer/EfClasses/Book.cs b/Source/DataLayer/EfClasses/Book.cs index d11fb75b..f93fb1ef 100644 --- a/Source/DataLayer/EfClasses/Book.cs +++ b/Source/DataLayer/EfClasses/Book.cs @@ -42,7 +42,6 @@ namespace DataLayer public int LengthInMinutes { get; private set; } public ContentType ContentType { get; private set; } public string Locale { get; private set; } - public DateTime? IncludedUntil { get; private set; } // mutable public string PictureId { get; set; } @@ -74,8 +73,7 @@ namespace DataLayer ContentType contentType, IEnumerable authors, IEnumerable narrators, - string localeName, - DateTime? includedUntil + string localeName ) { // validate @@ -100,7 +98,6 @@ namespace DataLayer UpdateTitle(title, subtitle); Description = description?.Trim() ?? ""; LengthInMinutes = lengthInMinutes; - IncludedUntil = includedUntil; ContentType = contentType; // assigns with biz logic @@ -117,9 +114,6 @@ namespace DataLayer public void UpdateLengthInMinutes(int lengthInMinutes) => LengthInMinutes = lengthInMinutes; - - public void UpdateIncludedUntil(DateTime? includedUntil) - => IncludedUntil = includedUntil; #region contributors, authors, narrators internal HashSet ContributorsLink { get; private set; } diff --git a/Source/DataLayer/EfClasses/LibraryBook.cs b/Source/DataLayer/EfClasses/LibraryBook.cs index 32ef263d..ad650ab5 100644 --- a/Source/DataLayer/EfClasses/LibraryBook.cs +++ b/Source/DataLayer/EfClasses/LibraryBook.cs @@ -13,9 +13,10 @@ namespace DataLayer public bool IsDeleted { get; set; } public bool AbsentFromLastScan { get; set; } - + + public DateTime? IncludedUntil { get; private set; } private LibraryBook() { } - public LibraryBook(Book book, DateTime dateAdded, string account) + public LibraryBook(Book book, DateTime dateAdded, string account, DateTime? includedUntil) { ArgumentValidator.EnsureNotNull(book, nameof(book)); ArgumentValidator.EnsureNotNull(account, nameof(account)); @@ -23,6 +24,7 @@ namespace DataLayer Book = book; DateAdded = dateAdded; Account = account; + IncludedUntil = includedUntil; } public void SetAccount(string account) => Account = account; diff --git a/Source/DataLayer/Migrations/LibationContextModelSnapshot.cs b/Source/DataLayer/Migrations/LibationContextModelSnapshot.cs index 99f70af0..65f867f9 100644 --- a/Source/DataLayer/Migrations/LibationContextModelSnapshot.cs +++ b/Source/DataLayer/Migrations/LibationContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace DataLayer.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.7"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); modelBuilder.Entity("CategoryCategoryLadder", b => { @@ -194,6 +194,9 @@ namespace DataLayer.Migrations b.Property("DateAdded") .HasColumnType("TEXT"); + b.Property("IncludedUntil") + .HasColumnType("TEXT"); + b.Property("IsDeleted") .HasColumnType("INTEGER"); diff --git a/Source/DtoImporterService/BookImporter.cs b/Source/DtoImporterService/BookImporter.cs index c67157ab..e804ce34 100644 --- a/Source/DtoImporterService/BookImporter.cs +++ b/Source/DtoImporterService/BookImporter.cs @@ -98,20 +98,6 @@ namespace DtoImporterService .DistinctBy(a => a.Name) .Select(n => contributorImporter.Cache[n.Name]) .ToList(); - - //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.Plans is not null) - { - foreach (var plan in item.Plans) - { - if (plan.IsAyce && plan.EndDate.Value.Year != 2099 && plan.EndDate.Value.Year != 9999 && plan.EndDate.HasValue) - { - includedUntil = plan.EndDate.Value.LocalDateTime; - } - } - } Book book; try @@ -125,8 +111,7 @@ namespace DtoImporterService contentType, authors, narrators, - importItem.LocaleName, - includedUntil + importItem.LocaleName ) ).Entity; Cache.Add(book.AudibleProductId, book); @@ -141,8 +126,7 @@ namespace DtoImporterService contentType, QtyAuthors = authors?.Count, QtyNarrators = narrators?.Count, - importItem.LocaleName, - includedUntil + importItem.LocaleName }); throw; } diff --git a/Source/DtoImporterService/LibraryBookImporter.cs b/Source/DtoImporterService/LibraryBookImporter.cs index a2dcfb45..8b694b7b 100644 --- a/Source/DtoImporterService/LibraryBookImporter.cs +++ b/Source/DtoImporterService/LibraryBookImporter.cs @@ -51,6 +51,9 @@ namespace DtoImporterService var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak); int qtyNew = 0; + + + foreach (var item in uniqueImportItems.Values) { @@ -66,10 +69,26 @@ namespace DtoImporterService } 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) + item.AccountId, + includedUntil) { AbsentFromLastScan = isUnavailable(item) }; diff --git a/Source/LibationUiBase/GridView/GridEntry.cs b/Source/LibationUiBase/GridView/GridEntry.cs index 3adf66f2..46aec3cc 100644 --- a/Source/LibationUiBase/GridView/GridEntry.cs +++ b/Source/LibationUiBase/GridView/GridEntry.cs @@ -132,7 +132,7 @@ namespace LibationUiBase.GridView protected virtual DateTime GetPurchaseDate() => LibraryBook.DateAdded; protected virtual int GetLengthInMinutes() => Book.LengthInMinutes; protected string GetPurchaseDateString() => GetPurchaseDate().ToString("d"); - protected string GetIncludedUntilString() => Book.IncludedUntil?.ToString("d") ?? string.Empty; + protected string GetIncludedUntilString() => LibraryBook.IncludedUntil?.ToString("d") ?? string.Empty; protected string GetBookLengthString() { int bookLenMins = GetLengthInMinutes(); @@ -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))); + Invoke(() => UpdateLibraryBook(new LibraryBook(udi.Book, LibraryBook.DateAdded, LibraryBook.Account,LibraryBook.IncludedUntil))); return; } From fcd79c55610b0a28fa450f23cf3186fca85056f3 Mon Sep 17 00:00:00 2001 From: delebash Date: Mon, 20 Oct 2025 12:55:48 -0400 Subject: [PATCH 4/7] 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; } From 07e51f219118d832cfcafe4e417a857b8bc4ef4e Mon Sep 17 00:00:00 2001 From: delebash Date: Mon, 20 Oct 2025 13:09:41 -0400 Subject: [PATCH 5/7] IncludedUntil migration --- .../20251018045524_IncludedUntil.Designer.cs | 477 ++++++++++++++++++ .../20251018045524_IncludedUntil.cs | 29 ++ 2 files changed, 506 insertions(+) create mode 100644 Source/DataLayer/Migrations/20251018045524_IncludedUntil.Designer.cs create mode 100644 Source/DataLayer/Migrations/20251018045524_IncludedUntil.cs diff --git a/Source/DataLayer/Migrations/20251018045524_IncludedUntil.Designer.cs b/Source/DataLayer/Migrations/20251018045524_IncludedUntil.Designer.cs new file mode 100644 index 00000000..121a547c --- /dev/null +++ b/Source/DataLayer/Migrations/20251018045524_IncludedUntil.Designer.cs @@ -0,0 +1,477 @@ +// +using System; +using DataLayer; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DataLayer.Migrations +{ + [DbContext(typeof(LibationContext))] + [Migration("20251018045524_IncludedUntil")] + partial class IncludedUntil + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); + + modelBuilder.Entity("CategoryCategoryLadder", b => + { + b.Property("_categoriesCategoryId") + .HasColumnType("INTEGER"); + + b.Property("_categoryLaddersCategoryLadderId") + .HasColumnType("INTEGER"); + + b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId"); + + b.HasIndex("_categoryLaddersCategoryLadderId"); + + b.ToTable("CategoryCategoryLadder"); + }); + + modelBuilder.Entity("DataLayer.Book", b => + { + b.Property("BookId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleProductId") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .HasColumnType("INTEGER"); + + b.Property("DatePublished") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsAbridged") + .HasColumnType("INTEGER"); + + b.Property("IsSpatial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LengthInMinutes") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .HasColumnType("TEXT"); + + b.Property("PictureId") + .HasColumnType("TEXT"); + + b.Property("PictureLarge") + .HasColumnType("TEXT"); + + b.Property("Subtitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("BookId"); + + b.HasIndex("AudibleProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("DataLayer.BookCategory", b => + { + b.Property("BookId") + .HasColumnType("INTEGER"); + + b.Property("CategoryLadderId") + .HasColumnType("INTEGER"); + + b.HasKey("BookId", "CategoryLadderId"); + + b.HasIndex("BookId"); + + b.HasIndex("CategoryLadderId"); + + b.ToTable("BookCategory"); + }); + + modelBuilder.Entity("DataLayer.BookContributor", b => + { + b.Property("BookId") + .HasColumnType("INTEGER"); + + b.Property("ContributorId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.HasKey("BookId", "ContributorId", "Role"); + + b.HasIndex("BookId"); + + b.HasIndex("ContributorId"); + + b.ToTable("BookContributor"); + }); + + modelBuilder.Entity("DataLayer.Category", b => + { + b.Property("CategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleCategoryId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("CategoryId"); + + b.HasIndex("AudibleCategoryId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("DataLayer.CategoryLadder", b => + { + b.Property("CategoryLadderId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("CategoryLadderId"); + + b.ToTable("CategoryLadders"); + }); + + modelBuilder.Entity("DataLayer.Contributor", b => + { + b.Property("ContributorId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleContributorId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("ContributorId"); + + b.HasIndex("Name"); + + b.ToTable("Contributors"); + + b.HasData( + new + { + ContributorId = -1, + Name = "" + }); + }); + + modelBuilder.Entity("DataLayer.LibraryBook", b => + { + b.Property("BookId") + .HasColumnType("INTEGER"); + + b.Property("AbsentFromLastScan") + .HasColumnType("INTEGER"); + + b.Property("Account") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IncludedUntil") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.HasKey("BookId"); + + b.ToTable("LibraryBooks"); + }); + + modelBuilder.Entity("DataLayer.Series", b => + { + b.Property("SeriesId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleSeriesId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("SeriesId"); + + b.HasIndex("AudibleSeriesId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("DataLayer.SeriesBook", b => + { + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("BookId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("TEXT"); + + b.HasKey("SeriesId", "BookId"); + + b.HasIndex("BookId"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBook"); + }); + + modelBuilder.Entity("CategoryCategoryLadder", b => + { + b.HasOne("DataLayer.Category", null) + .WithMany() + .HasForeignKey("_categoriesCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataLayer.CategoryLadder", null) + .WithMany() + .HasForeignKey("_categoryLaddersCategoryLadderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DataLayer.Book", b => + { + b.OwnsOne("DataLayer.Rating", "Rating", b1 => + { + b1.Property("BookId") + .HasColumnType("INTEGER"); + + b1.Property("OverallRating") + .HasColumnType("REAL"); + + b1.Property("PerformanceRating") + .HasColumnType("REAL"); + + b1.Property("StoryRating") + .HasColumnType("REAL"); + + b1.HasKey("BookId"); + + b1.ToTable("Books"); + + b1.WithOwner() + .HasForeignKey("BookId"); + }); + + b.OwnsMany("DataLayer.Supplement", "Supplements", b1 => + { + b1.Property("SupplementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b1.Property("BookId") + .HasColumnType("INTEGER"); + + b1.Property("Url") + .HasColumnType("TEXT"); + + b1.HasKey("SupplementId"); + + b1.HasIndex("BookId"); + + b1.ToTable("Supplement"); + + b1.WithOwner("Book") + .HasForeignKey("BookId"); + + b1.Navigation("Book"); + }); + + b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 => + { + b1.Property("BookId") + .HasColumnType("INTEGER"); + + b1.Property("BookStatus") + .HasColumnType("INTEGER"); + + b1.Property("IsFinished") + .HasColumnType("INTEGER"); + + b1.Property("LastDownloaded") + .HasColumnType("TEXT"); + + b1.Property("LastDownloadedFileVersion") + .HasColumnType("TEXT"); + + b1.Property("LastDownloadedFormat") + .HasColumnType("INTEGER"); + + b1.Property("LastDownloadedVersion") + .HasColumnType("TEXT"); + + b1.Property("PdfStatus") + .HasColumnType("INTEGER"); + + b1.Property("Tags") + .HasColumnType("TEXT"); + + b1.HasKey("BookId"); + + b1.ToTable("UserDefinedItem", (string)null); + + b1.WithOwner("Book") + .HasForeignKey("BookId"); + + b1.OwnsOne("DataLayer.Rating", "Rating", b2 => + { + b2.Property("UserDefinedItemBookId") + .HasColumnType("INTEGER"); + + b2.Property("OverallRating") + .HasColumnType("REAL"); + + b2.Property("PerformanceRating") + .HasColumnType("REAL"); + + b2.Property("StoryRating") + .HasColumnType("REAL"); + + b2.HasKey("UserDefinedItemBookId"); + + b2.ToTable("UserDefinedItem"); + + b2.WithOwner() + .HasForeignKey("UserDefinedItemBookId"); + }); + + b1.Navigation("Book"); + + b1.Navigation("Rating"); + }); + + b.Navigation("Rating"); + + b.Navigation("Supplements"); + + b.Navigation("UserDefinedItem"); + }); + + modelBuilder.Entity("DataLayer.BookCategory", b => + { + b.HasOne("DataLayer.Book", "Book") + .WithMany("CategoriesLink") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataLayer.CategoryLadder", "CategoryLadder") + .WithMany("BooksLink") + .HasForeignKey("CategoryLadderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("CategoryLadder"); + }); + + modelBuilder.Entity("DataLayer.BookContributor", b => + { + b.HasOne("DataLayer.Book", "Book") + .WithMany("ContributorsLink") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataLayer.Contributor", "Contributor") + .WithMany("BooksLink") + .HasForeignKey("ContributorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("Contributor"); + }); + + modelBuilder.Entity("DataLayer.LibraryBook", b => + { + b.HasOne("DataLayer.Book", "Book") + .WithOne() + .HasForeignKey("DataLayer.LibraryBook", "BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + }); + + modelBuilder.Entity("DataLayer.SeriesBook", b => + { + b.HasOne("DataLayer.Book", "Book") + .WithMany("SeriesLink") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataLayer.Series", "Series") + .WithMany("BooksLink") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("DataLayer.Book", b => + { + b.Navigation("CategoriesLink"); + + b.Navigation("ContributorsLink"); + + b.Navigation("SeriesLink"); + }); + + modelBuilder.Entity("DataLayer.CategoryLadder", b => + { + b.Navigation("BooksLink"); + }); + + modelBuilder.Entity("DataLayer.Contributor", b => + { + b.Navigation("BooksLink"); + }); + + modelBuilder.Entity("DataLayer.Series", b => + { + b.Navigation("BooksLink"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Source/DataLayer/Migrations/20251018045524_IncludedUntil.cs b/Source/DataLayer/Migrations/20251018045524_IncludedUntil.cs new file mode 100644 index 00000000..b4bc7baa --- /dev/null +++ b/Source/DataLayer/Migrations/20251018045524_IncludedUntil.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DataLayer.Migrations +{ + /// + public partial class IncludedUntil : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IncludedUntil", + table: "LibraryBooks", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IncludedUntil", + table: "LibraryBooks"); + } + } +} From f7cd2b106b3cb4b27cbd8506c2ffa38c7663e16b Mon Sep 17 00:00:00 2001 From: delebash Date: Mon, 20 Oct 2025 14:12:04 -0400 Subject: [PATCH 6/7] fix migration files --- ...esigner.cs => 20251020175053_AddIncludedUntil.Designer.cs} | 4 ++-- ...24_IncludedUntil.cs => 20251020175053_AddIncludedUntil.cs} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename Source/DataLayer/Migrations/{20251018045524_IncludedUntil.Designer.cs => 20251020175053_AddIncludedUntil.Designer.cs} (99%) rename Source/DataLayer/Migrations/{20251018045524_IncludedUntil.cs => 20251020175053_AddIncludedUntil.cs} (92%) diff --git a/Source/DataLayer/Migrations/20251018045524_IncludedUntil.Designer.cs b/Source/DataLayer/Migrations/20251020175053_AddIncludedUntil.Designer.cs similarity index 99% rename from Source/DataLayer/Migrations/20251018045524_IncludedUntil.Designer.cs rename to Source/DataLayer/Migrations/20251020175053_AddIncludedUntil.Designer.cs index 121a547c..104a17f0 100644 --- a/Source/DataLayer/Migrations/20251018045524_IncludedUntil.Designer.cs +++ b/Source/DataLayer/Migrations/20251020175053_AddIncludedUntil.Designer.cs @@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace DataLayer.Migrations { [DbContext(typeof(LibationContext))] - [Migration("20251018045524_IncludedUntil")] - partial class IncludedUntil + [Migration("20251020175053_AddIncludedUntil")] + partial class AddIncludedUntil { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) diff --git a/Source/DataLayer/Migrations/20251018045524_IncludedUntil.cs b/Source/DataLayer/Migrations/20251020175053_AddIncludedUntil.cs similarity index 92% rename from Source/DataLayer/Migrations/20251018045524_IncludedUntil.cs rename to Source/DataLayer/Migrations/20251020175053_AddIncludedUntil.cs index b4bc7baa..2f31c7fc 100644 --- a/Source/DataLayer/Migrations/20251018045524_IncludedUntil.cs +++ b/Source/DataLayer/Migrations/20251020175053_AddIncludedUntil.cs @@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace DataLayer.Migrations { /// - public partial class IncludedUntil : Migration + public partial class AddIncludedUntil : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) From fd95ac7a9cbbc03216de47f806beccbfa5d5652d Mon Sep 17 00:00:00 2001 From: delebash Date: Mon, 20 Oct 2025 16:31:06 -0400 Subject: [PATCH 7/7] changes per Mbucari --- Source/LibationUiBase/GridView/GridEntry.cs | 5 +++-- Source/LibationUiBase/GridView/SeriesEntry.cs | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/LibationUiBase/GridView/GridEntry.cs b/Source/LibationUiBase/GridView/GridEntry.cs index 9c55b809..06cb52d8 100644 --- a/Source/LibationUiBase/GridView/GridEntry.cs +++ b/Source/LibationUiBase/GridView/GridEntry.cs @@ -130,9 +130,10 @@ namespace LibationUiBase.GridView protected abstract string GetBookTags(); protected virtual DateTime GetPurchaseDate() => LibraryBook.DateAdded; + protected virtual DateTime? GetIncludedUntil() => LibraryBook.IncludedUntil; protected virtual int GetLengthInMinutes() => Book.LengthInMinutes; protected string GetPurchaseDateString() => GetPurchaseDate().ToString("d"); - protected string GetIncludedUntilString() => LibraryBook.IncludedUntil?.ToString("d") ?? string.Empty; + protected string GetIncludedUntilString() => GetIncludedUntil()?.ToString("d") ?? string.Empty; protected string GetBookLengthString() { int bookLenMins = GetLengthInMinutes(); @@ -213,7 +214,7 @@ namespace LibationUiBase.GridView nameof(Liberate) => Liberate, nameof(DateAdded) => DateAdded, nameof(IsSpatial) => IsSpatial, - nameof(IncludedUntil) => IncludedUntil, + nameof(IncludedUntil) => GetIncludedUntil() ?? default, _ => null }; diff --git a/Source/LibationUiBase/GridView/SeriesEntry.cs b/Source/LibationUiBase/GridView/SeriesEntry.cs index 3706a23b..998d8842 100644 --- a/Source/LibationUiBase/GridView/SeriesEntry.cs +++ b/Source/LibationUiBase/GridView/SeriesEntry.cs @@ -102,5 +102,6 @@ namespace LibationUiBase.GridView protected override string GetBookTags() => null; protected override int GetLengthInMinutes() => Children.Count == 0 ? 0 : Children.Sum(c => c.LibraryBook.Book.LengthInMinutes); protected override DateTime GetPurchaseDate() => Children.Count == 0 ? default : Children.Min(c => c.LibraryBook.DateAdded); + protected override DateTime? GetIncludedUntil() => Children.Count == 0 ? default : Children.Min(c => c.LibraryBook.IncludedUntil); } }