diff --git a/Source/AudibleUtilities/ApiExtended.cs b/Source/AudibleUtilities/ApiExtended.cs index 2a14de01..ff96a9ab 100644 --- a/Source/AudibleUtilities/ApiExtended.cs +++ b/Source/AudibleUtilities/ApiExtended.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using AudibleApi; using AudibleApi.Common; using Dinah.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Polly; using Polly.Retry; @@ -129,7 +132,7 @@ namespace AudibleUtilities await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions)) { - if (item.IsEpisodes && importEpisodes) + if ((item.IsEpisodes || item.IsSeriesParent) && importEpisodes) { //Get child episodes asynchronously and await all at the end getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item)); @@ -173,16 +176,65 @@ namespace AudibleUtilities { Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent); - var children = await getEpisodeChildrenAsync(parent); + List children; - if (!children.Any()) + if (parent.IsEpisodes) { - //The parent is the only episode in the podcase series, - //so the parent is its own child. - parent.Series = new Series[] { new Series { Asin = parent.Asin, Sequence = RelationshipToProduct.Parent, Title = parent.TitleWithSubtitle } }; - children.Add(parent); - return children; + //The 'parent' is a single episode that was added to the library. + //Get the episode's parent and add it to the database. + + Serilog.Log.Logger.Debug("Supplied Parent is an episode. Beginning parent scan for {parent}", parent); + + children = new() { parent }; + + var parentAsins = parent.Relationships + .Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent) + .Select(p => p.Asin); + + var seriesParents = await Api.GetCatalogProductsAsync(parentAsins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS); + + int numSeriesParents = seriesParents.Count(p => p.IsSeriesParent); + if (numSeriesParents != 1) + { + //There should only ever be 1 top-level parent per episode. If not, log + //and throw so we can figure out what to do about those special cases. + JsonSerializerSettings Settings = new() + { + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + DateParseHandling = DateParseHandling.None, + Converters = + { + new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } + }, + }; + var ex = new ApplicationException($"Found {numSeriesParents} parents for {parent.Asin}"); + Serilog.Log.Logger.Error(ex, $"Episode Product:\r\n{JsonConvert.SerializeObject(parent, Formatting.None, Settings)}"); + throw ex; + } + + var realParent = seriesParents.Single(p => p.IsSeriesParent); + realParent.PurchaseDate = parent.PurchaseDate; + + Serilog.Log.Logger.Debug("Completed parent scan for {parent}", parent); + parent = realParent; } + else + { + children = await getEpisodeChildrenAsync(parent); + if (!children.Any()) + return new(); + } + + //A series parent will always have exactly 1 Series + parent.Series = new Series[] + { + new Series + { + Asin = parent.Asin, + Sequence = "-1", + Title = parent.TitleWithSubtitle + } + }; foreach (var child in children) { @@ -199,17 +251,10 @@ namespace AudibleUtilities Title = parent.TitleWithSubtitle } }; - // overload (read: abuse) IsEpisodes flag - child.Relationships = new Relationship[] - { - new Relationship - { - RelationshipToProduct = RelationshipToProduct.Child, - RelationshipType = RelationshipType.Episode - } - }; } + children.Add(parent); + Serilog.Log.Logger.Debug("Completed episode scan for {parent}", parent); return children; diff --git a/Source/DataLayer/EfClasses/Book.cs b/Source/DataLayer/EfClasses/Book.cs index 804df40a..ef072484 100644 --- a/Source/DataLayer/EfClasses/Book.cs +++ b/Source/DataLayer/EfClasses/Book.cs @@ -16,8 +16,14 @@ namespace DataLayer } } - // enum will be easier than bool to extend later - public enum ContentType { Unknown = 0, Product = 1, Episode = 2 } + // enum will be easier than bool to extend later. + public enum ContentType + { + Unknown = 0, + Product = 1, + Episode = 2, + Parent = 4, + } public class Book { diff --git a/Source/DtoImporterService/BookImporter.cs b/Source/DtoImporterService/BookImporter.cs index bc69f382..297ae659 100644 --- a/Source/DtoImporterService/BookImporter.cs +++ b/Source/DtoImporterService/BookImporter.cs @@ -75,7 +75,7 @@ namespace DtoImporterService { var item = importItem.DtoItem; - var contentType = item.IsEpisodes ? DataLayer.ContentType.Episode : DataLayer.ContentType.Product; + var contentType = GetContentType(item); // absence of authors is very rare, but possible if (!item.Authors?.Any() ?? true) @@ -184,5 +184,15 @@ namespace DtoImporterService } } } + + private static DataLayer.ContentType GetContentType(Item item) + { + if (item.IsEpisodes) + return DataLayer.ContentType.Episode; + else if (item.IsSeriesParent) + return DataLayer.ContentType.Parent; + else + return DataLayer.ContentType.Product; + } } } diff --git a/Source/FileLiberator/Processable.cs b/Source/FileLiberator/Processable.cs index b8b984ff..ef1bb139 100644 --- a/Source/FileLiberator/Processable.cs +++ b/Source/FileLiberator/Processable.cs @@ -29,7 +29,8 @@ namespace FileLiberator public IEnumerable GetValidLibraryBooks(IEnumerable library) => library.Where(libraryBook => Validate(libraryBook) - && (libraryBook.Book.ContentType != ContentType.Episode || LibationFileManager.Configuration.Instance.DownloadEpisodes) + && libraryBook.Book.ContentType != ContentType.Parent + && (libraryBook.Book.ContentType != ContentType.Episode || Configuration.Instance.DownloadEpisodes) ); public async Task ProcessSingleAsync(LibraryBook libraryBook, bool validate)