Add category ladders

This commit is contained in:
Mbucari 2023-07-17 16:50:45 -06:00
parent 90eccbf2f6
commit ea6adeb58f
23 changed files with 983 additions and 122 deletions

View File

@ -141,7 +141,7 @@ namespace ApplicationServices
PictureId = a.Book.PictureId, PictureId = a.Book.PictureId,
IsAbridged = a.Book.IsAbridged, IsAbridged = a.Book.IsAbridged,
DatePublished = a.Book.DatePublished, DatePublished = a.Book.DatePublished,
CategoriesNames = a.Book.CategoriesNames().Any() ? a.Book.CategoriesNames().Aggregate((a, b) => $"{a}, {b}") : "", CategoriesNames = a.Book.LowestCategoryNames().Any() ? a.Book.LowestCategoryNames().Aggregate((a, b) => $"{a}, {b}") : "",
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating, MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating, MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating, MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,

View File

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore;
namespace DataLayer.Configurations
{
internal class BookCategoryConfig : IEntityTypeConfiguration<BookCategory>
{
public void Configure(EntityTypeBuilder<BookCategory> entity)
{
entity.HasKey(bc => new { bc.BookId, bc.CategoryLadderId });
entity.HasIndex(bc => bc.BookId);
entity.HasIndex(bc => bc.CategoryLadderId);
entity
.HasOne(bc => bc.Book)
.WithMany(b => b.CategoriesLink)
.HasForeignKey(bc => bc.BookId);
entity
.HasOne(bc => bc.CategoryLadder)
.WithMany(c => c.BooksLink)
.HasForeignKey(bc => bc.CategoryLadderId);
}
}
}

View File

@ -72,9 +72,10 @@ namespace DataLayer.Configurations
.SetPropertyAccessMode(PropertyAccessMode.Field); .SetPropertyAccessMode(PropertyAccessMode.Field);
entity entity
.HasOne(b => b.Category) .Metadata
.WithMany() .FindNavigation(nameof(Book.CategoriesLink))
.HasForeignKey(b => b.CategoryId); // PropertyAccessMode.Field : Categories is a get-only property, not a field, so use its backing field
.SetPropertyAccessMode(PropertyAccessMode.Field);
} }
} }
} }

View File

@ -10,8 +10,11 @@ namespace DataLayer.Configurations
entity.HasKey(c => c.CategoryId); entity.HasKey(c => c.CategoryId);
entity.HasIndex(c => c.AudibleCategoryId); entity.HasIndex(c => c.AudibleCategoryId);
// seeds go here. examples in Dinah.EntityFrameworkCore.Tests\DbContextFactoryExample.cs entity.Ignore(c => c.CategoryLadders);
entity.HasData(Category.GetEmpty());
entity
.HasMany(e => e._categoryLadders)
.WithMany(e => e._categories);
} }
} }
} }

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore;
namespace DataLayer.Configurations
{
internal class CategoryLadderConfig : IEntityTypeConfiguration<CategoryLadder>
{
public void Configure(EntityTypeBuilder<CategoryLadder> entity)
{
entity.HasKey(cl => cl.CategoryLadderId);
entity.Ignore(cl => cl.Categories);
entity
.HasMany(cl => cl._categories)
.WithMany(c => c._categoryLadders);
entity
.Metadata
.FindNavigation(nameof(CategoryLadder.BooksLink))
.SetPropertyAccessMode(PropertyAccessMode.Field);
}
}
}

View File

