Add episodes content type to Books in db

This commit is contained in:
Robert McRackan 2021-09-01 16:51:59 -04:00
parent e8423341ef
commit ad87f1851e
10 changed files with 480 additions and 13 deletions

View File

@ -89,11 +89,14 @@ namespace ApplicationServices
[Name("My Libation Tags")] [Name("My Libation Tags")]
public string MyLibationTags { get; set; } public string MyLibationTags { get; set; }
[Name("Book Liberation Status")] [Name("Book Liberated Status")]
public string BookStatus { get; set; } public string BookStatus { get; set; }
[Name("PDF Liberation Status")] [Name("PDF Liberated Status")]
public string PdfStatus { get; set; } public string PdfStatus { get; set; }
[Name("Content Type")]
public string ContentType { get; set; }
} }
public static class LibToDtos public static class LibToDtos
{ {
@ -124,7 +127,8 @@ namespace ApplicationServices
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating, MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
MyLibationTags = a.Book.UserDefinedItem.Tags, MyLibationTags = a.Book.UserDefinedItem.Tags,
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(), BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString() PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
ContentType = a.Book.ContentType.ToString()
}).ToList(); }).ToList();
} }
public static class LibraryExporter public static class LibraryExporter
@ -197,7 +201,8 @@ namespace ApplicationServices
nameof (ExportDto.MyRatingStory), nameof (ExportDto.MyRatingStory),
nameof (ExportDto.MyLibationTags), nameof (ExportDto.MyLibationTags),
nameof (ExportDto.BookStatus), nameof (ExportDto.BookStatus),
nameof (ExportDto.PdfStatus) nameof (ExportDto.PdfStatus),
nameof (ExportDto.ContentType)
}; };
var col = 0; var col = 0;
foreach (var c in columns) foreach (var c in columns)
@ -261,6 +266,7 @@ namespace ApplicationServices
row.CreateCell(col++).SetCellValue(dto.MyLibationTags); row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
row.CreateCell(col++).SetCellValue(dto.BookStatus); row.CreateCell(col++).SetCellValue(dto.BookStatus);
row.CreateCell(col++).SetCellValue(dto.PdfStatus); row.CreateCell(col++).SetCellValue(dto.PdfStatus);
row.CreateCell(col++).SetCellValue(dto.ContentType);
rowIndex++; rowIndex++;
} }

View File

@ -15,6 +15,10 @@ namespace DataLayer
Id = id; Id = id;
} }
} }
// enum will be easier than bool to extend later
public enum ContentType { Unknown = 0, Product = 1, Episode = 2 }
public class Book public class Book
{ {
// implementation detail. set by db only. only used by data layer // 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 Title { get; private set; }
public string Description { get; private set; } public string Description { get; private set; }
public int LengthInMinutes { 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 // immutable-ish. should be immutable. mutability is necessary for v3 => v4 upgrades
public string Locale { get; private set; } public string Locale { get; private set; }
@ -82,6 +87,7 @@ namespace DataLayer
string title, string title,
string description, string description,
int lengthInMinutes, int lengthInMinutes,
ContentType contentType,
IEnumerable<Contributor> authors, IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators, IEnumerable<Contributor> narrators,
Category category, string localeName) Category category, string localeName)
@ -109,6 +115,7 @@ namespace DataLayer
Title = title.Trim(); Title = title.Trim();
Description = description.Trim(); Description = description.Trim();
LengthInMinutes = lengthInMinutes; LengthInMinutes = lengthInMinutes;
ContentType = contentType;
// assigns with biz logic // assigns with biz logic
ReplaceAuthors(authors); ReplaceAuthors(authors);

View File

@ -18,7 +18,6 @@ namespace DataLayer
/// <summary>Application-state only. Not a valid persistence state.</summary> /// <summary>Application-state only. Not a valid persistence state.</summary>
PartialDownload = 0x1000 PartialDownload = 0x1000
} }
public class UserDefinedItem public class UserDefinedItem

View File

@ -0,0 +1,390 @@
// <auto-generated />
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<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
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<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
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.Property<int?>("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<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<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.ToTable("Library");
});
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<float?>("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<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<int?>("PdfStatus")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
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("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
}
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class BookIsEpisode : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ContentType",
table: "Books",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ContentType",
table: "Books");
}
}
}

View File

@ -14,7 +14,7 @@ namespace DataLayer.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "5.0.5"); .HasAnnotation("ProductVersion", "5.0.9");
modelBuilder.Entity("DataLayer.Book", b => modelBuilder.Entity("DataLayer.Book", b =>
{ {
@ -28,6 +28,9 @@ namespace DataLayer.Migrations
b.Property<int>("CategoryId") b.Property<int>("CategoryId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("ContentType")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished") b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View File

@ -67,6 +67,8 @@ namespace DtoImporterService
{ {
var item = importItem.DtoItem; var item = importItem.DtoItem;
var contentType = item.IsEpisodes ? DataLayer.ContentType.Episode : DataLayer.ContentType.Product;
// absence of authors is very rare, but possible // absence of authors is very rare, but possible
if (!item.Authors?.Any() ?? true) if (!item.Authors?.Any() ?? true)
item.Authors = new[] { new Person { Name = "", Asin = null } }; item.Authors = new[] { new Person { Name = "", Asin = null } };
@ -105,6 +107,7 @@ namespace DtoImporterService
item.TitleWithSubtitle, item.TitleWithSubtitle,
item.Description, item.Description,
item.LengthInMinutes, item.LengthInMinutes,
contentType,
authors, authors,
narrators, narrators,
category, category,

View File

@ -59,6 +59,10 @@ namespace InternalUtilities
private static async Task<List<Item>> getItemsAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups) private static async Task<List<Item>> getItemsAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups)
{ {
var items = await api.GetAllLibraryItemsAsync(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); await manageEpisodesAsync(api, items);
@ -82,15 +86,23 @@ namespace InternalUtilities
{ {
// get parents // get parents
var parents = items.Where(i => i.IsEpisodes).ToList(); 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()) if (!parents.Any())
return; 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); items.RemoveAll(i => i.IsEpisodes);
// add children // add children
var children = await getEpisodesAsync(api, parents); var children = await getEpisodesAsync(api, parents);
Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found");
items.AddRange(children); items.AddRange(children);
} }
catch (Exception ex) catch (Exception ex)
@ -101,17 +113,36 @@ namespace InternalUtilities
private static async Task<List<Item>> getEpisodesAsync(Api api, List<Item> parents) private static async Task<List<Item>> getEpisodesAsync(Api api, List<Item> parents)
{ {
Serilog.Log.Logger.Information($"{parents.Count} series of episodes/podcasts found");
var results = new List<Item>(); var results = new List<Item>();
foreach (var parent in parents) foreach (var parent in parents)
{ {
var children = await getEpisodeChildrenAsync(api, parent); var children = await getEpisodeChildrenAsync(api, parent);
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
foreach (var child in children) foreach (var child in children)
{
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
child.PurchaseDate = parent.PurchaseDate; 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); results.AddRange(children);
} }
@ -141,6 +172,10 @@ namespace InternalUtilities
try try
{ {
childrenBatch = await api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS); 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) catch (Exception ex)
{ {
@ -178,7 +213,7 @@ namespace InternalUtilities
return results; return results;
} }
#endregion #endregion
private static List<IValidator> getValidators() private static List<IValidator> getValidators()
{ {

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AudibleApi" Version="1.1.0.1" /> <PackageReference Include="AudibleApi" Version="1.1.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> --> <!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>5.6.0.2</Version> <Version>5.6.0.8</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">