diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index 259b3526..e0a6506f 100644 --- a/Source/AaxDecrypter/NetworkFileStream.cs +++ b/Source/AaxDecrypter/NetworkFileStream.cs @@ -217,40 +217,47 @@ namespace AaxDecrypter { var downloadPosition = WritePosition; var nextFlush = downloadPosition + DATA_FLUSH_SZ; - var buff = new byte[DOWNLOAD_BUFF_SZ]; - do + + try { - var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); - _writeFile.Write(buff, 0, bytesRead); - - downloadPosition += bytesRead; - - if (downloadPosition > nextFlush) + do { - _writeFile.Flush(); - WritePosition = downloadPosition; - Update(); - nextFlush = downloadPosition + DATA_FLUSH_SZ; - downloadedPiece.Set(); - } + var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); + _writeFile.Write(buff, 0, bytesRead); - } while (downloadPosition < ContentLength && !IsCancelled); + downloadPosition += bytesRead; - _writeFile.Close(); - _networkStream.Close(); - WritePosition = downloadPosition; - Update(); + if (downloadPosition > nextFlush) + { + _writeFile.Flush(); + WritePosition = downloadPosition; + Update(); + nextFlush = downloadPosition + DATA_FLUSH_SZ; + downloadedPiece.Set(); + } - downloadedPiece.Set(); - downloadEnded.Set(); + } while (downloadPosition < ContentLength && !IsCancelled); - if (!IsCancelled && WritePosition < ContentLength) - throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10})."); + _writeFile.Close(); + _networkStream.Close(); + WritePosition = downloadPosition; + Update(); - if (WritePosition > ContentLength) - throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10})."); + downloadedPiece.Set(); + downloadEnded.Set(); + if (!IsCancelled && WritePosition < ContentLength) + throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10})."); + + if (WritePosition > ContentLength) + throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10})."); + } + catch (Exception ex) + { + Serilog.Log.Error(ex, "An error was encountered while downloading {Uri}", Uri); + IsCancelled = true; + } } #endregion diff --git a/Source/AudibleUtilities/ApiExtended.cs b/Source/AudibleUtilities/ApiExtended.cs index 5a00c9a7..11ff3c39 100644 --- a/Source/AudibleUtilities/ApiExtended.cs +++ b/Source/AudibleUtilities/ApiExtended.cs @@ -118,31 +118,37 @@ namespace AudibleUtilities private async Task> getItemsAsync(LibraryOptions libraryOptions, bool importEpisodes) { var items = new List(); -#if DEBUG -//// this will not work for multi accounts -//var library_json = "library.json"; -//library_json = System.IO.Path.GetFullPath(library_json); -//if (System.IO.File.Exists(library_json)) -//{ -// items = AudibleApi.Common.Converter.FromJson>(System.IO.File.ReadAllText(library_json)); -//} -#endif - Serilog.Log.Logger.Debug("Begin initial library scan"); + Serilog.Log.Logger.Debug("Begin library scan"); - if (!items.Any()) - items = await Api.GetAllLibraryItemsAsync(libraryOptions); + List>> getChildEpisodesTasks = new(); - Serilog.Log.Logger.Debug("Initial library scan complete. Begin episode scan"); + int count = 0; - await manageEpisodesAsync(items, importEpisodes); + await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions)) + { + if (item.IsEpisodes && importEpisodes) + { + //Get child episodes asynchronously and await all at the end + getChildEpisodesTasks.Add(getChildEpisodesAsync(item)); + } + else if (!item.IsEpisodes) + items.Add(item); - Serilog.Log.Logger.Debug("Episode scan complete"); + count++; + } + + Serilog.Log.Logger.Debug("Library scan complete. Found {count} books. Waiting on episode scans to complete", count); + + //await and add all episides from all parents + foreach (var epList in await Task.WhenAll(getChildEpisodesTasks)) + items.AddRange(epList); + + Serilog.Log.Logger.Debug("Scan complete"); #if DEBUG //System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items)); #endif - var validators = new List(); validators.AddRange(getValidators()); foreach (var v in validators) @@ -156,63 +162,25 @@ namespace AudibleUtilities } #region episodes and podcasts - private async Task manageEpisodesAsync(List items, bool importEpisodes) + + private async Task> getChildEpisodesAsync(Item parent) { - // add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import - try + Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent); + + var children = await getEpisodeChildrenAsync(parent); + + // actual individual episode, not the parent of a series. + // for now I'm keeping it inside this method since it fits the work flow, incl. importEpisodes logic + if (!children.Any()) + return new List() { parent }; + + foreach (var child in children) { - // 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 - items.RemoveAll(i => i.IsEpisodes); - - if (importEpisodes) + // 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[] { - // add children - var children = await getEpisodesAsync(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 async Task> getEpisodesAsync(List parents) - { - var results = new List(); - - foreach (var parent in parents) - { - var children = await getEpisodeChildrenAsync(parent); - - // actual individual episode, not the parent of a series. - // for now I'm keeping it inside this method since it fits the work flow, incl. importEpisodes logic - if (!children.Any()) - { - results.Add(parent); - continue; - } - - 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, @@ -220,22 +188,18 @@ namespace AudibleUtilities Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin).Sort.ToString(), Title = parent.TitleWithSubtitle } - }; - // overload (read: abuse) IsEpisodes flag - child.Relationships = new Relationship[] - { + }; + // overload (read: abuse) IsEpisodes flag + child.Relationships = new Relationship[] + { new Relationship { RelationshipToProduct = RelationshipToProduct.Child, RelationshipType = RelationshipType.Episode } - }; - } - - results.AddRange(children); + }; } - - return results; + return children; } private async Task> getEpisodeChildrenAsync(Item parent) @@ -277,7 +241,7 @@ namespace AudibleUtilities throw; } - Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results"); + Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results\t({{parent}})", parent); // the service returned no results. probably indicates an error. stop running batches if (!childrenBatch.Any()) break; @@ -295,7 +259,7 @@ namespace AudibleUtilities 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."); + Serilog.Log.Logger.Error(ex, "{parent} - Quantity of series episodes defined by parent does not match quantity returned by batch fetching.", parent); throw ex; } diff --git a/Source/FileLiberator/AudioDecodable.cs b/Source/FileLiberator/AudioDecodable.cs index 3f230fb7..9b441efb 100644 --- a/Source/FileLiberator/AudioDecodable.cs +++ b/Source/FileLiberator/AudioDecodable.cs @@ -4,7 +4,8 @@ namespace FileLiberator { public abstract class AudioDecodable : Processable { - public event EventHandler> RequestCoverArt; + public delegate byte[] RequestCoverArtHandler(object sender, EventArgs eventArgs); + public event RequestCoverArtHandler RequestCoverArt; public event EventHandler TitleDiscovered; public event EventHandler AuthorsDiscovered; public event EventHandler NarratorsDiscovered; @@ -32,10 +33,10 @@ namespace FileLiberator NarratorsDiscovered?.Invoke(this, narrators); } - protected void OnRequestCoverArt(Action setCoverArtDel) + protected byte[] OnRequestCoverArt() { Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) }); - RequestCoverArt?.Invoke(this, setCoverArtDel); + return RequestCoverArt?.Invoke(this, new()); } protected void OnCoverImageDiscovered(byte[] coverImage) diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 20e55e39..97221782 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -247,7 +247,7 @@ namespace FileLiberator if (e is not null) OnCoverImageDiscovered(e); else if (Configuration.Instance.AllowLibationFixup) - OnRequestCoverArt(abDownloader.SetCoverArt); + abDownloader.SetCoverArt(OnRequestCoverArt()); } /// Move new files to 'Books' directory diff --git a/Source/LibationWinForms/GridView/GridEntry.cs b/Source/LibationWinForms/GridView/GridEntry.cs index 6d7e9cd8..b20c1287 100644 --- a/Source/LibationWinForms/GridView/GridEntry.cs +++ b/Source/LibationWinForms/GridView/GridEntry.cs @@ -100,9 +100,9 @@ namespace LibationWinForms.GridView { #nullable enable public static IEnumerable Series(this IEnumerable gridEntries) - => gridEntries.Where(i => i is SeriesEntry).Cast(); + => gridEntries.OfType(); public static IEnumerable LibraryBooks(this IEnumerable gridEntries) - => gridEntries.Where(i => i is LibraryBookEntry).Cast(); + => gridEntries.OfType(); public static LibraryBookEntry? FindBookByAsin(this IEnumerable gridEntries, string audibleProductID) => gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID); public static SeriesEntry? FindBookSeriesEntry(this IEnumerable gridEntries, IEnumerable matchSeries) diff --git a/Source/LibationWinForms/GridView/LiberateDataGridViewImageButtonColumn.cs b/Source/LibationWinForms/GridView/LiberateDataGridViewImageButtonColumn.cs index 62c56538..1984587d 100644 --- a/Source/LibationWinForms/GridView/LiberateDataGridViewImageButtonColumn.cs +++ b/Source/LibationWinForms/GridView/LiberateDataGridViewImageButtonColumn.cs @@ -30,10 +30,7 @@ namespace LibationWinForms.GridView if (status.IsSeries) { - var imageName = status.Expanded ? "minus" : "plus"; - - var bmp = (Bitmap)Properties.Resources.ResourceManager.GetObject(imageName); - DrawButtonImage(graphics, bmp, cellBounds); + DrawButtonImage(graphics, status.Expanded ? Properties.Resources.minus: Properties.Resources.plus, cellBounds); ToolTipText = status.Expanded ? "Click to Collpase" : "Click to Expand"; } diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs b/Source/LibationWinForms/ProcessQueue/ProcessBook.cs index 9891e68b..0ddf4433 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessBook.cs @@ -60,7 +60,6 @@ namespace LibationWinForms.ProcessQueue private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke(); private Processable NextProcessable() => _currentProcessable = null; private Processable _currentProcessable; - private Func GetCoverArtDelegate; private readonly Queue> Processes = new(); private readonly LogMe Logger; @@ -232,11 +231,14 @@ namespace LibationWinForms.ProcessQueue BookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}"; } - public void AudioDecodable_RequestCoverArt(object sender, Action setCoverArtDelegate) + private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e) { - byte[] coverData = GetCoverArtDelegate(); - setCoverArtDelegate(coverData); + byte[] coverData = PictureStorage + .GetPictureSynchronously( + new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500)); + AudioDecodable_CoverImageDiscovered(this, coverData); + return coverData; } private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt) @@ -273,11 +275,6 @@ namespace LibationWinForms.ProcessQueue Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}"); - GetCoverArtDelegate = () => PictureStorage.GetPictureSynchronously( - new PictureDefinition( - libraryBook.Book.PictureId, - PictureSize._500x500)); - title = libraryBook.Book.Title; authorNames = libraryBook.Book.AuthorNames(); narratorNames = libraryBook.Book.NarratorNames(); @@ -286,7 +283,6 @@ namespace LibationWinForms.ProcessQueue private async void Processable_Completed(object sender, LibraryBook libraryBook) { - Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}"); UnlinkProcessable((Processable)sender);