@ -55,10 +55,6 @@ namespace DataLayer
public DateTime? DatePublished { get; private set; } public DateTime? DatePublished { get; private set; }
public string Language { get; private set; } public string Language { get; private set; }
// non-null. use "empty pattern"
internal int CategoryId { get; private set; }
public Category Category { get; private set; }
// is owned, not optional 1:1 // is owned, not optional 1:1
public UserDefinedItem UserDefinedItem { get; private set; } public UserDefinedItem UserDefinedItem { get; private set; }
@ -79,7 +75,6 @@ namespace DataLayer
ContentType contentType, ContentType contentType,
IEnumerable<Contributor> authors, IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators, IEnumerable<Contributor> narrators,
Category category,
string localeName) string localeName)
{ {
// validate // validate
@ -96,11 +91,10 @@ namespace DataLayer
// non-ef-ctor init.s // non-ef-ctor init.s
UserDefinedItem = new UserDefinedItem(this); UserDefinedItem = new UserDefinedItem(this);
_contributorsLink = new HashSet<BookContributor>(); _contributorsLink = new HashSet<BookContributor>();
_categoriesLink = new HashSet<BookCategory>();
_seriesLink = new HashSet<SeriesBook>(); _seriesLink = new HashSet<SeriesBook>();
_supplements = new HashSet<Supplement>(); _supplements = new HashSet<Supplement>();
Category = category;
// simple assigns // simple assigns
UpdateTitle(title, subtitle); UpdateTitle(title, subtitle);
Description = description?.Trim() ?? ""; Description = description?.Trim() ?? "";
@ -182,9 +176,30 @@ namespace DataLayer
return entry; return entry;
} }
#region categories
private HashSet<BookCategory> _categoriesLink;
public IEnumerable<BookCategory> CategoriesLink => _categoriesLink?.ToList();
public void UpsertCategories(CategoryLadder ladder)
{
ArgumentValidator.EnsureNotNull(ladder, nameof(ladder));
#region series var singleBookCategory = _categoriesLink.SingleOrDefault(bc => bc.CategoryLadder.Equals(ladder));
private HashSet<SeriesBook> _seriesLink;
if (singleBookCategory is null)
_categoriesLink.Add(new BookCategory(this, ladder));
else
{
for (var i = 0; i < ladder._categories.Count; i++)
{
//Update the category name
singleBookCategory.CategoryLadder._categories[i].Name = ladder._categories[i].Name;
}
}
}
#endregion
#region series
private HashSet<SeriesBook> _seriesLink;
public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList(); public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList();
public void UpsertSeries(Series series, string order, DbContext context = null) public void UpsertSeries(Series series, string order, DbContext context = null)
@ -234,15 +249,6 @@ namespace DataLayer
Language = language?.FirstCharToUpper() ?? Language; Language = language?.FirstCharToUpper() ?? Language;
} }
public void UpdateCategory(Category category, DbContext context = null)
{
// since category is never null, nullity means it hasn't been loaded
if (Category is null)
getEntry(context).Reference(s => s.Category).Load();
Category = category;
}
public override string ToString() => $"[{AudibleProductId}] {TitleWithSubtitle}"; public override string ToString() => $"[{AudibleProductId}] {TitleWithSubtitle}";
} }
} }

View File

@ -0,0 +1,20 @@
using Dinah.Core;
namespace DataLayer
{
public class BookCategory
{
internal int BookId { get; private set; }
internal int CategoryLadderId { get; private set; }
public Book Book { get; private set; }
public CategoryLadder CategoryLadder { get; private set; }
private BookCategory() { }
internal BookCategory(Book book, CategoryLadder categoriesList)
{
Book = ArgumentValidator.EnsureNotNull(book, nameof(book));
CategoryLadder = ArgumentValidator.EnsureNotNull(categoriesList, nameof(categoriesList));
}
}
}

View File

