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 ) == true
).ToList(); ).ToList();
public static IEnumerable<LibraryBook> UnLiberated(this IEnumerable<LibraryBook> bookList) public static bool NeedsPdfDownload(this LibraryBook libraryBook)
=> bookList => !libraryBook.AbsentFromLastScan && libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated;
.Where( public static bool NeedsBookDownload(this LibraryBook libraryBook)
lb => => !libraryBook.AbsentFromLastScan && libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload;
!lb.AbsentFromLastScan && public static IEnumerable<LibraryBook> UnLiberated(this IEnumerable<LibraryBook> bookList)
(lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload => bookList.Where(lb => lb.NeedsPdfDownload() || lb.NeedsBookDownload());
|| lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
);
} }
} }

View File

@ -20,41 +20,39 @@ namespace LibationAvalonia
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
=> ShowCoreAsync(null, text, caption, buttons, icon, defaultButton); => ShowCoreAsync(null, text, caption, buttons, icon, defaultButton);
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, bool saveAndRestorePosition = true) public static Task<DialogResult> 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<DialogResult> Show(string text, string caption, MessageBoxButtons buttons) public static Task<DialogResult> 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<DialogResult> Show(string text, string caption) public static Task<DialogResult> 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<DialogResult> Show(string text) public static Task<DialogResult> 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<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true) 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); => ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton, saveAndRestorePosition);
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) public static Task<DialogResult> 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<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons) public static Task<DialogResult> 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<DialogResult> Show(Window owner, string text, string caption) public static Task<DialogResult> 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<DialogResult> Show(Window owner, string text) public static Task<DialogResult> Show(Window owner, string text)
=> ShowCoreAsync(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); => ShowCoreAsync(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static async Task VerboseLoggingWarning_ShowIfTrue() public static async Task VerboseLoggingWarning_ShowIfTrue()
{ {
// when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured // when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured
if (Serilog.Log.Logger.IsVerboseEnabled()) if (Serilog.Log.Logger.IsVerboseEnabled())
await Show(@" await Show("""
Warning: verbose logging is enabled. Warning: verbose logging is enabled.
This should be used for debugging only. It creates many This should be used for debugging only. It creates many
more logs and debug files, neither of which are as more logs and debug files, neither of which are as
strictly anonymous. strictly anonymous.
When you are finished debugging, it's highly recommended When you are finished debugging, it's highly recommended
to set your debug MinimumLevel to Information and restart to set your debug MinimumLevel to Information and restart
Libation. Libation.
".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning); """, "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
} }
/// <summary> /// <summary>
@ -94,7 +92,8 @@ Libation.
{ {
// for development and debugging, show me what broke! // for development and debugging, show me what broke!
if (System.Diagnostics.Debugger.IsAttached) 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 try
{ {
@ -131,7 +130,6 @@ Libation.
tbx.MinWidth = vm.TextBlockMinWidth; tbx.MinWidth = vm.TextBlockMinWidth;
tbx.Text = message; tbx.Text = message;
var thisScreen = owner.Screens?.ScreenFromVisual(owner); var thisScreen = owner.Screens?.ScreenFromVisual(owner);
var maxSize var maxSize
@ -185,6 +183,5 @@ Libation.
return await toDisplay.ShowDialog<DialogResult>(owner); return await toDisplay.ShowDialog<DialogResult>(owner);
} }
} }
} }
} }

View File

@ -5,30 +5,64 @@ using DataLayer;
using LibationFileManager; using LibationFileManager;
using LibationUiBase.ProcessQueue; using LibationUiBase.ProcessQueue;
using System; using System;
using System.Collections.ObjectModel;
#nullable enable #nullable enable
namespace LibationAvalonia.ViewModels; namespace LibationAvalonia.ViewModels;
public record LogEntry(DateTime LogDate, string? LogMessage)
{
public string LogDateString => LogDate.ToShortTimeString();
}
public class ProcessQueueViewModel : ProcessQueueViewModelBase 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()) public ProcessQueueViewModel() : base(CreateEmptyList())
{ {
Items = Queue.UnderlyingList as AvaloniaList<ProcessBookViewModelBase> Items = Queue.UnderlyingList as AvaloniaList<ProcessBookViewModelBase>
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList)); ?? 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; } 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); => new ProcessBookViewModel(libraryBook, Logger);
private static AvaloniaList<ProcessBookViewModelBase> CreateEmptyList() private static AvaloniaList<ProcessBookViewModelBase> CreateEmptyList()

