using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AudibleApi; using AudibleApi.Common; using Dinah.Core; using Polly; using Polly.Retry; namespace InternalUtilities { public static class AudibleApiActions { /// USE THIS from within Libation. It wraps the call with correct JSONPath public static Task GetApiAsync(string username, string localeName, ILoginCallback loginCallback = null) { Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new { Username = username.ToMask(), LocaleName = localeName, }); return EzApiCreator.GetApiAsync( Localization.Get(localeName), AudibleApiStorage.AccountsSettingsFile, AudibleApiStorage.GetIdentityTokensJsonPath(username, localeName), loginCallback); } /// USE THIS from within Libation. It wraps the call with correct JSONPath public static Task GetApiAsync(ILoginCallback loginCallback, Account account) { Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new { Account = account?.MaskedLogEntry ?? "[null]", LocaleName = account?.Locale?.Name }); return EzApiCreator.GetApiAsync( account.Locale, AudibleApiStorage.AccountsSettingsFile, account.GetIdentityTokensJsonPath(), loginCallback); } private static AsyncRetryPolicy policy { get; } = Policy.Handle() // 2 retries == 3 total .RetryAsync(2); public static Task> GetLibraryValidatedAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS) { // bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or tokens are refreshed // worse, this 1st dummy call doesn't seem to help: // var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS }); // i don't want to incur the cost of making a full dummy call every time because it fails sometimes return policy.ExecuteAsync(() => getItemsAsync(api, responseGroups)); } private static async Task> getItemsAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups) { var items = new List(); #if DEBUG //// this will not work for multi accounts //var library_json = "library.json"; //if (System.IO.File.Exists(library_json)) //{ // items = AudibleApi.Common.Converter.FromJson>(System.IO.File.ReadAllText(library_json)); //} #endif if (!items.Any()) items = await api.GetAllLibraryItemsAsync(responseGroups); #if DEBUG //System.IO.File.WriteAllText("library.json", AudibleApi.Common.Converter.ToJson(items)); #endif await manageEpisodesAsync(api, items); var validators = new List(); validators.AddRange(getValidators()); foreach (var v in validators) { var exceptions = v.Validate(items); if (exceptions != null && exceptions.Any()) throw new AggregateException(exceptions); } return items; } #region episodes and podcasts private static async Task manageEpisodesAsync(Api api, List items) { // add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import try { // 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; 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) { Serilog.Log.Logger.Error(ex, "Error adding podcasts and episodes"); } } private static async Task> getEpisodesAsync(Api api, List parents) { var results = new List(); foreach (var parent in parents) { var children = await getEpisodeChildrenAsync(api, parent); 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); } return results; } private static async Task> getEpisodeChildrenAsync(Api api, Item parent) { var childrenIds = parent.Relationships .Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode) .Select(r => r.Asin) .ToList(); // fetch children in batches const int batchSize = 20; var results = new List(); for (var i = 1; ; i++) { var idBatch = childrenIds.Skip((i - 1) * batchSize).Take(batchSize).ToList(); if (!idBatch.Any()) break; List childrenBatch; 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) { Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new { ParentId = parent.Asin, ParentTitle = parent.Title, BatchNumber = i, ChildIdBatch = idBatch }); throw; } Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results"); // the service returned no results. probably indicates an error. stop running batches if (!childrenBatch.Any()) break; results.AddRange(childrenBatch); } Serilog.Log.Logger.Debug("Parent episodes/podcasts series. Children found. {@DebugInfo}", new { ParentId = parent.Asin, ParentTitle = parent.Title, ChildCount = childrenIds.Count }); if (childrenIds.Count != results.Count) { var ex = new ApplicationException($"Mis-match: Children defined by parent={childrenIds.Count}. Children returned by batches={results.Count}"); Serilog.Log.Logger.Error(ex, "Quantity of series episodes defined by parent does not match quantity returned by batch fetching."); throw ex; } return results; } #endregion private static List getValidators() { var type = typeof(IValidator); var types = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(s => s.GetTypes()) .Where(p => type.IsAssignableFrom(p) && !p.IsInterface); return types.Select(t => Activator.CreateInstance(t) as IValidator).ToList(); } } }