@ -1,8 +1,5 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Dinah.Core; using Dinah.Core;
using Microsoft.EntityFrameworkCore;
namespace DataLayer namespace DataLayer
{ {
@ -15,20 +12,20 @@ namespace DataLayer
Id = id; Id = id;
} }
} }
public class Category public class Category
{ {
// Empty is a special case. use private ctor w/o validation
public static Category GetEmpty() => new() { CategoryId = -1, AudibleCategoryId = "", Name = "" };
internal int CategoryId { get; private set; } internal int CategoryId { get; private set; }
public string AudibleCategoryId { get; private set; } public string AudibleCategoryId { get; private set; }
public string Name { get; private set; } public string Name { get; internal set; }
public Category ParentCategory { get; private set; }
private Category() { } internal List<CategoryLadder> _categoryLadders = new();
public IReadOnlyCollection<CategoryLadder> CategoryLadders => _categoryLadders.AsReadOnly();
private Category() { }
/// <summary>special id class b/c it's too easy to get string order mixed up</summary> /// <summary>special id class b/c it's too easy to get string order mixed up</summary>
public Category(AudibleCategoryId audibleSeriesId, string name, Category parentCategory = null) public Category(AudibleCategoryId audibleSeriesId, string name)
{ {
ArgumentValidator.EnsureNotNull(audibleSeriesId, nameof(audibleSeriesId)); ArgumentValidator.EnsureNotNull(audibleSeriesId, nameof(audibleSeriesId));
var id = audibleSeriesId.Id; var id = audibleSeriesId.Id;
@ -37,15 +34,6 @@ namespace DataLayer
AudibleCategoryId = id; AudibleCategoryId = id;
Name = name; Name = name;
UpdateParentCategory(parentCategory);
}
public void UpdateParentCategory(Category parentCategory)
{
// don't overwrite with null but not an error
if (parentCategory is not null)
ParentCategory = parentCategory;
} }
public override string ToString() => $"[{AudibleCategoryId}] {Name}"; public override string ToString() => $"[{AudibleCategoryId}] {Name}";

View File

@ -0,0 +1,48 @@
using Dinah.Core;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace DataLayer
{
public class CategoryLadder : IEquatable<CategoryLadder>
{
internal int CategoryLadderId { get; private set; }
internal List<Category> _categories;
public ReadOnlyCollection<Category> Categories => _categories.AsReadOnly();
private HashSet<BookCategory> _booksLink;
public IEnumerable<BookCategory> BooksLink => _booksLink?.ToList();
private CategoryLadder() { _categories = new(); }
public CategoryLadder(List<Category> categories)
{
ArgumentValidator.EnsureNotNull(categories, nameof(categories));
ArgumentValidator.EnsureGreaterThan(categories.Count, nameof(categories), 0);
_booksLink = new HashSet<BookCategory>();
_categories = categories;
}
public override int GetHashCode()
{
HashCode hashCode = default;
foreach (var category in _categories)
hashCode.Add(category.AudibleCategoryId);
return hashCode.ToHashCode();
}
public bool Equals(CategoryLadder other)
{
if (other?._categories is null)
return false;
return Equals(other._categories.Select(c => c.AudibleCategoryId));
}
public bool Equals(IEnumerable<string> categoryIds)
=> _categories.Select(c => c.AudibleCategoryId).SequenceEqual(categoryIds);
public override bool Equals(object obj) => obj is CategoryLadder other && Equals(other);
public override string ToString() => string.Join(" > ", _categories.Select(c => c.Name));
}
}

View File

@ -46,14 +46,23 @@ namespace DataLayer
? $"{sb.Series.Name} (#{sb.Order})" ? $"{sb.Series.Name} (#{sb.Order})"
: sb.Series.Name; : sb.Series.Name;
} }
public static string[] CategoriesNames(this Book book)
=> book.Category is null ? new string[0] public static string[] LowestCategoryNames(this Book book)
: book.Category.ParentCategory is null ? new[] { book.Category.Name } => book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
: new[] { book.Category.ParentCategory.Name, book.Category.Name }; : book
public static string[] CategoriesIds(this Book book) .CategoriesLink
=> book.Category is null ? null .Select(cl => cl.CategoryLadder.Categories.LastOrDefault()?.Name)
: book.Category.ParentCategory is null ? new[] { book.Category.AudibleCategoryId } .Where(c => c is not null)
: new[] { book.Category.ParentCategory.AudibleCategoryId, book.Category.AudibleCategoryId }; .Distinct()
.ToArray();
public static string[] CategoriesIds(this Book book)
=> book.CategoriesLink?.Any() is not true ? null
: book
.CategoriesLink
.SelectMany(cl => cl.CategoryLadder.Categories)
.Select(c => c.AudibleCategoryId)
.ToArray();
public static string AggregateTitles(this IEnumerable<LibraryBook> libraryBooks, int max = 5) public static string AggregateTitles(this IEnumerable<LibraryBook> libraryBooks, int max = 5)
{ {

View File

@ -23,6 +23,7 @@ namespace DataLayer
public DbSet<Contributor> Contributors { get; private set; } public DbSet<Contributor> Contributors { get; private set; }
public DbSet<Series> Series { get; private set; } public DbSet<Series> Series { get; private set; }
public DbSet<Category> Categories { get; private set; } public DbSet<Category> Categories { get; private set; }
public DbSet<CategoryLadder> CategoryLadders { get; private set; }
public static LibationContext Create(string connectionString) public static LibationContext Create(string connectionString)
{ {
@ -39,13 +40,15 @@ namespace DataLayer
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new BookConfig()); modelBuilder.ApplyConfiguration(new BookConfig());
modelBuilder.ApplyConfiguration(new ContributorConfig()); modelBuilder.ApplyConfiguration(new ContributorConfig());
modelBuilder.ApplyConfiguration(new BookContributorConfig()); modelBuilder.ApplyConfiguration(new BookContributorConfig());
modelBuilder.ApplyConfiguration(new LibraryBookConfig()); modelBuilder.ApplyConfiguration(new LibraryBookConfig());
modelBuilder.ApplyConfiguration(new SeriesConfig()); modelBuilder.ApplyConfiguration(new SeriesConfig());
modelBuilder.ApplyConfiguration(new SeriesBookConfig()); modelBuilder.ApplyConfiguration(new SeriesBookConfig());
modelBuilder.ApplyConfiguration(new CategoryConfig()); modelBuilder.ApplyConfiguration(new CategoryConfig());
modelBuilder.ApplyConfiguration(new CategoryLadderConfig());
modelBuilder.ApplyConfiguration(new BookCategoryConfig());
// views are now supported via "keyless entity types" (instead of "entity types" or the prev "query types"): // views are now supported via "keyless entity types" (instead of "entity types" or the prev "query types"):
// https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types // https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types

View File

@ -0,0 +1,479 @@
// <auto-generated />
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("20230717220642_AddCategoriesList")]
partial class AddCategoriesList
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
b.Property<int>("_categoriesCategoryId")
.HasColumnType("INTEGER");
b.Property<int>("_categoryLaddersCategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId");
b.HasIndex("_categoryLaddersCategoryLadderId");
b.ToTable("CategoryCategoryLadder");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("ContentType")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<string>("Language")
.HasColumnType("TEXT");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("PictureLarge")
.HasColumnType("TEXT");
b.Property<string>("Subtitle")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<long>("_audioFormat")
.HasColumnType("INTEGER");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookCategory", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("CategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("BookId", "CategoryLadderId");
b.HasIndex("BookId");
b.HasIndex("CategoryLadderId");
b.ToTable("BookCategory");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Property<int>("CategoryLadderId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.HasKey("CategoryLadderId");
b.ToTable("CategoryLadders");
b.HasData(
new
{
CategoryLadderId = -1
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("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<int>("BookId")
.HasColumnType("INTEGER");
b.Property<bool>("AbsentFromLastScan")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.HasKey("BookId");
b.ToTable("LibraryBooks");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("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<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<int>("BookStatus")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("LastDownloaded")
.HasColumnType("TEXT");
b1.Property<string>("LastDownloadedVersion")
.HasColumnType("TEXT");
b1.Property<int?>("PdfStatus")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem", (string)null);
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("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
}
}
}

View File

@ -0,0 +1,176 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DataLayer.Migrations
{
/// <inheritdoc />
public partial class AddCategoriesList : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books");
migrationBuilder.DropForeignKey(
name: "FK_Categories_Categories_ParentCategoryCategoryId",
table: "Categories");
migrationBuilder.DropIndex(
name: "IX_Categories_ParentCategoryCategoryId",
table: "Categories");
migrationBuilder.DropIndex(
name: "IX_Books_CategoryId",
table: "Books");
migrationBuilder.DropColumn(
name: "ParentCategoryCategoryId",
table: "Categories");
migrationBuilder.DropColumn(
name: "CategoryId",
table: "Books");
migrationBuilder.CreateTable(
name: "CategoryLadders",
columns: table => new
{
CategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true)
},
constraints: table =>
{
table.PrimaryKey("PK_CategoryLadders", x => x.CategoryLadderId);
});
migrationBuilder.CreateTable(
name: "BookCategory",
columns: table => new
{
BookId = table.Column<int>(type: "INTEGER", nullable: false),
CategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BookCategory", x => new { x.BookId, x.CategoryLadderId });
table.ForeignKey(
name: "FK_BookCategory_Books_BookId",
column: x => x.BookId,
principalTable: "Books",
principalColumn: "BookId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BookCategory_CategoryLadders_CategoryLadderId",
column: x => x.CategoryLadderId,
principalTable: "CategoryLadders",
principalColumn: "CategoryLadderId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "CategoryCategoryLadder",
columns: table => new
{
_categoriesCategoryId = table.Column<int>(type: "INTEGER", nullable: false),
_categoryLaddersCategoryLadderId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CategoryCategoryLadder", x => new { x._categoriesCategoryId, x._categoryLaddersCategoryLadderId });
table.ForeignKey(
name: "FK_CategoryCategoryLadder_Categories__categoriesCategoryId",
column: x => x._categoriesCategoryId,
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_CategoryCategoryLadder_CategoryLadders__categoryLaddersCategoryLadderId",
column: x => x._categoryLaddersCategoryLadderId,
principalTable: "CategoryLadders",
principalColumn: "CategoryLadderId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
table: "CategoryLadders",
column: "CategoryLadderId",
value: -1);
migrationBuilder.CreateIndex(
name: "IX_BookCategory_BookId",
table: "BookCategory",
column: "BookId");
migrationBuilder.CreateIndex(
name: "IX_BookCategory_CategoryLadderId",
table: "BookCategory",
column: "CategoryLadderId");
migrationBuilder.CreateIndex(
name: "IX_CategoryCategoryLadder__categoryLaddersCategoryLadderId",
table: "CategoryCategoryLadder",
column: "_categoryLaddersCategoryLadderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BookCategory");
migrationBuilder.DropTable(
name: "CategoryCategoryLadder");
migrationBuilder.DropTable(
name: "CategoryLadders");
migrationBuilder.AddColumn<int>(
name: "ParentCategoryCategoryId",
table: "Categories",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "CategoryId",
table: "Books",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.UpdateData(
table: "Categories",
keyColumn: "CategoryId",
keyValue: -1,
column: "ParentCategoryCategoryId",
value: null);
migrationBuilder.CreateIndex(
name: "IX_Categories_ParentCategoryCategoryId",
table: "Categories",
column: "ParentCategoryCategoryId");
migrationBuilder.CreateIndex(
name: "IX_Books_CategoryId",
table: "Books",
column: "CategoryId");
migrationBuilder.AddForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Categories_Categories_ParentCategoryCategoryId",
table: "Categories",
column: "ParentCategoryCategoryId",
principalTable: "Categories",
principalColumn: "CategoryId");
}
}
}

View File

@ -17,6 +17,21 @@ namespace DataLayer.Migrations
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
b.Property<int>("_categoriesCategoryId")
.HasColumnType("INTEGER");
b.Property<int>("_categoryLaddersCategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId");
b.HasIndex("_categoryLaddersCategoryLadderId");
b.ToTable("CategoryCategoryLadder");
});
modelBuilder.Entity("DataLayer.Book", b => modelBuilder.Entity("DataLayer.Book", b =>
{ {
b.Property<int>("BookId") b.Property<int>("BookId")
@ -26,9 +41,6 @@ namespace DataLayer.Migrations
b.Property<string>("AudibleProductId") b.Property<string>("AudibleProductId")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<int>("ContentType") b.Property<int>("ContentType")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -69,11 +81,26 @@ namespace DataLayer.Migrations
b.HasIndex("AudibleProductId"); b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books"); b.ToTable("Books");
}); });
modelBuilder.Entity("DataLayer.BookCategory", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("CategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("BookId", "CategoryLadderId");
b.HasIndex("BookId");
b.HasIndex("CategoryLadderId");
b.ToTable("BookCategory");
});
modelBuilder.Entity("DataLayer.BookContributor", b => modelBuilder.Entity("DataLayer.BookContributor", b =>
{ {
b.Property<int>("BookId") b.Property<int>("BookId")
@ -109,15 +136,10 @@ namespace DataLayer.Migrations
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("INTEGER");
b.HasKey("CategoryId"); b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId"); b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories"); b.ToTable("Categories");
b.HasData( b.HasData(
@ -129,6 +151,23 @@ namespace DataLayer.Migrations
}); });
}); });
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Property<int>("CategoryLadderId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.HasKey("CategoryLadderId");
b.ToTable("CategoryLadders");
b.HasData(
new
{
CategoryLadderId = -1
});
});
modelBuilder.Entity("DataLayer.Contributor", b => modelBuilder.Entity("DataLayer.Contributor", b =>
{ {
b.Property<int>("ContributorId") b.Property<int>("ContributorId")
@ -216,14 +255,23 @@ namespace DataLayer.Migrations
b.ToTable("SeriesBook"); b.ToTable("SeriesBook");
}); });
modelBuilder.Entity("DataLayer.Book", b => modelBuilder.Entity("CategoryCategoryLadder", b =>
{ {
b.HasOne("DataLayer.Category", "Category") b.HasOne("DataLayer.Category", null)
.WithMany() .WithMany()
.HasForeignKey("CategoryId") .HasForeignKey("_categoriesCategoryId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("DataLayer.CategoryLadder", null)
.WithMany()
.HasForeignKey("_categoryLaddersCategoryLadderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.OwnsOne("DataLayer.Rating", "Rating", b1 => b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{ {
b1.Property<int>("BookId") b1.Property<int>("BookId")
@ -324,8 +372,6 @@ namespace DataLayer.Migrations
b1.Navigation("Rating"); b1.Navigation("Rating");
}); });
b.Navigation("Category");
b.Navigation("Rating"); b.Navigation("Rating");
b.Navigation("Supplements"); b.Navigation("Supplements");
@ -333,6 +379,25 @@ namespace DataLayer.Migrations
b.Navigation("UserDefinedItem"); 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 => modelBuilder.Entity("DataLayer.BookContributor", b =>
{ {
b.HasOne("DataLayer.Book", "Book") b.HasOne("DataLayer.Book", "Book")
@ -352,15 +417,6 @@ namespace DataLayer.Migrations
b.Navigation("Contributor"); b.Navigation("Contributor");
}); });
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
b.Navigation("ParentCategory");
});
modelBuilder.Entity("DataLayer.LibraryBook", b => modelBuilder.Entity("DataLayer.LibraryBook", b =>
{ {
b.HasOne("DataLayer.Book", "Book") b.HasOne("DataLayer.Book", "Book")
@ -393,11 +449,18 @@ namespace DataLayer.Migrations
modelBuilder.Entity("DataLayer.Book", b => modelBuilder.Entity("DataLayer.Book", b =>
{ {
b.Navigation("CategoriesLink");
b.Navigation("ContributorsLink"); b.Navigation("ContributorsLink");
b.Navigation("SeriesLink"); b.Navigation("SeriesLink");
}); });
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Contributor", b => modelBuilder.Entity("DataLayer.Contributor", b =>
{ {
b.Navigation("BooksLink"); b.Navigation("BooksLink");

View File

@ -34,7 +34,7 @@ namespace DataLayer
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements // owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
.Include(b => b.SeriesLink).ThenInclude(sb => sb.Series) .Include(b => b.SeriesLink).ThenInclude(sb => sb.Series)
.Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor) .Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(b => b.Category).ThenInclude(c => c.ParentCategory); .Include(b => b.CategoriesLink).ThenInclude(c => c.CategoryLadder).ThenInclude(c => c._categories);
public static bool IsProduct(this Book book) public static bool IsProduct(this Book book)
=> book.ContentType is not ContentType.Episode and not ContentType.Parent; => book.ContentType is not ContentType.Episode and not ContentType.Parent;

View File

@ -55,7 +55,7 @@ namespace DataLayer
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements // owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
.Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series) .Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series)
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor) .Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory); .Include(le => le.Book).ThenInclude(b => b.CategoriesLink).ThenInclude(c => c.CategoryLadder).ThenInclude(c => c._categories);
public static IEnumerable<LibraryBook> ParentedEpisodes(this IEnumerable<LibraryBook> libraryBooks) public static IEnumerable<LibraryBook> ParentedEpisodes(this IEnumerable<LibraryBook> libraryBooks)
=> libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).SelectMany(libraryBooks.FindChildren); => libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).SelectMany(libraryBooks.FindChildren);

