diff --git a/ApplicationServices/LibraryExporter.cs b/ApplicationServices/LibraryExporter.cs index 28c99cd7..22813002 100644 --- a/ApplicationServices/LibraryExporter.cs +++ b/ApplicationServices/LibraryExporter.cs @@ -89,11 +89,14 @@ namespace ApplicationServices [Name("My Libation Tags")] public string MyLibationTags { get; set; } - [Name("Book Liberation Status")] + [Name("Book Liberated Status")] public string BookStatus { get; set; } - [Name("PDF Liberation Status")] + [Name("PDF Liberated Status")] public string PdfStatus { get; set; } + + [Name("Content Type")] + public string ContentType { get; set; } } public static class LibToDtos { @@ -124,7 +127,8 @@ namespace ApplicationServices MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating, MyLibationTags = a.Book.UserDefinedItem.Tags, BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(), - PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString() + PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(), + ContentType = a.Book.ContentType.ToString() }).ToList(); } public static class LibraryExporter @@ -197,7 +201,8 @@ namespace ApplicationServices nameof (ExportDto.MyRatingStory), nameof (ExportDto.MyLibationTags), nameof (ExportDto.BookStatus), - nameof (ExportDto.PdfStatus) + nameof (ExportDto.PdfStatus), + nameof (ExportDto.ContentType) }; var col = 0; foreach (var c in columns) @@ -261,6 +266,7 @@ namespace ApplicationServices row.CreateCell(col++).SetCellValue(dto.MyLibationTags); row.CreateCell(col++).SetCellValue(dto.BookStatus); row.CreateCell(col++).SetCellValue(dto.PdfStatus); + row.CreateCell(col++).SetCellValue(dto.ContentType); rowIndex++; } diff --git a/DataLayer/EfClasses/Book.cs b/DataLayer/EfClasses/Book.cs index da366bfa..45fee3d2 100644 --- a/DataLayer/EfClasses/Book.cs +++ b/DataLayer/EfClasses/Book.cs @@ -15,6 +15,10 @@ namespace DataLayer Id = id; } } + + // enum will be easier than bool to extend later + public enum ContentType { Unknown = 0, Product = 1, Episode = 2 } + public class Book { // implementation detail. set by db only. only used by data layer @@ -25,6 +29,7 @@ namespace DataLayer public string Title { get; private set; } public string Description { get; private set; } public int LengthInMinutes { get; private set; } + public ContentType ContentType { get; private set; } // immutable-ish. should be immutable. mutability is necessary for v3 => v4 upgrades public string Locale { get; private set; } @@ -82,6 +87,7 @@ namespace DataLayer string title, string description, int lengthInMinutes, + ContentType contentType, IEnumerable authors, IEnumerable narrators, Category category, string localeName) @@ -109,6 +115,7 @@ namespace DataLayer Title = title.Trim(); Description = description.Trim(); LengthInMinutes = lengthInMinutes; + ContentType = contentType; // assigns with biz logic ReplaceAuthors(authors); diff --git a/DataLayer/EfClasses/UserDefinedItem.cs b/DataLayer/EfClasses/UserDefinedItem.cs index 5bd52beb..acc484fe 100644 --- a/DataLayer/EfClasses/UserDefinedItem.cs +++ b/DataLayer/EfClasses/UserDefinedItem.cs @@ -18,7 +18,6 @@ namespace DataLayer /// Application-state only. Not a valid persistence state. PartialDownload = 0x1000 - } public class UserDefinedItem diff --git a/DataLayer/Migrations/20210901205042_BookIsEpisode.Designer.cs b/DataLayer/Migrations/20210901205042_BookIsEpisode.Designer.cs new file mode 100644 index 00000000..bba21c46 --- /dev/null +++ b/DataLayer/Migrations/20210901205042_BookIsEpisode.Designer.cs @@ -0,0 +1,390 @@ +// +using System; +using DataLayer; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace DataLayer.Migrations +{ + [DbContext(typeof(LibationContext))] + [Migration("20210901205042_BookIsEpisode")] + partial class BookIsEpisode + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.9"); + + 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("LengthInMinutes") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .HasColumnType("TEXT"); + + b.Property("PictureId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + 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.HasKey("BookId"); + + b.ToTable("Library"); + }); + + 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("Index") + .HasColumnType("REAL"); + + 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("PdfStatus") + .HasColumnType("INTEGER"); + + b1.Property("Tags") + .HasColumnType("TEXT"); + + b1.HasKey("BookId"); + + b1.ToTable("UserDefinedItem"); + + 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/DataLayer/Migrations/20210901205042_BookIsEpisode.cs b/DataLayer/Migrations/20210901205042_BookIsEpisode.cs new file mode 100644 index 00000000..30721e46 --- /dev/null +++ b/DataLayer/Migrations/20210901205042_BookIsEpisode.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace DataLayer.Migrations +{ + public partial class BookIsEpisode : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ContentType", + table: "Books", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ContentType", + table: "Books"); + } + } +} diff --git a/DataLayer/Migrations/LibationContextModelSnapshot.cs b/DataLayer/Migrations/LibationContextModelSnapshot.cs index 73814c05..ffe5b201 100644 --- a/DataLayer/Migrations/LibationContextModelSnapshot.cs +++ b/DataLayer/Migrations/LibationContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace DataLayer.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.5"); + .HasAnnotation("ProductVersion", "5.0.9"); modelBuilder.Entity("DataLayer.Book", b => { @@ -28,6 +28,9 @@ namespace DataLayer.Migrations b.Property("CategoryId") .HasColumnType("INTEGER"); + b.Property("ContentType") + .HasColumnType("INTEGER"); + b.Property("DatePublished") .HasColumnType("TEXT"); diff --git a/DtoImporterService/BookImporter.cs b/DtoImporterService/BookImporter.cs index 0245b92e..cb0f7ccb 100644 --- a/DtoImporterService/BookImporter.cs +++ b/DtoImporterService/BookImporter.cs @@ -67,6 +67,8 @@ namespace DtoImporterService { var item = importItem.DtoItem; + var contentType = item.IsEpisodes ? DataLayer.ContentType.Episode : DataLayer.ContentType.Product; + // absence of authors is very rare, but possible if (!item.Authors?.Any() ?? true) item.Authors = new[] { new Person { Name = "", Asin = null } }; @@ -105,6 +107,7 @@ namespace DtoImporterService item.TitleWithSubtitle, item.Description, item.LengthInMinutes, + contentType, authors, narrators, category, diff --git a/InternalUtilities/AudibleApiActions.cs b/InternalUtilities/AudibleApiActions.cs index 62c570fa..a1cd2128 100644 --- a/InternalUtilities/AudibleApiActions.cs +++ b/InternalUtilities/AudibleApiActions.cs @@ -59,6 +59,10 @@ namespace InternalUtilities private static async Task> getItemsAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups) { var items = await api.GetAllLibraryItemsAsync(responseGroups); +#if DEBUG +//var itemsDebug = items.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}"); +//System.IO.File.WriteAllText("library.json", itemsDebug); +#endif await manageEpisodesAsync(api, items); @@ -82,15 +86,23 @@ namespace InternalUtilities { // get parents var parents = items.Where(i => i.IsEpisodes).ToList(); +#if DEBUG +//var parentsDebug = parents.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}"); +//System.IO.File.WriteAllText("parents.json", parentsDebug); +#endif if (!parents.Any()) return; - // remove episode parents. even if the following stuff fails, these will still be removed from the collection + Serilog.Log.Logger.Information($"{parents.Count} series of shows/podcasts found"); + + // remove episode parents. even if the following stuff fails, these will still be removed from the collection. + // also must happen before processing children because children abuses this flag items.RemoveAll(i => i.IsEpisodes); // add children var children = await getEpisodesAsync(api, parents); + Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found"); items.AddRange(children); } catch (Exception ex) @@ -101,17 +113,36 @@ namespace InternalUtilities private static async Task> getEpisodesAsync(Api api, List parents) { - Serilog.Log.Logger.Information($"{parents.Count} series of episodes/podcasts found"); - var results = new List(); foreach (var parent in parents) { var children = await getEpisodeChildrenAsync(api, parent); - // use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime foreach (var child in children) + { + // use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime child.PurchaseDate = parent.PurchaseDate; + // parent is essentially a series + child.Series = new Series[] + { + new Series + { + Asin = parent.Asin, + Sequence = parent.Relationships.Single(r => r.Asin == child.Asin).Sort.ToString(), + Title = parent.TitleWithSubtitle + } + }; + // overload (read: abuse) IsEpisodes flag + child.Relationships = new Relationship[] + { + new Relationship + { + RelationshipToProduct = RelationshipToProduct.Child, + RelationshipType = RelationshipType.Episode + } + }; + } results.AddRange(children); } @@ -141,6 +172,10 @@ namespace InternalUtilities try { childrenBatch = await api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS); +#if DEBUG +//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}"); +//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug); +#endif } catch (Exception ex) { @@ -178,7 +213,7 @@ namespace InternalUtilities return results; } - #endregion +#endregion private static List getValidators() { diff --git a/InternalUtilities/InternalUtilities.csproj b/InternalUtilities/InternalUtilities.csproj index 23e715e9..1ba32c43 100644 --- a/InternalUtilities/InternalUtilities.csproj +++ b/InternalUtilities/InternalUtilities.csproj @@ -5,7 +5,7 @@ - + diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index 8284fd30..52c8d1a6 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -13,7 +13,7 @@ win-x64 - 5.6.0.2 + 5.6.0.8