From 4b7939541a634129ee959f92165a83d4cb3d2b62 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 16 Jul 2025 11:28:37 -0600 Subject: [PATCH] Code cleanup and refactoring for clarity --- .../QueryObjects/LibraryBookQueries.cs | 14 +- Source/LibationAvalonia/MessageBox.cs | 41 ++--- .../ViewModels/ProcessQueueViewModel.cs | 56 ++++-- .../ProcessQueue/ProcessBookViewModelBase.cs | 116 ++++++------ .../ProcessQueue/ProcessQueueViewModelBase.cs | 173 +++++------------- Source/LibationUiBase/TrackedQueue[T].cs | 10 +- Source/LibationWinForms/MessageBoxLib.cs | 3 +- .../ProcessQueue/ProcessBookControl.cs | 29 +-- .../ProcessQueue/ProcessQueueControl.cs | 13 +- .../ProcessQueue/ProcessQueueViewModel.cs | 2 +- 10 files changed, 179 insertions(+), 278 deletions(-) diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs index 91f89995..deca475b 100644 --- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs +++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs @@ -103,13 +103,11 @@ namespace DataLayer ) == true ).ToList(); - public static IEnumerable UnLiberated(this IEnumerable bookList) - => bookList - .Where( - lb => - !lb.AbsentFromLastScan && - (lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload - || lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload) - ); + public static bool NeedsPdfDownload(this LibraryBook libraryBook) + => !libraryBook.AbsentFromLastScan && libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated; + public static bool NeedsBookDownload(this LibraryBook libraryBook) + => !libraryBook.AbsentFromLastScan && libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload; + public static IEnumerable UnLiberated(this IEnumerable bookList) + => bookList.Where(lb => lb.NeedsPdfDownload() || lb.NeedsBookDownload()); } } diff --git a/Source/LibationAvalonia/MessageBox.cs b/Source/LibationAvalonia/MessageBox.cs index 7519606d..ae243330 100644 --- a/Source/LibationAvalonia/MessageBox.cs +++ b/Source/LibationAvalonia/MessageBox.cs @@ -20,41 +20,39 @@ namespace LibationAvalonia public static Task Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) => ShowCoreAsync(null, text, caption, buttons, icon, defaultButton); public static Task Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, bool saveAndRestorePosition = true) - => ShowCoreAsync(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1, saveAndRestorePosition); + => ShowCoreAsync(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1, saveAndRestorePosition); public static Task Show(string text, string caption, MessageBoxButtons buttons) - => ShowCoreAsync(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + => ShowCoreAsync(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); public static Task Show(string text, string caption) - => ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + => ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); public static Task Show(string text) - => ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + => ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); public static Task Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true) - => ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton, saveAndRestorePosition); - + => ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton, saveAndRestorePosition); public static Task Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) - => ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); + => ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); public static Task Show(Window owner, string text, string caption, MessageBoxButtons buttons) - => ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + => ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); public static Task Show(Window owner, string text, string caption) - => ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + => ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); public static Task Show(Window owner, string text) => ShowCoreAsync(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - public static async Task VerboseLoggingWarning_ShowIfTrue() { // when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured if (Serilog.Log.Logger.IsVerboseEnabled()) - await Show(@" -Warning: verbose logging is enabled. + await Show(""" + Warning: verbose logging is enabled. -This should be used for debugging only. It creates many -more logs and debug files, neither of which are as -strictly anonymous. + This should be used for debugging only. It creates many + more logs and debug files, neither of which are as + strictly anonymous. -When you are finished debugging, it's highly recommended -to set your debug MinimumLevel to Information and restart -Libation. -".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning); + When you are finished debugging, it's highly recommended + to set your debug MinimumLevel to Information and restart + Libation. + """, "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning); } /// @@ -94,7 +92,8 @@ Libation. { // for development and debugging, show me what broke! if (System.Diagnostics.Debugger.IsAttached) - throw exception; + //Wrap the exception to preserve its stack trace. + throw new Exception("An unhandled exception was encountered", exception); try { @@ -131,7 +130,6 @@ Libation. tbx.MinWidth = vm.TextBlockMinWidth; tbx.Text = message; - var thisScreen = owner.Screens?.ScreenFromVisual(owner); var maxSize @@ -185,6 +183,5 @@ Libation. return await toDisplay.ShowDialog(owner); } } - } } diff --git a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs index 498dfd47..de329357 100644 --- a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs @@ -5,30 +5,64 @@ using DataLayer; using LibationFileManager; using LibationUiBase.ProcessQueue; using System; +using System.Collections.ObjectModel; #nullable enable namespace LibationAvalonia.ViewModels; +public record LogEntry(DateTime LogDate, string? LogMessage) +{ + public string LogDateString => LogDate.ToShortTimeString(); +} + public class ProcessQueueViewModel : ProcessQueueViewModelBase { - public override void WriteLine(string text) - { - Dispatcher.UIThread.Invoke(() => - LogEntries.Add(new() - { - LogDate = DateTime.Now, - LogMessage = text.Trim() - })); - } - public ProcessQueueViewModel() : base(CreateEmptyList()) { Items = Queue.UnderlyingList as AvaloniaList ?? throw new ArgumentNullException(nameof(Queue.UnderlyingList)); + + SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024; } + private decimal _speedLimit; + public decimal SpeedLimitIncrement { get; private set; } + public ObservableCollection LogEntries { get; } = new(); public AvaloniaList Items { get; } - protected override ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook) + + public decimal SpeedLimit + { + get + { + return _speedLimit; + } + set + { + var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024)); + var config = Configuration.Instance; + config.DownloadSpeedLimit = newValue; + + _speedLimit + = config.DownloadSpeedLimit <= newValue ? value + : value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024 + : 0; + + config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024); + + SpeedLimitIncrement = _speedLimit > 100 ? 10 + : _speedLimit > 10 ? 1 + : _speedLimit > 1 ? 0.1m + : 0.01m; + + RaisePropertyChanged(nameof(SpeedLimitIncrement)); + RaisePropertyChanged(nameof(SpeedLimit)); + } + } + + public override void WriteLine(string text) + => Dispatcher.UIThread.Invoke(() => LogEntries.Add(new(DateTime.Now, text.Trim()))); + + protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook) => new ProcessBookViewModel(libraryBook, Logger); private static AvaloniaList CreateEmptyList() diff --git a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs index 1b535168..ca794be9 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs @@ -42,8 +42,6 @@ public enum ProcessBookStatus /// public abstract class ProcessBookViewModelBase : ReactiveObject { - public event EventHandler? Completed; - private readonly LogMe Logger; public LibraryBook LibraryBook { get; protected set; } @@ -86,12 +84,12 @@ public abstract class ProcessBookViewModelBase : ReactiveObject #endregion - protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke(); - protected Processable? NextProcessable() => _currentProcessable = null; + protected void NextProcessable() => _currentProcessable = null; private Processable? _currentProcessable; - protected readonly Queue> Processes = new(); + /// A series of Processable actions to perform on this book + protected Queue> Processes { get; } = new(); protected ProcessBookViewModelBase(LibraryBook libraryBook, LogMe logme) { @@ -120,6 +118,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject PictureStorage.PictureCached -= PictureStorage_PictureCached; } } + public async Task ProcessOneAsync() { string procName = CurrentProcessable.Name; @@ -168,7 +167,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject finally { if (result == ProcessBookResult.None) - result = await showRetry(LibraryBook); + result = await GetFailureActionAsync(LibraryBook); var status = result switch { @@ -197,13 +196,14 @@ public abstract class ProcessBookViewModelBase : ReactiveObject } } - public void AddDownloadPdf() => AddProcessable(); - public void AddDownloadDecryptBook() => AddProcessable(); - public void AddConvertToMp3() => AddProcessable(); + public ProcessBookViewModelBase AddDownloadPdf() => AddProcessable(); + public ProcessBookViewModelBase AddDownloadDecryptBook() => AddProcessable(); + public ProcessBookViewModelBase AddConvertToMp3() => AddProcessable(); - private void AddProcessable() where T : Processable, new() + private ProcessBookViewModelBase AddProcessable() where T : Processable, new() { Processes.Enqueue(() => new T()); + return this; } public override string ToString() => LibraryBook.ToString(); @@ -246,16 +246,13 @@ public abstract class ProcessBookViewModelBase : ReactiveObject #endregion - - #region AudioDecodable event handlers private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title; - private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors; - private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators; - + private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt) + => Cover = LoadImageFromBytes(coverArt, PictureSize._80x80); private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e) { @@ -270,17 +267,11 @@ public abstract class ProcessBookViewModelBase : ReactiveObject return coverData; } - private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt) - { - Cover = LoadImageFromBytes(coverArt, PictureSize._80x80); - } - #endregion #region Streamable event handlers + private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining; - - private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress) { if (!downloadProgress.ProgressPercentage.HasValue) @@ -317,10 +308,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject } if (Processes.Count == 0) - { - Completed?.Invoke(this, EventArgs.Empty); return; - } NextProcessable(); LinkProcessable(CurrentProcessable); @@ -342,28 +330,39 @@ public abstract class ProcessBookViewModelBase : ReactiveObject { foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed")) Logger.Error(errorMessage); - - Completed?.Invoke(this, EventArgs.Empty); } } #endregion - #region Failure Handler + #region Failure Handler - protected async Task showRetry(LibraryBook libraryBook) + protected async Task GetFailureActionAsync(LibraryBook libraryBook) { - Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed"); + const DialogResult SkipResult = DialogResult.Ignore; + Logger.Error($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}"); DialogResult? dialogResult = Configuration.Instance.BadBook switch { Configuration.BadBookAction.Abort => DialogResult.Abort, Configuration.BadBookAction.Retry => DialogResult.Retry, Configuration.BadBookAction.Ignore => DialogResult.Ignore, - Configuration.BadBookAction.Ask => null, - _ => null + Configuration.BadBookAction.Ask or _ => await ShowRetryDialogAsync(libraryBook) }; + if (dialogResult == SkipResult) + { + libraryBook.UpdateBookStatus(LiberatedStatus.Error); + Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}"); + } + + return dialogResult is SkipResult ? ProcessBookResult.FailedSkip + : dialogResult is DialogResult.Abort ? ProcessBookResult.FailedAbort + : ProcessBookResult.FailedRetry; + } + + protected async Task ShowRetryDialogAsync(LibraryBook libraryBook) + { string details; try { @@ -372,49 +371,36 @@ public abstract class ProcessBookViewModelBase : ReactiveObject : (str.Length > 50) ? $"{str.Truncate(47)}..." : str; - details = -$@" Title: {libraryBook.Book.TitleWithSubtitle} - ID: {libraryBook.Book.AudibleProductId} - Author: {trunc(libraryBook.Book.AuthorNames())} - Narr: {trunc(libraryBook.Book.NarratorNames())}"; + details = $""" + Title: {libraryBook.Book.TitleWithSubtitle} + ID: {libraryBook.Book.AudibleProductId} + Author: {trunc(libraryBook.Book.AuthorNames())} + Narr: {trunc(libraryBook.Book.NarratorNames())} + """; } catch { details = "[Error retrieving details]"; } - // if null then ask user - dialogResult ??= await MessageBoxBase.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton); + var skipDialogText = $""" + An error occurred while trying to process this book. + {details} + + - ABORT: Stop processing books. + + - RETRY: Skip this book for now, but retry if it is requeued. Continue processing the queued books. + + - IGNORE: Permanently ignore this book. Continue processing the queued books. (Will not try this book again later.) - if (dialogResult == DialogResult.Abort) - return ProcessBookResult.FailedAbort; + See Settings in the Download/Decrypt tab to avoid this box in the future. + """; - if (dialogResult == SkipResult) - { - libraryBook.UpdateBookStatus(LiberatedStatus.Error); + const MessageBoxButtons SkipDialogButtons = MessageBoxButtons.AbortRetryIgnore; + const MessageBoxDefaultButton SkipDialogDefaultButton = MessageBoxDefaultButton.Button1; - Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}"); - - return ProcessBookResult.FailedSkip; - } - - return ProcessBookResult.FailedRetry; + return await MessageBoxBase.Show(skipDialogText, "Skip this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton); } - private static string SkipDialogText => @" -An error occurred while trying to process this book. -{0} - -- ABORT: Stop processing books. - -- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.) - -- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.) -".Trim(); - private static MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore; - private static MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1; - private static DialogResult SkipResult => DialogResult.Ignore; - #endregion - } diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs index 40ff8da8..a6452ceb 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs @@ -1,9 +1,7 @@ using DataLayer; -using LibationFileManager; using LibationUiBase.Forms; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using ApplicationServices; using System.Threading.Tasks; @@ -14,24 +12,19 @@ namespace LibationUiBase.ProcessQueue; public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm { public abstract void WriteLine(string text); + protected abstract ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook); - protected abstract ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook); - - public ObservableCollection LogEntries { get; } = new(); public TrackedQueue Queue { get; } - public ProcessBookViewModelBase? SelectedItem { get; set; } public Task? QueueRunner { get; private set; } public bool Running => !QueueRunner?.IsCompleted ?? false; - - protected readonly LogMe Logger; + protected LogMe Logger { get; } public ProcessQueueViewModelBase(ICollection? underlyingList) { Logger = LogMe.RegisterForm(this); Queue = new(underlyingList); - Queue.QueuededCountChanged += Queue_QueuededCountChanged; + Queue.QueuedCountChanged += Queue_QueuedCountChanged; Queue.CompletedCountChanged += Queue_CompletedCountChanged; - SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024; } private int _completedCount; @@ -39,7 +32,6 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm private int _queuedCount; private string? _runningTime; private bool _progressBarVisible; - private decimal _speedLimit; public int CompletedCount { get => _completedCount; private set { RaiseAndSetIfChanged(ref _completedCount, value); RaisePropertyChanged(nameof(AnyCompleted)); } } public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); RaisePropertyChanged(nameof(AnyQueued)); } } @@ -51,37 +43,6 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm public bool AnyErrors => ErrorCount > 0; public double Progress => 100d * Queue.Completed.Count / Queue.Count; - public decimal SpeedLimit - { - get - { - return _speedLimit; - } - set - { - var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024)); - var config = Configuration.Instance; - config.DownloadSpeedLimit = newValue; - - _speedLimit - = config.DownloadSpeedLimit <= newValue ? value - : value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024 - : 0; - - config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024); - - SpeedLimitIncrement = _speedLimit > 100 ? 10 - : _speedLimit > 10 ? 1 - : _speedLimit > 1 ? 0.1m - : 0.01m; - - RaisePropertyChanged(nameof(SpeedLimitIncrement)); - RaisePropertyChanged(nameof(SpeedLimit)); - } - } - - public decimal SpeedLimitIncrement { get; private set; } - private void Queue_CompletedCountChanged(object? sender, int e) { int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail); @@ -91,7 +52,8 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm CompletedCount = completeCount; RaisePropertyChanged(nameof(Progress)); } - private void Queue_QueuededCountChanged(object? sender, int cueCount) + + private void Queue_QueuedCountChanged(object? sender, int cueCount) { QueuedCount = cueCount; RaisePropertyChanged(nameof(Progress)); @@ -101,7 +63,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm public bool QueueDownloadPdf(IList libraryBooks) { - var needsPdf = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated).ToArray(); + var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray(); if (needsPdf.Length > 0) { Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length); @@ -132,14 +94,14 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm if (item.AbsentFromLastScan) return false; - else if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload) + else if (item.NeedsBookDownload()) { RemoveCompleted(item); Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item); AddDownloadDecrypt([item]); return true; } - else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated) + else if (item.NeedsPdfDownload()) { RemoveCompleted(item); Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item); @@ -149,10 +111,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm } else { - var toLiberate - = libraryBooks - .Where(x => !x.AbsentFromLastScan && x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated) - .ToArray(); + var toLiberate = libraryBooks.UnLiberated().ToArray(); if (toLiberate.Length > 0) { @@ -164,16 +123,10 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm return false; } - private bool isBookInQueue(LibraryBook libraryBook) - { - var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId); - if (entry == null) - return false; - else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed) - return !Queue.RemoveCompleted(entry); - else - return true; - } + private bool IsBookInQueue(LibraryBook libraryBook) + => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModelBase entry ? false + : entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry) + : true; private bool RemoveCompleted(LibraryBook libraryBook) => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModelBase entry @@ -182,69 +135,43 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm private void AddDownloadPdf(IEnumerable entries) { - List procs = new(); - foreach (var entry in entries) - { - if (isBookInQueue(entry)) - continue; - - var pbook = CreateNewBook(entry); - pbook.AddDownloadPdf(); - procs.Add(pbook); - } - - Serilog.Log.Logger.Information("Queueing {count} books", procs.Count); + var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray(); + Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length); AddToQueue(procs); + + ProcessBookViewModelBase Create(LibraryBook entry) + => CreateNewProcessBook(entry).AddDownloadPdf(); } private void AddDownloadDecrypt(IEnumerable entries) { - List procs = new(); - foreach (var entry in entries) - { - if (isBookInQueue(entry)) - continue; - - var pbook = CreateNewBook(entry); - pbook.AddDownloadDecryptBook(); - pbook.AddDownloadPdf(); - procs.Add(pbook); - } - - Serilog.Log.Logger.Information("Queueing {count} books", procs.Count); + var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray(); + Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length); AddToQueue(procs); + + ProcessBookViewModelBase Create(LibraryBook entry) + => CreateNewProcessBook(entry).AddDownloadDecryptBook().AddDownloadPdf(); } private void AddConvertMp3(IEnumerable entries) { - List procs = new(); - foreach (var entry in entries) - { - if (isBookInQueue(entry)) - continue; - - var pbook = CreateNewBook(entry); - pbook.AddConvertToMp3(); - procs.Add(pbook); - } - - Serilog.Log.Logger.Information("Queueing {count} books", procs.Count); + var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray(); + Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length); AddToQueue(procs); + + ProcessBookViewModelBase Create(LibraryBook entry) + => CreateNewProcessBook(entry).AddConvertToMp3(); } private void AddToQueue(IEnumerable pbook) { - Invoke(() => - { - Queue.Enqueue(pbook); - if (!Running) - QueueRunner = QueueLoop(); - }); + Queue.Enqueue(pbook); + if (!Running) + QueueRunner = Task.Run(QueueLoop); } #endregion - private DateTime StartingTime; private async Task QueueLoop() { try @@ -253,12 +180,11 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm RunningTime = string.Empty; ProgressBarVisible = true; - StartingTime = DateTime.Now; - - using var counterTimer = new System.Threading.Timer(CounterTimer_Tick, null, 0, 500); - + var startingTime = DateTime.Now; bool shownServiceOutageMessage = false; + using var counterTimer = new System.Threading.Timer(_ => RunningTime = timeToStr(DateTime.Now - startingTime), null, 0, 500); + while (Queue.MoveNext()) { if (Queue.Current is not ProcessBookViewModelBase nextBook) @@ -267,11 +193,11 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm continue; } - Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook.LibraryBook); + Serilog.Log.Logger.Information("Begin processing queued item: '{item_LibraryBook}'", nextBook.LibraryBook); var result = await nextBook.ProcessOneAsync(); - Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook.LibraryBook, result); + Serilog.Log.Logger.Information("Completed processing queued item: '{item_LibraryBook}' with result: {result}", nextBook.LibraryBook, result); if (result == ProcessBookResult.ValidationFail) Queue.ClearCurrent(); @@ -281,11 +207,11 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error); else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage) { - await MessageBoxBase.Show(@$" -You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle} + await MessageBoxBase.Show($""" + You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle} -This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app. -", + This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app. + """, "Possible Interruption of Service", MessageBoxButtons.OK, MessageBoxIcon.Asterisk); @@ -301,24 +227,9 @@ This error appears to be caused by a temporary interruption of service that some { Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items"); } - } - private void CounterTimer_Tick(object? state) - { string timeToStr(TimeSpan time) - { - string minsSecs = $"{time:mm\\:ss}"; - if (time.TotalHours >= 1) - return $"{time.TotalHours:F0}:{minsSecs}"; - return minsSecs; - } - RunningTime = timeToStr(DateTime.Now - StartingTime); + => time.TotalHours < 1 ? $"{time:mm\\:ss}" + : $"{time.TotalHours:F0}:{time:mm\\:ss}"; } } - -public class LogEntry -{ - public DateTime LogDate { get; init; } - public string LogDateString => LogDate.ToShortTimeString(); - public string? LogMessage { get; init; } -} diff --git a/Source/LibationUiBase/TrackedQueue[T].cs b/Source/LibationUiBase/TrackedQueue[T].cs index e36dac4b..746f8c6b 100644 --- a/Source/LibationUiBase/TrackedQueue[T].cs +++ b/Source/LibationUiBase/TrackedQueue[T].cs @@ -36,7 +36,7 @@ namespace LibationUiBase public class TrackedQueue where T : class { public event EventHandler? CompletedCountChanged; - public event EventHandler? QueuededCountChanged; + public event EventHandler? QueuedCountChanged; public T? Current { get; private set; } @@ -115,7 +115,7 @@ namespace LibationUiBase if (itemsRemoved) { - QueuededCountChanged?.Invoke(this, queuedCount); + QueuedCountChanged?.Invoke(this, queuedCount); RebuildSecondary(); } return itemsRemoved; @@ -151,7 +151,7 @@ namespace LibationUiBase { lock (lockObject) _queued.Clear(); - QueuededCountChanged?.Invoke(this, 0); + QueuedCountChanged?.Invoke(this, 0); RebuildSecondary(); } @@ -248,7 +248,7 @@ namespace LibationUiBase { if (completedChanged) CompletedCountChanged?.Invoke(this, completedCount); - QueuededCountChanged?.Invoke(this, queuedCount); + QueuedCountChanged?.Invoke(this, queuedCount); RebuildSecondary(); } } @@ -263,7 +263,7 @@ namespace LibationUiBase } foreach (var i in item) _underlyingList?.Add(i); - QueuededCountChanged?.Invoke(this, queueCount); + QueuedCountChanged?.Invoke(this, queueCount); } private void RebuildSecondary() diff --git a/Source/LibationWinForms/MessageBoxLib.cs b/Source/LibationWinForms/MessageBoxLib.cs index 62a7f9f8..e189ef1f 100644 --- a/Source/LibationWinForms/MessageBoxLib.cs +++ b/Source/LibationWinForms/MessageBoxLib.cs @@ -23,7 +23,8 @@ namespace LibationWinForms { // for development and debugging, show me what broke! if (System.Diagnostics.Debugger.IsAttached) - throw exception; + //Wrap the exception to preserve its stack trace. + throw new Exception("An unhandled exception was encountered", exception); try { diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs index 07d24012..89bcf31c 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs @@ -10,7 +10,7 @@ namespace LibationWinForms.ProcessQueue private static int ControlNumberCounter = 0; /// - /// The contol's position within + /// The control's position within /// public int ControlNumber { get; } private ProcessBookStatus Status { get; set; } = ProcessBookStatus.Queued; @@ -25,7 +25,6 @@ namespace LibationWinForms.ProcessQueue public ProcessBookControl() { InitializeComponent(); - statusLbl.Text = "Queued"; remainingTimeLbl.Visible = false; progressBar1.Visible = false; etaLbl.Visible = false; @@ -45,7 +44,7 @@ namespace LibationWinForms.ProcessQueue bookInfoLbl.Text = title; } - public void SetProgrss(int progress) + public void SetProgress(int progress) { //Disable slow fill //https://stackoverflow.com/a/5332770/3335599 @@ -59,25 +58,7 @@ namespace LibationWinForms.ProcessQueue remainingTimeLbl.Text = $"{remaining:mm\\:ss}"; } - public void SetResult(ProcessBookResult result) - { - (string statusText, ProcessBookStatus status) = result switch - { - ProcessBookResult.Success => ("Finished", ProcessBookStatus.Completed), - ProcessBookResult.Cancelled => ("Cancelled", ProcessBookStatus.Cancelled), - ProcessBookResult.FailedRetry => ("Error, will retry later", ProcessBookStatus.Failed), - ProcessBookResult.FailedSkip => ("Error, Skipping", ProcessBookStatus.Failed), - ProcessBookResult.FailedAbort => ("Error, Abort", ProcessBookStatus.Failed), - ProcessBookResult.ValidationFail => ("Validation fail", ProcessBookStatus.Failed), - ProcessBookResult.LicenseDenied => ("License Denied", ProcessBookStatus.Failed), - ProcessBookResult.LicenseDeniedPossibleOutage => ("Possible Service Interruption", ProcessBookStatus.Failed), - _ => ("UNKNOWN", ProcessBookStatus.Failed), - }; - - SetStatus(status, statusText); - } - - public void SetStatus(ProcessBookStatus status, string statusText = null) + public void SetStatus(ProcessBookStatus status, string statusText) { Status = status; @@ -101,7 +82,7 @@ namespace LibationWinForms.ProcessQueue progressBar1.Visible = Status == ProcessBookStatus.Working; etaLbl.Visible = Status == ProcessBookStatus.Working; statusLbl.Visible = Status != ProcessBookStatus.Working; - statusLbl.Text = statusText ?? Status.ToString(); + statusLbl.Text = statusText; BackColor = backColor; int deltaX = Width - cancelBtn.Location.X - CancelBtnDistanceFromEdge; @@ -110,7 +91,7 @@ namespace LibationWinForms.ProcessQueue { //If the last book to occupy this control before resizing was not //queued, the buttons were not Visible so the Anchor property was - //ignored. Manually resize and reposition everyhting + //ignored. Manually resize and reposition everything cancelBtn.Location = new Point(cancelBtn.Location.X + deltaX, cancelBtn.Location.Y); moveFirstBtn.Location = new Point(moveFirstBtn.Location.X + deltaX, moveFirstBtn.Location.Y); diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs index a6b3a48d..50bc57c9 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs @@ -169,17 +169,10 @@ internal partial class ProcessQueueControl : UserControl Panels[i].SetCover(proc.Cover as Image); if (propertyName is null or nameof(proc.Title) or nameof(proc.Author) or nameof(proc.Narrator)) Panels[i].SetBookInfo($"{proc.Title}\r\nBy {proc.Author}\r\nNarrated by {proc.Narrator}"); - - if (proc.Result != ProcessBookResult.None) - { - Panels[i].SetResult(proc.Result); - return; - } - - if (propertyName is null or nameof(proc.Status)) - Panels[i].SetStatus(proc.Status); + if (propertyName is null or nameof(proc.Status) or nameof(proc.StatusText)) + Panels[i].SetStatus(proc.Status, proc.StatusText); if (propertyName is null or nameof(proc.Progress)) - Panels[i].SetProgrss(proc.Progress); + Panels[i].SetProgress(proc.Progress); if (propertyName is null or nameof(proc.TimeRemaining)) Panels[i].SetRemainingTime(proc.TimeRemaining); Panels[i].ResumeLayout(); diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs index d94eb845..6c4afd63 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueViewModel.cs @@ -56,7 +56,7 @@ internal class ProcessQueueViewModel : ProcessQueueViewModelBase public override void WriteLine(string text) => Invoke(() => LogWritten?.Invoke(this, text.Trim())); - protected override ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook) + protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook) => new ProcessBookViewModel(libraryBook, Logger); private static ObservableCollection CreateEmptyList()