View File

@ -99,20 +99,6 @@ namespace DtoImporterService
.Select(n => contributorImporter.Cache[n.Name]) .Select(n => contributorImporter.Cache[n.Name])
.ToList(); .ToList();
// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
// absence of categories is also possible
// CATEGORY HACK: only use the 1st 2 categories
// after we support full arbitrary-depth category trees and multiple categories per book, the real impl will be something like this
// var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";
var lastCategory
= item.Categories.Length == 0 ? ""
: item.Categories.Length == 1 ? item.Categories[0].CategoryId
// 2+
: item.Categories[1].CategoryId;
var category = categoryImporter.Cache[lastCategory];
Book book; Book book;
try try
{ {
@ -125,7 +111,6 @@ namespace DtoImporterService
contentType, contentType,
authors, authors,
narrators, narrators,
category,
importItem.LocaleName) importItem.LocaleName)
).Entity; ).Entity;
Cache.Add(book.AudibleProductId, book); Cache.Add(book.AudibleProductId, book);
@ -140,7 +125,6 @@ namespace DtoImporterService
contentType, contentType,
QtyAuthors = authors?.Count, QtyAuthors = authors?.Count,
QtyNarrators = narrators?.Count, QtyNarrators = narrators?.Count,
Category = category?.Name,
importItem.LocaleName importItem.LocaleName
}); });
throw; throw;
@ -201,6 +185,17 @@ namespace DtoImporterService
book.UpsertSeries(series, seriesEntry.Sequence); book.UpsertSeries(series, seriesEntry.Sequence);
} }
} }
if (item.CategoryLadders is not null)
{
foreach (var ladder in item.CategoryLadders.Select(cl => cl.Ladder).Where(l => l?.Length > 0))
{
var categoryIds = ladder.Select(l => l.CategoryId).ToList();
var cata = categoryImporter.LadderCache.Single(c => c.Equals(categoryIds));
book.UpsertCategories(cata);
}
}
} }
private static DataLayer.ContentType GetContentType(Item item) private static DataLayer.ContentType GetContentType(Item item)