View File

@ -42,8 +42,6 @@ public enum ProcessBookStatus
/// </summary> /// </summary>
public abstract class ProcessBookViewModelBase : ReactiveObject public abstract class ProcessBookViewModelBase : ReactiveObject
{ {
public event EventHandler? Completed;
private readonly LogMe Logger; private readonly LogMe Logger;
public LibraryBook LibraryBook { get; protected set; } public LibraryBook LibraryBook { get; protected set; }
@ -86,12 +84,12 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
#endregion #endregion
protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke(); protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
protected Processable? NextProcessable() => _currentProcessable = null; protected void NextProcessable() => _currentProcessable = null;
private Processable? _currentProcessable; 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) protected ProcessBookViewModelBase(LibraryBook libraryBook, LogMe logme)
{ {
@ -120,6 +118,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
PictureStorage.PictureCached -= PictureStorage_PictureCached; PictureStorage.PictureCached -= PictureStorage_PictureCached;
} }
} }
public async Task<ProcessBookResult> ProcessOneAsync() public async Task<ProcessBookResult> ProcessOneAsync()
{ {
string procName = CurrentProcessable.Name; string procName = CurrentProcessable.Name;
@ -168,7 +167,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
finally finally
{ {
if (result == ProcessBookResult.None) if (result == ProcessBookResult.None)
result = await showRetry(LibraryBook); result = await GetFailureActionAsync(LibraryBook);
var status = result switch var status = result switch
{ {
@ -197,13 +196,14 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
} }
} }
public void AddDownloadPdf() => AddProcessable<DownloadPdf>(); public ProcessBookViewModelBase AddDownloadPdf() => AddProcessable<DownloadPdf>();
public void AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>(); public ProcessBookViewModelBase AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
public void AddConvertToMp3() => AddProcessable<ConvertToMp3>(); 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()); Processes.Enqueue(() => new T());
return this;
} }
public override string ToString() => LibraryBook.ToString(); public override string ToString() => LibraryBook.ToString();
@ -246,16 +246,13 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
#endregion #endregion
#region AudioDecodable event handlers #region AudioDecodable event handlers
private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title; private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title;
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors; private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators; 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) private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
{ {
@ -270,17 +267,11 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
return coverData; return coverData;
} }
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
{
Cover = LoadImageFromBytes(coverArt, PictureSize._80x80);
}
#endregion #endregion
#region Streamable event handlers #region Streamable event handlers
private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining; private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress) private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
{ {
if (!downloadProgress.ProgressPercentage.HasValue) if (!downloadProgress.ProgressPercentage.HasValue)
@ -317,10 +308,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
} }
if (Processes.Count == 0) if (Processes.Count == 0)
{
Completed?.Invoke(this, EventArgs.Empty);
return; return;
}
NextProcessable(); NextProcessable();
LinkProcessable(CurrentProcessable); LinkProcessable(CurrentProcessable);
@ -342,28 +330,39 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
{ {
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed")) foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
Logger.Error(errorMessage); Logger.Error(errorMessage);
Completed?.Invoke(this, EventArgs.Empty);
} }
} }
#endregion #endregion
#region Failure Handler #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 DialogResult? dialogResult = Configuration.Instance.BadBook switch
{ {
Configuration.BadBookAction.Abort => DialogResult.Abort, Configuration.BadBookAction.Abort => DialogResult.Abort,
Configuration.BadBookAction.Retry => DialogResult.Retry, Configuration.BadBookAction.Retry => DialogResult.Retry,
Configuration.BadBookAction.Ignore => DialogResult.Ignore, Configuration.BadBookAction.Ignore => DialogResult.Ignore,
Configuration.BadBookAction.Ask => null, Configuration.BadBookAction.Ask or _ => await ShowRetryDialogAsync(libraryBook)
_ => null
}; };
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; string details;
try try
{ {
@ -372,49 +371,36 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
: (str.Length > 50) ? $"{str.Truncate(47)}..." : (str.Length > 50) ? $"{str.Truncate(47)}..."
: str; : str;
details = details = $"""
$@" Title: {libraryBook.Book.TitleWithSubtitle} Title: {libraryBook.Book.TitleWithSubtitle}
ID: {libraryBook.Book.AudibleProductId} ID: {libraryBook.Book.AudibleProductId}
Author: {trunc(libraryBook.Book.AuthorNames())} Author: {trunc(libraryBook.Book.AuthorNames())}
Narr: {trunc(libraryBook.Book.NarratorNames())}"; Narr: {trunc(libraryBook.Book.NarratorNames())}
""";
} }
catch catch
{ {
details = "[Error retrieving details]"; details = "[Error retrieving details]";
} }
// if null then ask user var skipDialogText = $"""
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); 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) See Settings in the Download/Decrypt tab to avoid this box in the future.
return ProcessBookResult.FailedAbort; """;
if (dialogResult == SkipResult) const MessageBoxButtons SkipDialogButtons = MessageBoxButtons.AbortRetryIgnore;
{ const MessageBoxDefaultButton SkipDialogDefaultButton = MessageBoxDefaultButton.Button1;
libraryBook.UpdateBookStatus(LiberatedStatus.Error);
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}"); return await MessageBoxBase.Show(skipDialogText, "Skip this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
return ProcessBookResult.FailedSkip;
}
return ProcessBookResult.FailedRetry;
} }
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 #endregion
} }

View File

@ -1,9 +1,7 @@
using DataLayer; using DataLayer;
using LibationFileManager;
using LibationUiBase.Forms; using LibationUiBase.Forms;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using ApplicationServices; using ApplicationServices;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -14,24 +12,19 @@ namespace LibationUiBase.ProcessQueue;
public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
{ {
public abstract void WriteLine(string text); 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 TrackedQueue<ProcessBookViewModelBase> Queue { get; }
public ProcessBookViewModelBase? SelectedItem { get; set; }
public Task? QueueRunner { get; private set; } public Task? QueueRunner { get; private set; }
public bool Running => !QueueRunner?.IsCompleted ?? false; public bool Running => !QueueRunner?.IsCompleted ?? false;
protected LogMe Logger { get; }
protected readonly LogMe Logger;
public ProcessQueueViewModelBase(ICollection<ProcessBookViewModelBase>? underlyingList) public ProcessQueueViewModelBase(ICollection<ProcessBookViewModelBase>? underlyingList)
{ {
Logger = LogMe.RegisterForm(this); Logger = LogMe.RegisterForm(this);
Queue = new(underlyingList); Queue = new(underlyingList);
Queue.QueuededCountChanged += Queue_QueuededCountChanged; Queue.QueuedCountChanged += Queue_QueuedCountChanged;
Queue.CompletedCountChanged += Queue_CompletedCountChanged; Queue.CompletedCountChanged += Queue_CompletedCountChanged;
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
} }
private int _completedCount; private int _completedCount;
@ -39,7 +32,6 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
private int _queuedCount; private int _queuedCount;
private string? _runningTime; private string? _runningTime;
private bool _progressBarVisible; private bool _progressBarVisible;
private decimal _speedLimit;
public int CompletedCount { get => _completedCount; private set { RaiseAndSetIfChanged(ref _completedCount, value); RaisePropertyChanged(nameof(AnyCompleted)); } } 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)); } } 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 bool AnyErrors => ErrorCount > 0;
public double Progress => 100d * Queue.Completed.Count / Queue.Count; 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) 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); 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; CompletedCount = completeCount;
RaisePropertyChanged(nameof(Progress)); RaisePropertyChanged(nameof(Progress));
} }
private void Queue_QueuededCountChanged(object? sender, int cueCount)
private void Queue_QueuedCountChanged(object? sender, int cueCount)
{ {
QueuedCount = cueCount; QueuedCount = cueCount;
RaisePropertyChanged(nameof(Progress)); RaisePropertyChanged(nameof(Progress));
@ -101,7 +63,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks) 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) if (needsPdf.Length > 0)
{ {
Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length); Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
@ -132,14 +94,14 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
if (item.AbsentFromLastScan) if (item.AbsentFromLastScan)
return false; return false;
else if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload) else if (item.NeedsBookDownload())
{ {
RemoveCompleted(item); RemoveCompleted(item);
Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item); Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item);
AddDownloadDecrypt([item]); AddDownloadDecrypt([item]);
return true; return true;
} }
else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated) else if (item.NeedsPdfDownload())
{ {
RemoveCompleted(item); RemoveCompleted(item);
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item); Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
@ -149,10 +111,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
} }
else else
{ {
var toLiberate var toLiberate = libraryBooks.UnLiberated().ToArray();
= libraryBooks
.Where(x => !x.AbsentFromLastScan && x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
.ToArray();
if (toLiberate.Length > 0) if (toLiberate.Length > 0)
{ {
@ -164,16 +123,10 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
return false; return false;
} }
private bool isBookInQueue(LibraryBook libraryBook) private bool IsBookInQueue(LibraryBook libraryBook)
{ => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModelBase entry ? false
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId); : entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry)
if (entry == null) : true;
return false;
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
return !Queue.RemoveCompleted(entry);
else
return true;
}
private bool RemoveCompleted(LibraryBook libraryBook) private bool RemoveCompleted(LibraryBook libraryBook)
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModelBase entry => 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) private void AddDownloadPdf(IEnumerable<LibraryBook> entries)
{ {
List<ProcessBookViewModelBase> procs = new(); var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
foreach (var entry in entries) Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
{
if (isBookInQueue(entry))
continue;
var pbook = CreateNewBook(entry);
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs); AddToQueue(procs);
ProcessBookViewModelBase Create(LibraryBook entry)
=> CreateNewProcessBook(entry).AddDownloadPdf();
} }
private void AddDownloadDecrypt(IEnumerable<LibraryBook> entries) private void AddDownloadDecrypt(IEnumerable<LibraryBook> entries)
{ {
List<ProcessBookViewModelBase> procs = new(); var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
foreach (var entry in entries) Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
{
if (isBookInQueue(entry))
continue;
var pbook = CreateNewBook(entry);
pbook.AddDownloadDecryptBook();
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs); AddToQueue(procs);
ProcessBookViewModelBase Create(LibraryBook entry)
=> CreateNewProcessBook(entry).AddDownloadDecryptBook().AddDownloadPdf();
} }
private void AddConvertMp3(IEnumerable<LibraryBook> entries) private void AddConvertMp3(IEnumerable<LibraryBook> entries)
{ {
List<ProcessBookViewModelBase> procs = new(); var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
foreach (var entry in entries) Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
{
if (isBookInQueue(entry))
continue;
var pbook = CreateNewBook(entry);
pbook.AddConvertToMp3();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs); AddToQueue(procs);
ProcessBookViewModelBase Create(LibraryBook entry)
=> CreateNewProcessBook(entry).AddConvertToMp3();
} }
private void AddToQueue(IEnumerable<ProcessBookViewModelBase> pbook) private void AddToQueue(IEnumerable<ProcessBookViewModelBase> pbook)
{ {
Invoke(() => Queue.Enqueue(pbook);
{ if (!Running)
Queue.Enqueue(pbook); QueueRunner = Task.Run(QueueLoop);
if (!Running)
QueueRunner = QueueLoop();
});
} }
#endregion #endregion
private DateTime StartingTime;
private async Task QueueLoop() private async Task QueueLoop()
{ {
try try
@ -253,12 +180,11 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
RunningTime = string.Empty; RunningTime = string.Empty;
ProgressBarVisible = true; ProgressBarVisible = true;
StartingTime = DateTime.Now; var startingTime = DateTime.Now;
using var counterTimer = new System.Threading.Timer(CounterTimer_Tick, null, 0, 500);
bool shownServiceOutageMessage = false; bool shownServiceOutageMessage = false;
using var counterTimer = new System.Threading.Timer(_ => RunningTime = timeToStr(DateTime.Now - startingTime), null, 0, 500);
while (Queue.MoveNext()) while (Queue.MoveNext())
{ {
if (Queue.Current is not ProcessBookViewModelBase nextBook) if (Queue.Current is not ProcessBookViewModelBase nextBook)
@ -267,11 +193,11 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
continue; 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(); 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) if (result == ProcessBookResult.ValidationFail)
Queue.ClearCurrent(); Queue.ClearCurrent();
@ -281,11 +207,11 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error); nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error);
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage) else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
{ {
await MessageBoxBase.Show(@$" await MessageBoxBase.Show($"""
You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle} 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", "Possible Interruption of Service",
MessageBoxButtons.OK, MessageBoxButtons.OK,
MessageBoxIcon.Asterisk); 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"); Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
} }
}
private void CounterTimer_Tick(object? state)
{
string timeToStr(TimeSpan time) string timeToStr(TimeSpan time)
{ => time.TotalHours < 1 ? $"{time:mm\\:ss}"
string minsSecs = $"{time:mm\\:ss}"; : $"{time.TotalHours:F0}:{time:mm\\:ss}";
if (time.TotalHours >= 1)
return $"{time.TotalHours:F0}:{minsSecs}";
return minsSecs;
}
RunningTime = timeToStr(DateTime.Now - StartingTime);
} }
} }
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 class TrackedQueue<T> where T : class
{ {
public event EventHandler<int>? CompletedCountChanged; public event EventHandler<int>? CompletedCountChanged;
public event EventHandler<int>? QueuededCountChanged; public event EventHandler<int>? QueuedCountChanged;
public T? Current { get; private set; } public T? Current { get; private set; }
@ -115,7 +115,7 @@ namespace LibationUiBase
if (itemsRemoved) if (itemsRemoved)
{ {
QueuededCountChanged?.Invoke(this, queuedCount); QueuedCountChanged?.Invoke(this, queuedCount);
RebuildSecondary(); RebuildSecondary();
} }
return itemsRemoved; return itemsRemoved;
@ -151,7 +151,7 @@ namespace LibationUiBase
{ {
lock (lockObject) lock (lockObject)
_queued.Clear(); _queued.Clear();
QueuededCountChanged?.Invoke(this, 0); QueuedCountChanged?.Invoke(this, 0);
RebuildSecondary(); RebuildSecondary();
} }
@ -248,7 +248,7 @@ namespace LibationUiBase
{ {
if (completedChanged) if (completedChanged)
CompletedCountChanged?.Invoke(this, completedCount); CompletedCountChanged?.Invoke(this, completedCount);
QueuededCountChanged?.Invoke(this, queuedCount); QueuedCountChanged?.Invoke(this, queuedCount);
RebuildSecondary(); RebuildSecondary();
} }
} }
@ -263,7 +263,7 @@ namespace LibationUiBase
} }
foreach (var i in item) foreach (var i in item)
_underlyingList?.Add(i); _underlyingList?.Add(i);
QueuededCountChanged?.Invoke(this, queueCount); QueuedCountChanged?.Invoke(this, queueCount);
} }
private void RebuildSecondary() private void RebuildSecondary()

