Code cleanup and refactoring for clarity

This commit is contained in:
Michael Bucari-Tovo 2025-07-16 11:28:37 -06:00 committed by MBucari
parent a3734c76b1
commit 4b7939541a
10 changed files with 179 additions and 278 deletions

View File

@ -103,13 +103,11 @@ namespace DataLayer
) == true
).ToList();
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<LibraryBook> UnLiberated(this IEnumerable<LibraryBook> 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)
);
=> bookList.Where(lb => lb.NeedsPdfDownload() || lb.NeedsBookDownload());
}
}

View File

@ -29,7 +29,6 @@ namespace LibationAvalonia
=> ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
=> ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton, saveAndRestorePosition);
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
=> ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons)
@ -39,12 +38,11 @@ namespace LibationAvalonia
public static Task<DialogResult> 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(@"
await Show("""
Warning: verbose logging is enabled.
This should be used for debugging only. It creates many
@ -54,7 +52,7 @@ 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);
""", "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
/// <summary>
@ -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<DialogResult>(owner);
}
}
}
}

View File

@ -5,30 +5,64 @@ using DataLayer;
using LibationFileManager;
using LibationUiBase.ProcessQueue;
using System;
using System.Collections.ObjectModel;
#nullable enable
namespace LibationAvalonia.ViewModels;
public class ProcessQueueViewModel : ProcessQueueViewModelBase
public record LogEntry(DateTime LogDate, string? LogMessage)
{
public override void WriteLine(string text)
{
Dispatcher.UIThread.Invoke(() =>
LogEntries.Add(new()
{
LogDate = DateTime.Now,
LogMessage = text.Trim()
}));
public string LogDateString => LogDate.ToShortTimeString();
}
public class ProcessQueueViewModel : ProcessQueueViewModelBase
{
public ProcessQueueViewModel() : base(CreateEmptyList())
{
Items = Queue.UnderlyingList as AvaloniaList<ProcessBookViewModelBase>
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
}
private decimal _speedLimit;
public decimal SpeedLimitIncrement { get; private set; }
public ObservableCollection<LogEntry> LogEntries { get; } = new();
public AvaloniaList<ProcessBookViewModelBase> 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<ProcessBookViewModelBase> CreateEmptyList()

View File

@ -42,8 +42,6 @@ public enum ProcessBookStatus
/// </summary>
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<Func<Processable>> Processes = new();
/// <summary> A series of Processable actions to perform on this book </summary>
protected Queue<Func<Processable>> 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<ProcessBookResult> 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<DownloadPdf>();
public void AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
public void AddConvertToMp3() => AddProcessable<ConvertToMp3>();
public ProcessBookViewModelBase AddDownloadPdf() => AddProcessable<DownloadPdf>();
public ProcessBookViewModelBase AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
public ProcessBookViewModelBase AddConvertToMp3() => AddProcessable<ConvertToMp3>();
private void AddProcessable<T>() where T : Processable, new()
private ProcessBookViewModelBase AddProcessable<T>() 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,8 +330,6 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
{
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
Logger.Error(errorMessage);
Completed?.Invoke(this, EventArgs.Empty);
}
}
@ -351,19 +337,32 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
#region Failure Handler
protected async Task<ProcessBookResult> showRetry(LibraryBook libraryBook)
protected async Task<ProcessBookResult> 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<DialogResult> 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}
details = $"""
Title: {libraryBook.Book.TitleWithSubtitle}
ID: {libraryBook.Book.AudibleProductId}
Author: {trunc(libraryBook.Book.AuthorNames())}
Narr: {trunc(libraryBook.Book.NarratorNames())}";
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);
if (dialogResult == DialogResult.Abort)
return ProcessBookResult.FailedAbort;
if (dialogResult == SkipResult)
{
libraryBook.UpdateBookStatus(LiberatedStatus.Error);
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
return ProcessBookResult.FailedSkip;
}
return ProcessBookResult.FailedRetry;
}
private static string SkipDialogText => @"
var skipDialogText = $"""
An error occurred while trying to process this book.
{0}
{details}
- ABORT: Stop processing books.
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
- RETRY: Skip this book for now, but retry if it is requeued. Continue processing the queued books.
- 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;
- IGNORE: Permanently ignore this book. Continue processing the queued books. (Will not try this book again later.)
See Settings in the Download/Decrypt tab to avoid this box in the future.
""";
const MessageBoxButtons SkipDialogButtons = MessageBoxButtons.AbortRetryIgnore;
const MessageBoxDefaultButton SkipDialogDefaultButton = MessageBoxDefaultButton.Button1;
return await MessageBoxBase.Show(skipDialogText, "Skip this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
}
#endregion
}

View File

@ -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<LogEntry> LogEntries { get; } = new();
public TrackedQueue<ProcessBookViewModelBase> 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<ProcessBookViewModelBase>? 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<LibraryBook> 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<LibraryBook> entries)
{
List<ProcessBookViewModelBase> 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<LibraryBook> entries)
{
List<ProcessBookViewModelBase> 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<LibraryBook> entries)
{
List<ProcessBookViewModelBase> 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<ProcessBookViewModelBase> pbook)
{
Invoke(() =>
{
Queue.Enqueue(pbook);
if (!Running)
QueueRunner = QueueLoop();
});
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(@$"
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.
",
""",
"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; }
}

View File

@ -36,7 +36,7 @@ namespace LibationUiBase
public class TrackedQueue<T> where T : class
{
public event EventHandler<int>? CompletedCountChanged;
public event EventHandler<int>? QueuededCountChanged;
public event EventHandler<int>? 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()

View File

@ -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
{

View File

@ -10,7 +10,7 @@ namespace LibationWinForms.ProcessQueue
private static int ControlNumberCounter = 0;
/// <summary>
/// The contol's position within <see cref="VirtualFlowControl"/>
/// The control's position within <see cref="VirtualFlowControl"/>
/// </summary>
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);

View File

@ -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();

View File

@ -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<ProcessBookViewModelBase> CreateEmptyList()