View File

@ -12,7 +12,8 @@ namespace DtoImporterService
{ {
protected override IValidator Validator => new CategoryValidator(); protected override IValidator Validator => new CategoryValidator();
public Dictionary<string, Category> Cache { get; private set; } = new(); private Dictionary<string, Category> Cache { get; set; } = new();
public HashSet<DataLayer.CategoryLadder> LadderCache { get; private set; } = new();
public CategoryImporter(LibationContext context) : base(context) { } public CategoryImporter(LibationContext context) : base(context) { }
@ -30,44 +31,39 @@ namespace DtoImporterService
loadLocal_categories(categoryIds); loadLocal_categories(categoryIds);
// upsert // upsert
var categoryPairs = importItems var categoryLadders = importItems
.Select(i => i.DtoItem) .SelectMany(i => i.DtoItem.CategoryLadders)
.GetCategoryPairsDistinct() .Select(cl => cl.Ladder)
.Where(l => l?.Length > 0)
.ToList(); .ToList();
var qtyNew = upsertCategories(categoryPairs);
var qtyNew = upsertCategories(categoryLadders);
return qtyNew; return qtyNew;
} }
private void loadLocal_categories(List<string> categoryIds) private void loadLocal_categories(List<string> categoryIds)
{ {
// must include default/empty/missing
categoryIds.Add(Category.GetEmpty().AudibleCategoryId);
// load existing => local // load existing => local
Cache = DbContext.Categories Cache = DbContext.Categories
.Where(c => categoryIds.Contains(c.AudibleCategoryId)) .Where(c => categoryIds.Contains(c.AudibleCategoryId))
.ToDictionarySafe(c => c.AudibleCategoryId); .ToDictionarySafe(c => c.AudibleCategoryId);
LadderCache = DbContext.CategoryLadders.ToHashSet();
} }
// only use after loading contributors => local // only use after loading contributors => local
private int upsertCategories(List<Ladder[]> categoryPairs) private int upsertCategories(List<Ladder[]> ladders)
{ {
var qtyNew = 0; var qtyNew = 0;
foreach (var pair in categoryPairs) foreach (var ladder in ladders)
{ {
for (var i = 0; i < pair.Length; i++) var categories = new List<Category>(ladder.Length);
for (var i = 0; i < ladder.Length; i++)
{ {
// CATEGORY HACK: not yet supported: depth beyond 0 and 1 var id = ladder[i].CategoryId;
if (i > 1) var name = ladder[i].CategoryName;
break;
var id = pair[i].CategoryId;
var name = pair[i].CategoryName;
Category parentCategory = null;
if (i == 1)
Cache.TryGetValue(pair[0].CategoryId, out parentCategory);
if (!Cache.TryGetValue(id, out var category)) if (!Cache.TryGetValue(id, out var category))
{ {
@ -75,13 +71,37 @@ namespace DtoImporterService
qtyNew++; qtyNew++;
} }
category.UpdateParentCategory(parentCategory); categories.Add(category);
}
var categoryLadder = new DataLayer.CategoryLadder(categories);
if (!LadderCache.Contains(categoryLadder))
{
addCategoryLadder(categoryLadder);
qtyNew++;
} }
} }
return qtyNew; return qtyNew;
} }
private DataLayer.CategoryLadder addCategoryLadder(DataLayer.CategoryLadder categoryList)
{
try
{
var entityEntry = DbContext.CategoryLadders.Add(categoryList);
var entity = entityEntry.Entity;
LadderCache.Add(entity);
return entity;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding category ladder. {@DebugInfo}", categoryList);
throw;
}
}
private Category addCategory(string id, string name) private Category addCategory(string id, string name)
{ {
try try

View File

@ -140,7 +140,7 @@ namespace FileLiberator
new AaxcDownloadSingleConverter(outFileName, cacheDir, dlOptions); new AaxcDownloadSingleConverter(outFileName, cacheDir, dlOptions);
if (config.AllowLibationFixup) if (config.AllowLibationFixup)
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames()); converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.LowestCategoryNames());
abDownloader = converter; abDownloader = converter;
} }