View File

@ -23,7 +23,8 @@ namespace LibationWinForms
{ {
// for development and debugging, show me what broke! // for development and debugging, show me what broke!
if (System.Diagnostics.Debugger.IsAttached) 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 try
{ {

View File

@ -10,7 +10,7 @@ namespace LibationWinForms.ProcessQueue
private static int ControlNumberCounter = 0; private static int ControlNumberCounter = 0;
/// <summary> /// <summary>
/// The contol's position within <see cref="VirtualFlowControl"/> /// The control's position within <see cref="VirtualFlowControl"/>
/// </summary> /// </summary>
public int ControlNumber { get; } public int ControlNumber { get; }
private ProcessBookStatus Status { get; set; } = ProcessBookStatus.Queued; private ProcessBookStatus Status { get; set; } = ProcessBookStatus.Queued;
@ -25,7 +25,6 @@ namespace LibationWinForms.ProcessQueue
public ProcessBookControl() public ProcessBookControl()
{ {
InitializeComponent(); InitializeComponent();
statusLbl.Text = "Queued";
remainingTimeLbl.Visible = false; remainingTimeLbl.Visible = false;
progressBar1.Visible = false; progressBar1.Visible = false;
etaLbl.Visible = false; etaLbl.Visible = false;
@ -45,7 +44,7 @@ namespace LibationWinForms.ProcessQueue
bookInfoLbl.Text = title; bookInfoLbl.Text = title;
} }
public void SetProgrss(int progress) public void SetProgress(int progress)
{ {
//Disable slow fill //Disable slow fill
//https://stackoverflow.com/a/5332770/3335599 //https://stackoverflow.com/a/5332770/3335599
@ -59,25 +58,7 @@ namespace LibationWinForms.ProcessQueue
remainingTimeLbl.Text = $"{remaining:mm\\:ss}"; remainingTimeLbl.Text = $"{remaining:mm\\:ss}";
} }
public void SetResult(ProcessBookResult result) public void SetStatus(ProcessBookStatus status, string statusText)
{
(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)
{ {
Status = status; Status = status;
@ -101,7 +82,7 @@ namespace LibationWinForms.ProcessQueue
progressBar1.Visible = Status == ProcessBookStatus.Working; progressBar1.Visible = Status == ProcessBookStatus.Working;
etaLbl.Visible = Status == ProcessBookStatus.Working; etaLbl.Visible = Status == ProcessBookStatus.Working;
statusLbl.Visible = Status != ProcessBookStatus.Working; statusLbl.Visible = Status != ProcessBookStatus.Working;
statusLbl.Text = statusText ?? Status.ToString(); statusLbl.Text = statusText;
BackColor = backColor; BackColor = backColor;
int deltaX = Width - cancelBtn.Location.X - CancelBtnDistanceFromEdge; 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 //If the last book to occupy this control before resizing was not
//queued, the buttons were not Visible so the Anchor property was //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); cancelBtn.Location = new Point(cancelBtn.Location.X + deltaX, cancelBtn.Location.Y);
moveFirstBtn.Location = new Point(moveFirstBtn.Location.X + deltaX, moveFirstBtn.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); Panels[i].SetCover(proc.Cover as Image);
if (propertyName is null or nameof(proc.Title) or nameof(proc.Author) or nameof(proc.Narrator)) 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}"); Panels[i].SetBookInfo($"{proc.Title}\r\nBy {proc.Author}\r\nNarrated by {proc.Narrator}");
if (propertyName is null or nameof(proc.Status) or nameof(proc.StatusText))
if (proc.Result != ProcessBookResult.None) Panels[i].SetStatus(proc.Status, proc.StatusText);
{
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.Progress)) 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)) if (propertyName is null or nameof(proc.TimeRemaining))
Panels[i].SetRemainingTime(proc.TimeRemaining); Panels[i].SetRemainingTime(proc.TimeRemaining);
Panels[i].ResumeLayout(); 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())); 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); => new ProcessBookViewModel(libraryBook, Logger);
private static ObservableCollection<ProcessBookViewModelBase> CreateEmptyList() private static ObservableCollection<ProcessBookViewModelBase> CreateEmptyList()