From bd7e45ca3c5ceda43ef1dbedd2e7adf62dc05504 Mon Sep 17 00:00:00 2001 From: Mbucari Date: Thu, 2 Mar 2023 15:09:10 -0700 Subject: [PATCH] Add last download into to database --- Source/AppScaffolding/LibationScaffolding.cs | 6 +- Source/ApplicationServices/LibraryCommands.cs | 4 +- Source/DataLayer/Configurations/BookConfig.cs | 58 +-- Source/DataLayer/EfClasses/UserDefinedItem.cs | 21 +- ...02220539_AddLastDownloadedInfo.Designer.cs | 410 ++++++++++++++++++ .../20230302220539_AddLastDownloadedInfo.cs | 39 ++ .../LibationContextModelSnapshot.cs | 8 +- Source/FileLiberator/DownloadDecryptBook.cs | 2 +- .../Configuration.Environment.cs | 2 + 9 files changed, 518 insertions(+), 32 deletions(-) create mode 100644 Source/DataLayer/Migrations/20230302220539_AddLastDownloadedInfo.Designer.cs create mode 100644 Source/DataLayer/Migrations/20230302220539_AddLastDownloadedInfo.cs diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 1756da77..fbe45a4e 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -75,13 +75,15 @@ namespace AppScaffolding ??= new[] { ExecutingAssembly.GetName(), EntryAssembly.GetName() } .Max(a => a.Version); - /// Run migrations before loading Configuration for the first time. Then load and return Configuration - public static Configuration RunPreConfigMigrations() + /// Run migrations before loading Configuration for the first time. Then load and return Configuration + public static Configuration RunPreConfigMigrations() { // must occur before access to Configuration instance // // outdated. kept here as an example of what belongs in this area // // Migrations.migrate_to_v5_2_0__pre_config(); + Configuration.SetLibationVersion(BuildVersion); + //***********************************************// // // // do not use Configuration before this line // diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index 4691c67b..d166a158 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -369,8 +369,10 @@ namespace ApplicationServices if (rating is not null) udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating); - }); + }); + public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus, Version libationVersion) + => book.UpdateUserDefinedItem(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion); }); public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus) => book.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus); public static int UpdateBookStatus(this IEnumerable books, LiberatedStatus bookStatus) diff --git a/Source/DataLayer/Configurations/BookConfig.cs b/Source/DataLayer/Configurations/BookConfig.cs index 17054ea7..55c13038 100644 --- a/Source/DataLayer/Configurations/BookConfig.cs +++ b/Source/DataLayer/Configurations/BookConfig.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; namespace DataLayer.Configurations { @@ -19,40 +20,45 @@ namespace DataLayer.Configurations entity.Ignore(nameof(Book.Authors)); entity.Ignore(nameof(Book.Narrators)); entity.Ignore(nameof(Book.AudioFormat)); - //// these don't seem to matter - //entity.Ignore(nameof(Book.AuthorNames)); - //entity.Ignore(nameof(Book.NarratorNames)); - //entity.Ignore(nameof(Book.HasPdfs)); + //// these don't seem to matter + //entity.Ignore(nameof(Book.AuthorNames)); + //entity.Ignore(nameof(Book.NarratorNames)); + //entity.Ignore(nameof(Book.HasPdfs)); - // OwnsMany: "Can only ever appear on navigation properties of other entity types. - // Are automatically loaded, and can only be tracked by a DbContext alongside their owner." - entity - .OwnsMany(b => b.Supplements, b_s => - { - b_s.WithOwner(s => s.Book) - .HasForeignKey(s => s.BookId); - b_s.HasKey(s => s.SupplementId); - }); + // OwnsMany: "Can only ever appear on navigation properties of other entity types. + // Are automatically loaded, and can only be tracked by a DbContext alongside their owner." + entity + .OwnsMany(b => b.Supplements, b_s => + { + b_s.WithOwner(s => s.Book) + .HasForeignKey(s => s.BookId); + b_s.HasKey(s => s.SupplementId); + }); // even though it's owned, we need to map its backing field entity .Metadata .FindNavigation(nameof(Book.Supplements)) .SetPropertyAccessMode(PropertyAccessMode.Field); - // owns it 1:1, store in separate table - entity - .OwnsOne(b => b.UserDefinedItem, b_udi => - { - b_udi.WithOwner(udi => udi.Book) - .HasForeignKey(udi => udi.BookId); - b_udi.Property(udi => udi.BookId).ValueGeneratedNever(); - b_udi.ToTable(nameof(Book.UserDefinedItem)); + // owns it 1:1, store in separate table + entity + .OwnsOne(b => b.UserDefinedItem, b_udi => + { + b_udi.WithOwner(udi => udi.Book) + .HasForeignKey(udi => udi.BookId); + b_udi.Property(udi => udi.BookId).ValueGeneratedNever(); + b_udi.ToTable(nameof(Book.UserDefinedItem)); - // owns it 1:1, store in same table - b_udi.OwnsOne(udi => udi.Rating); - }); + b_udi.Property(udi => udi.LastDownloaded); + b_udi + .Property(udi => udi.LastDownloadedVersion) + .HasConversion(ver => ver.ToString(), str => Version.Parse(str)); - entity + // owns it 1:1, store in same table + b_udi.OwnsOne(udi => udi.Rating); + }); + + entity .Metadata .FindNavigation(nameof(Book.ContributorsLink)) // PropertyAccessMode.Field : Contributions is a get-only property, not a field, so use its backing field @@ -68,6 +74,6 @@ namespace DataLayer.Configurations .HasOne(b => b.Category) .WithMany() .HasForeignKey(b => b.CategoryId); - } + } } } \ No newline at end of file diff --git a/Source/DataLayer/EfClasses/UserDefinedItem.cs b/Source/DataLayer/EfClasses/UserDefinedItem.cs index d34c45e7..86c87fcf 100644 --- a/Source/DataLayer/EfClasses/UserDefinedItem.cs +++ b/Source/DataLayer/EfClasses/UserDefinedItem.cs @@ -24,8 +24,27 @@ namespace DataLayer { internal int BookId { get; private set; } public Book Book { get; private set; } + public DateTime? LastDownloaded { get; private set; } + public Version LastDownloadedVersion { get; private set; } - private UserDefinedItem() { } + public void SetLastDownloaded(Version version) + { + if (LastDownloadedVersion != version) + { + LastDownloadedVersion = version; + OnItemChanged(nameof(LastDownloadedVersion)); + } + + if (version is null) + LastDownloaded = null; + else + { + LastDownloaded = DateTime.Now; + OnItemChanged(nameof(LastDownloaded)); + } + } + + private UserDefinedItem() { } internal UserDefinedItem(Book book) { ArgumentValidator.EnsureNotNull(book, nameof(book)); diff --git a/Source/DataLayer/Migrations/20230302220539_AddLastDownloadedInfo.Designer.cs b/Source/DataLayer/Migrations/20230302220539_AddLastDownloadedInfo.Designer.cs new file mode 100644 index 00000000..ffcec6cc --- /dev/null +++ b/Source/DataLayer/Migrations/20230302220539_AddLastDownloadedInfo.Designer.cs @@ -0,0 +1,410 @@ +// +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("20230302220539_AddLastDownloadedInfo")] + partial class AddLastDownloadedInfo + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("DataLayer.Book", b => + { + b.Property("BookId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudibleProductId") + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ContentType") + .HasColumnType("INTEGER"); + + b.Property("DatePublished") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsAbridged") + .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("Title") + .HasColumnType("TEXT"); + + b.Property("_audioFormat") + .HasColumnType("INTEGER"); + + b.HasKey("BookId"); + + b.HasIndex("AudibleProductId"); + + b.HasIndex("CategoryId"); + + b.ToTable("Books"); + }); + + 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.Property("ParentCategoryCategoryId") + .HasColumnType("INTEGER"); + + b.HasKey("CategoryId"); + + b.HasIndex("AudibleCategoryId"); + + b.HasIndex("ParentCategoryCategoryId"); + + b.ToTable("Categories"); + + b.HasData( + new + { + CategoryId = -1, + AudibleCategoryId = "", + Name = "" + }); + }); + + 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("Account") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .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("DataLayer.Book", b => + { + b.HasOne("DataLayer.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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("LastDownloaded") + .HasColumnType("TEXT"); + + 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("Category"); + + b.Navigation("Rating"); + + b.Navigation("Supplements"); + + b.Navigation("UserDefinedItem"); + }); + + 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.Category", b => + { + b.HasOne("DataLayer.Category", "ParentCategory") + .WithMany() + .HasForeignKey("ParentCategoryCategoryId"); + + b.Navigation("ParentCategory"); + }); + + 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("ContributorsLink"); + + b.Navigation("SeriesLink"); + }); + + 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/20230302220539_AddLastDownloadedInfo.cs b/Source/DataLayer/Migrations/20230302220539_AddLastDownloadedInfo.cs new file mode 100644 index 00000000..db823779 --- /dev/null +++ b/Source/DataLayer/Migrations/20230302220539_AddLastDownloadedInfo.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DataLayer.Migrations +{ + /// + public partial class AddLastDownloadedInfo : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastDownloaded", + table: "UserDefinedItem", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastDownloadedVersion", + table: "UserDefinedItem", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastDownloaded", + table: "UserDefinedItem"); + + migrationBuilder.DropColumn( + name: "LastDownloadedVersion", + table: "UserDefinedItem"); + } + } +} diff --git a/Source/DataLayer/Migrations/LibationContextModelSnapshot.cs b/Source/DataLayer/Migrations/LibationContextModelSnapshot.cs index 080d7beb..747513d6 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", "7.0.2"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); modelBuilder.Entity("DataLayer.Book", b => { @@ -272,6 +272,12 @@ namespace DataLayer.Migrations b1.Property("BookStatus") .HasColumnType("INTEGER"); + b1.Property("LastDownloaded") + .HasColumnType("TEXT"); + + b1.Property("LastDownloadedVersion") + .HasColumnType("TEXT"); + b1.Property("PdfStatus") .HasColumnType("INTEGER"); diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 39b5a86d..3d43c8b6 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -80,7 +80,7 @@ namespace FileLiberator { Task.Run(() => downloadCoverArt(libraryBook)), Task.Run(() => moveFilesToBooksDir(libraryBook, entries)), - Task.Run(() => libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated)), + Task.Run(() => libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion)), Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir)) }; diff --git a/Source/LibationFileManager/Configuration.Environment.cs b/Source/LibationFileManager/Configuration.Environment.cs index 1eec30a1..55ce4fdc 100644 --- a/Source/LibationFileManager/Configuration.Environment.cs +++ b/Source/LibationFileManager/Configuration.Environment.cs @@ -20,6 +20,8 @@ namespace LibationFileManager public static bool IsWindows { get; } = OperatingSystem.IsWindows(); public static bool IsLinux { get; } = OperatingSystem.IsLinux(); public static bool IsMacOs { get; } = OperatingSystem.IsMacOS(); + public static Version LibationVersion { get; private set; } + public static void SetLibationVersion(Version version) => LibationVersion = version; public static OS OS { get; } = IsLinux ? OS.Linux