View File

@ -115,7 +115,7 @@ Author(s): {Book.AuthorNames()}
Narrator(s): {Book.NarratorNames()} Narrator(s): {Book.NarratorNames()}
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")} Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
Audio Bitrate: {Book.AudioFormat} Audio Bitrate: {Book.AudioFormat}
Category: {string.Join(" > ", Book.CategoriesNames())} Category: {string.Join(", ", Book.LowestCategoryNames())}
Purchase Date: {libraryBook.DateAdded:d} Purchase Date: {libraryBook.DateAdded:d}
Language: {Book.Language} Language: {Book.Language}
Audible ID: {Book.AudibleProductId} Audible ID: {Book.AudibleProductId}

View File

@ -41,7 +41,7 @@ namespace LibationSearchEngine
{ FieldType.String, lb => lb.Book.Publisher, nameof(Book.Publisher) }, { FieldType.String, lb => lb.Book.Publisher, nameof(Book.Publisher) },
{ FieldType.String, lb => lb.Book.SeriesNames(), "SeriesNames", "Narrator", "Series" }, { FieldType.String, lb => lb.Book.SeriesNames(), "SeriesNames", "Narrator", "Series" },
{ FieldType.String, lb => string.Join(", ", lb.Book.SeriesLink.Select(s => s.Series.AudibleSeriesId)), "SeriesId" }, { FieldType.String, lb => string.Join(", ", lb.Book.SeriesLink.Select(s => s.Series.AudibleSeriesId)), "SeriesId" },
{ FieldType.String, lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()), nameof(Book.Category), "Categories", "CategoriesId", "CategoryId", "CategoriesNames" }, { FieldType.String, lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()), "Category", "Categories", "CategoriesId", "CategoryId", "CategoriesNames" },
{ FieldType.String, lb => lb.Book.UserDefinedItem.Tags, TAGS.FirstCharToUpper() }, { FieldType.String, lb => lb.Book.UserDefinedItem.Tags, TAGS.FirstCharToUpper() },
{ FieldType.String, lb => lb.Book.Locale, "Locale", "Region" }, { FieldType.String, lb => lb.Book.Locale, "Locale", "Region" },
{ FieldType.String, lb => lb.Account, "Account", "Email" }, { FieldType.String, lb => lb.Account, "Account", "Email" },

View File

@ -116,7 +116,7 @@ namespace LibationUiBase.GridView
ProductRating = Book.Rating ?? new Rating(0, 0, 0); ProductRating = Book.Rating ?? new Rating(0, 0, 0);
Authors = Book.AuthorNames(); Authors = Book.AuthorNames();
Narrators = Book.NarratorNames(); Narrators = Book.NarratorNames();
Category = string.Join(" > ", Book.CategoriesNames()); Category = string.Join(", ", Book.LowestCategoryNames());
Misc = GetMiscDisplay(libraryBook); Misc = GetMiscDisplay(libraryBook);
LastDownload = new(Book.UserDefinedItem); LastDownload = new(Book.UserDefinedItem);
LongDescription = GetDescriptionDisplay(Book); LongDescription = GetDescriptionDisplay(Book);

View File

@ -50,7 +50,7 @@ Author(s): {Book.AuthorNames()}
Narrator(s): {Book.NarratorNames()} Narrator(s): {Book.NarratorNames()}
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")} Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
Audio Bitrate: {Book.AudioFormat} Audio Bitrate: {Book.AudioFormat}
Category: {string.Join(" > ", Book.CategoriesNames())} Category: {string.Join(", ", Book.LowestCategoryNames())}
Purchase Date: {_libraryBook.DateAdded:d} Purchase Date: {_libraryBook.DateAdded:d}
Language: {Book.Language} Language: {Book.Language}
Audible ID: {Book.AudibleProductId} Audible ID: {Book.AudibleProductId}