diff --git a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs
index deeaf945..214ea33b 100644
--- a/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs
+++ b/Source/LibationAvalonia/Controls/ThemePreviewControl.axaml.cs
@@ -1,12 +1,10 @@
using Avalonia.Controls;
-using Avalonia.Media.Imaging;
using DataLayer;
using Dinah.Core.ErrorHandling;
using LibationAvalonia.ViewModels;
using LibationFileManager;
-using NPOI.Util.Collections;
+using LibationUiBase.ProcessQueue;
using System.Collections.Generic;
-using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
diff --git a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs
index 8e750812..3bc4b30f 100644
--- a/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs
+++ b/Source/LibationAvalonia/ViewModels/ProcessBookViewModel.cs
@@ -1,420 +1,17 @@
-using ApplicationServices;
-using AudibleApi;
-using AudibleApi.Common;
-using Avalonia.Media.Imaging;
-using Avalonia.Threading;
-using DataLayer;
-using Dinah.Core;
-using Dinah.Core.ErrorHandling;
-using FileLiberator;
+using DataLayer;
using LibationFileManager;
using LibationUiBase;
-using LibationUiBase.Forms;
-using ReactiveUI;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
+using LibationUiBase.ProcessQueue;
#nullable enable
-namespace LibationAvalonia.ViewModels
+namespace LibationAvalonia.ViewModels;
+
+public class ProcessBookViewModel : ProcessBookViewModelBase
{
- public enum ProcessBookResult
- {
- None,
- Success,
- Cancelled,
- ValidationFail,
- FailedRetry,
- FailedSkip,
- FailedAbort,
- LicenseDenied,
- LicenseDeniedPossibleOutage
- }
- public enum ProcessBookStatus
- {
- Queued,
- Cancelled,
- Working,
- Completed,
- Failed
- }
+ public ProcessBookViewModel(LibraryBook libraryBook, LogMe logme) : base(libraryBook, logme) { }
- ///
- /// This is the viewmodel for queued processables
- ///
- public class ProcessBookViewModel : ViewModelBase
- {
- public event EventHandler? Completed;
+ protected override object? LoadImageFromBytes(byte[] bytes, PictureSize pictureSize)
+ => AvaloniaUtils.TryLoadImageOrDefault(bytes, pictureSize);
- public LibraryBook LibraryBook { get; private set; }
-
- private ProcessBookResult _result = ProcessBookResult.None;
- private ProcessBookStatus _status = ProcessBookStatus.Queued;
- private string? _narrator;
- private string? _author;
- private string? _title;
- private int _progress;
- private string? _eta;
- private Bitmap? _cover;
-
- #region Properties exposed to the view
- public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
- public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
- public string? Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
- public string? Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
- public string? Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
- public int Progress { get => _progress; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
- public string? ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
- public Bitmap? Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
- public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
- public bool IsDownloading => Status is ProcessBookStatus.Working;
- public bool Queued => Status is ProcessBookStatus.Queued;
-
- public string StatusText => Result switch
- {
- ProcessBookResult.Success => "Finished",
- ProcessBookResult.Cancelled => "Cancelled",
- ProcessBookResult.ValidationFail => "Validation fail",
- ProcessBookResult.FailedRetry => "Error, will retry later",
- ProcessBookResult.FailedSkip => "Error, Skipping",
- ProcessBookResult.FailedAbort => "Error, Abort",
- ProcessBookResult.LicenseDenied => "License Denied",
- ProcessBookResult.LicenseDeniedPossibleOutage => "Possible Service Interruption",
- _ => Status.ToString(),
- };
-
- #endregion
-
- private TimeSpan TimeRemaining { set { ETA = $"ETA: {value:mm\\:ss}"; } }
- private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
- private Processable? NextProcessable() => _currentProcessable = null;
- private Processable? _currentProcessable;
- private readonly Queue> Processes = new();
- private readonly LogMe Logger;
-
- public ProcessBookViewModel(LibraryBook libraryBook, LogMe logme)
- {
- LibraryBook = libraryBook;
- Logger = logme;
-
- _title = LibraryBook.Book.TitleWithSubtitle;
- _author = LibraryBook.Book.AuthorNames();
- _narrator = LibraryBook.Book.NarratorNames();
-
- (bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._80x80));
-
- if (isDefault)
- PictureStorage.PictureCached += PictureStorage_PictureCached;
-
- // Mutable property. Set the field so PropertyChanged isn't fired.
- _cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
- }
-
- private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
- {
- if (e.Definition.PictureId == LibraryBook.Book.PictureId)
- {
- Cover = AvaloniaUtils.TryLoadImageOrDefault(e.Picture, PictureSize._80x80);
- PictureStorage.PictureCached -= PictureStorage_PictureCached;
- }
- }
-
- public async Task ProcessOneAsync()
- {
- string procName = CurrentProcessable.Name;
- ProcessBookResult result = ProcessBookResult.None;
- try
- {
- LinkProcessable(CurrentProcessable);
-
- var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
-
- if (statusHandler.IsSuccess)
- result = ProcessBookResult.Success;
- else if (statusHandler.Errors.Contains("Cancelled"))
- {
- Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
- result = ProcessBookResult.Cancelled;
- }
- else if (statusHandler.Errors.Contains("Validation failed"))
- {
- Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
- result = ProcessBookResult.ValidationFail;
- }
- else
- {
- foreach (var errorMessage in statusHandler.Errors)
- Logger.Error($"{procName}: {errorMessage}");
- }
- }
- catch (ContentLicenseDeniedException ldex)
- {
- if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
- {
- Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
- result = ProcessBookResult.LicenseDeniedPossibleOutage;
- }
- else
- {
- Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
- result = ProcessBookResult.LicenseDenied;
- }
- }
- catch (Exception ex)
- {
- Logger.Error(ex, procName);
- }
- finally
- {
- if (result == ProcessBookResult.None)
- result = await showRetry(LibraryBook);
-
- var status = result switch
- {
- ProcessBookResult.Success => ProcessBookStatus.Completed,
- ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
- _ => ProcessBookStatus.Failed,
- };
-
- await Dispatcher.UIThread.InvokeAsync(() => Status = status);
- }
-
- await Dispatcher.UIThread.InvokeAsync(() => Result = result);
- return result;
- }
-
- public async Task CancelAsync()
- {
- try
- {
- if (CurrentProcessable is AudioDecodable audioDecodable)
- await audioDecodable.CancelAsync();
- }
- catch (Exception ex)
- {
- Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
- }
- }
-
- public void AddDownloadPdf() => AddProcessable();
- public void AddDownloadDecryptBook() => AddProcessable();
- public void AddConvertToMp3() => AddProcessable();
-
- private void AddProcessable() where T : Processable, new()
- {
- Processes.Enqueue(() => new T());
- }
-
- public override string ToString() => LibraryBook.ToString();
-
- #region Subscribers and Unsubscribers
-
- private void LinkProcessable(Processable processable)
- {
- processable.Begin += Processable_Begin;
- processable.Completed += Processable_Completed;
- processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
- processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
-
- if (processable is AudioDecodable audioDecodable)
- {
- audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
- audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
- audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
- audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
- audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
- }
- }
-
- private void UnlinkProcessable(Processable processable)
- {
- processable.Begin -= Processable_Begin;
- processable.Completed -= Processable_Completed;
- processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
- processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
-
- if (processable is AudioDecodable audioDecodable)
- {
- audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
- audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
- audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
- audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
- audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
- }
- }
-
- #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 byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
- {
- var quality
- = Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
- ? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
- : new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
-
- byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
-
- AudioDecodable_CoverImageDiscovered(this, coverData);
- return coverData;
- }
-
- private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
- {
- using var ms = new System.IO.MemoryStream(coverArt);
- Cover = new Avalonia.Media.Imaging.Bitmap(ms);
- }
-
- #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)
- return;
-
- if (downloadProgress.ProgressPercentage == 0)
- TimeRemaining = TimeSpan.Zero;
- else
- Progress = (int)downloadProgress.ProgressPercentage;
- }
-
- #endregion
-
- #region Processable event handlers
-
- private async void Processable_Begin(object? sender, LibraryBook libraryBook)
- {
- await Dispatcher.UIThread.InvokeAsync(() => Status = ProcessBookStatus.Working);
-
- if (sender is Processable processable)
- Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
-
- Title = libraryBook.Book.TitleWithSubtitle;
- Author = libraryBook.Book.AuthorNames();
- Narrator = libraryBook.Book.NarratorNames();
- }
-
- private async void Processable_Completed(object? sender, LibraryBook libraryBook)
- {
- if (sender is Processable processable)
- {
- Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
- UnlinkProcessable(processable);
- }
-
- if (Processes.Count == 0)
- {
- Completed?.Invoke(this, EventArgs.Empty);
- return;
- }
-
- NextProcessable();
- LinkProcessable(CurrentProcessable);
-
- StatusHandler result;
- try
- {
- result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, $"{nameof(Processable_Completed)} error");
-
- result = new StatusHandler();
- result.AddError($"{nameof(Processable_Completed)} error. See log for details. Error summary: {ex.Message}");
- }
-
- if (result.HasErrors)
- {
- foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
- Logger.Error(errorMessage);
-
- Completed?.Invoke(this, EventArgs.Empty);
- }
- }
-
- #endregion
-
- #region Failure Handler
-
- private async Task showRetry(LibraryBook libraryBook)
- {
- Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
-
- 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
- };
-
- string details;
- try
- {
- static string trunc(string str)
- => string.IsNullOrWhiteSpace(str) ? "[empty]"
- : (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())}";
- }
- catch
- {
- details = "[Error retrieving details]";
- }
-
- // if null then ask user
- dialogResult ??= await MessageBox.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 => @"
-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
-}
+}
\ No newline at end of file
diff --git a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs
index 7c9430aa..7eed8118 100644
--- a/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs
+++ b/Source/LibationAvalonia/ViewModels/ProcessQueueViewModel.cs
@@ -6,6 +6,7 @@ using DataLayer;
using LibationFileManager;
using LibationUiBase;
using LibationUiBase.Forms;
+using LibationUiBase.ProcessQueue;
using ReactiveUI;
using System;
using System.Collections.Generic;
diff --git a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
index b79ea913..9e66987d 100644
--- a/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProcessBookControl.axaml.cs
@@ -4,6 +4,7 @@ using Avalonia.Controls;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase;
+using LibationUiBase.ProcessQueue;
namespace LibationAvalonia.Views
{
diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
index de001556..8eda2373 100644
--- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs
@@ -5,6 +5,7 @@ using Avalonia.Data.Converters;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase;
+using LibationUiBase.ProcessQueue;
using System;
using System.Collections.Generic;
using System.Globalization;
diff --git a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs
new file mode 100644
index 00000000..1b535168
--- /dev/null
+++ b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs
@@ -0,0 +1,420 @@
+using ApplicationServices;
+using AudibleApi;
+using AudibleApi.Common;
+using DataLayer;
+using Dinah.Core;
+using Dinah.Core.ErrorHandling;
+using FileLiberator;
+using LibationFileManager;
+using LibationUiBase.Forms;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+#nullable enable
+namespace LibationUiBase.ProcessQueue;
+
+public enum ProcessBookResult
+{
+ None,
+ Success,
+ Cancelled,
+ ValidationFail,
+ FailedRetry,
+ FailedSkip,
+ FailedAbort,
+ LicenseDenied,
+ LicenseDeniedPossibleOutage
+}
+
+public enum ProcessBookStatus
+{
+ Queued,
+ Cancelled,
+ Working,
+ Completed,
+ Failed
+}
+
+///
+/// This is the viewmodel for queued processables
+///
+public abstract class ProcessBookViewModelBase : ReactiveObject
+{
+ public event EventHandler? Completed;
+
+ private readonly LogMe Logger;
+ public LibraryBook LibraryBook { get; protected set; }
+
+ private ProcessBookResult _result = ProcessBookResult.None;
+ private ProcessBookStatus _status = ProcessBookStatus.Queued;
+ private string? _narrator;
+ private string? _author;
+ private string? _title;
+ private int _progress;
+ private string? _eta;
+ private object? _cover;
+ private TimeSpan _timeRemaining;
+
+ #region Properties exposed to the view
+ public ProcessBookResult Result { get => _result; set { RaiseAndSetIfChanged(ref _result, value); RaisePropertyChanged(nameof(StatusText)); } }
+ public ProcessBookStatus Status { get => _status; set { RaiseAndSetIfChanged(ref _status, value); RaisePropertyChanged(nameof(IsFinished)); RaisePropertyChanged(nameof(IsDownloading)); RaisePropertyChanged(nameof(Queued)); } }
+ public string? Narrator { get => _narrator; set => RaiseAndSetIfChanged(ref _narrator, value); }
+ public string? Author { get => _author; set => RaiseAndSetIfChanged(ref _author, value); }
+ public string? Title { get => _title; set => RaiseAndSetIfChanged(ref _title, value); }
+ public int Progress { get => _progress; protected set => RaiseAndSetIfChanged(ref _progress, value); }
+ public TimeSpan TimeRemaining { get => _timeRemaining; set { RaiseAndSetIfChanged(ref _timeRemaining, value); ETA = $"ETA: {value:mm\\:ss}"; } }
+ public string? ETA { get => _eta; private set => RaiseAndSetIfChanged(ref _eta, value); }
+ public object? Cover { get => _cover; protected set => RaiseAndSetIfChanged(ref _cover, value); }
+ public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
+ public bool IsDownloading => Status is ProcessBookStatus.Working;
+ public bool Queued => Status is ProcessBookStatus.Queued;
+
+ public string StatusText => Result switch
+ {
+ ProcessBookResult.Success => "Finished",
+ ProcessBookResult.Cancelled => "Cancelled",
+ ProcessBookResult.ValidationFail => "Validation fail",
+ ProcessBookResult.FailedRetry => "Error, will retry later",
+ ProcessBookResult.FailedSkip => "Error, Skipping",
+ ProcessBookResult.FailedAbort => "Error, Abort",
+ ProcessBookResult.LicenseDenied => "License Denied",
+ ProcessBookResult.LicenseDeniedPossibleOutage => "Possible Service Interruption",
+ _ => Status.ToString(),
+ };
+
+ #endregion
+
+
+ protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
+ protected Processable? NextProcessable() => _currentProcessable = null;
+ private Processable? _currentProcessable;
+ protected readonly Queue> Processes = new();
+
+
+ protected ProcessBookViewModelBase(LibraryBook libraryBook, LogMe logme)
+ {
+ LibraryBook = libraryBook;
+ Logger = logme;
+
+ _title = LibraryBook.Book.TitleWithSubtitle;
+ _author = LibraryBook.Book.AuthorNames();
+ _narrator = LibraryBook.Book.NarratorNames();
+
+ (bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._80x80));
+
+ if (isDefault)
+ PictureStorage.PictureCached += PictureStorage_PictureCached;
+
+ // Mutable property. Set the field so PropertyChanged isn't fired.
+ _cover = LoadImageFromBytes(picture, PictureSize._80x80);
+ }
+
+ protected abstract object? LoadImageFromBytes(byte[] bytes, PictureSize pictureSize);
+ private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
+ {
+ if (e.Definition.PictureId == LibraryBook.Book.PictureId)
+ {
+ Cover = LoadImageFromBytes(e.Picture, PictureSize._80x80);
+ PictureStorage.PictureCached -= PictureStorage_PictureCached;
+ }
+ }
+ public async Task ProcessOneAsync()
+ {
+ string procName = CurrentProcessable.Name;
+ ProcessBookResult result = ProcessBookResult.None;
+ try
+ {
+ LinkProcessable(CurrentProcessable);
+
+ var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
+
+ if (statusHandler.IsSuccess)
+ result = ProcessBookResult.Success;
+ else if (statusHandler.Errors.Contains("Cancelled"))
+ {
+ Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
+ result = ProcessBookResult.Cancelled;
+ }
+ else if (statusHandler.Errors.Contains("Validation failed"))
+ {
+ Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
+ result = ProcessBookResult.ValidationFail;
+ }
+ else
+ {
+ foreach (var errorMessage in statusHandler.Errors)
+ Logger.Error($"{procName}: {errorMessage}");
+ }
+ }
+ catch (ContentLicenseDeniedException ldex)
+ {
+ if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
+ {
+ Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
+ result = ProcessBookResult.LicenseDeniedPossibleOutage;
+ }
+ else
+ {
+ Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
+ result = ProcessBookResult.LicenseDenied;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, procName);
+ }
+ finally
+ {
+ if (result == ProcessBookResult.None)
+ result = await showRetry(LibraryBook);
+
+ var status = result switch
+ {
+ ProcessBookResult.Success => ProcessBookStatus.Completed,
+ ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
+ _ => ProcessBookStatus.Failed,
+ };
+
+ Status = status;
+ }
+
+ Result = result;
+ return result;
+ }
+
+ public async Task CancelAsync()
+ {
+ try
+ {
+ if (CurrentProcessable is AudioDecodable audioDecodable)
+ await audioDecodable.CancelAsync();
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
+ }
+ }
+
+ public void AddDownloadPdf() => AddProcessable();
+ public void AddDownloadDecryptBook() => AddProcessable();
+ public void AddConvertToMp3() => AddProcessable();
+
+ private void AddProcessable() where T : Processable, new()
+ {
+ Processes.Enqueue(() => new T());
+ }
+
+ public override string ToString() => LibraryBook.ToString();
+
+ #region Subscribers and Unsubscribers
+
+ private void LinkProcessable(Processable processable)
+ {
+ processable.Begin += Processable_Begin;
+ processable.Completed += Processable_Completed;
+ processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
+ processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
+
+ if (processable is AudioDecodable audioDecodable)
+ {
+ audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
+ audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
+ audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
+ audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
+ audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
+ }
+ }
+
+ private void UnlinkProcessable(Processable processable)
+ {
+ processable.Begin -= Processable_Begin;
+ processable.Completed -= Processable_Completed;
+ processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
+ processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
+
+ if (processable is AudioDecodable audioDecodable)
+ {
+ audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
+ audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
+ audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
+ audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
+ audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
+ }
+ }
+
+ #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 byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
+ {
+ var quality
+ = Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
+ ? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
+ : new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
+
+ byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
+
+ AudioDecodable_CoverImageDiscovered(this, coverData);
+ 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)
+ return;
+
+ if (downloadProgress.ProgressPercentage == 0)
+ TimeRemaining = TimeSpan.Zero;
+ else
+ Progress = (int)downloadProgress.ProgressPercentage;
+ }
+
+ #endregion
+
+ #region Processable event handlers
+
+ private void Processable_Begin(object? sender, LibraryBook libraryBook)
+ {
+ Status = ProcessBookStatus.Working;
+
+ if (sender is Processable processable)
+ Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
+
+ Title = libraryBook.Book.TitleWithSubtitle;
+ Author = libraryBook.Book.AuthorNames();
+ Narrator = libraryBook.Book.NarratorNames();
+ }
+
+ private async void Processable_Completed(object? sender, LibraryBook libraryBook)
+ {
+ if (sender is Processable processable)
+ {
+ Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
+ UnlinkProcessable(processable);
+ }
+
+ if (Processes.Count == 0)
+ {
+ Completed?.Invoke(this, EventArgs.Empty);
+ return;
+ }
+
+ NextProcessable();
+ LinkProcessable(CurrentProcessable);
+
+ StatusHandler result;
+ try
+ {
+ result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
+ }
+ catch (Exception ex)
+ {
+ Serilog.Log.Logger.Error(ex, $"{nameof(Processable_Completed)} error");
+
+ result = new StatusHandler();
+ result.AddError($"{nameof(Processable_Completed)} error. See log for details. Error summary: {ex.Message}");
+ }
+
+ if (result.HasErrors)
+ {
+ foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
+ Logger.Error(errorMessage);
+
+ Completed?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ #endregion
+
+ #region Failure Handler
+
+ protected async Task showRetry(LibraryBook libraryBook)
+ {
+ Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
+
+ 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
+ };
+
+ string details;
+ try
+ {
+ static string trunc(string str)
+ => string.IsNullOrWhiteSpace(str) ? "[empty]"
+ : (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())}";
+ }
+ 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 => @"
+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/ReactiveObject.cs b/Source/LibationUiBase/ReactiveObject.cs
new file mode 100644
index 00000000..d7c8b723
--- /dev/null
+++ b/Source/LibationUiBase/ReactiveObject.cs
@@ -0,0 +1,33 @@
+using Dinah.Core.Threading;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+#nullable enable
+namespace LibationUiBase;
+
+public class ReactiveObject : SynchronizeInvoker, INotifyPropertyChanged, INotifyPropertyChanging
+{
+ public event PropertyChangedEventHandler? PropertyChanged;
+ public event PropertyChangingEventHandler? PropertyChanging;
+
+ public void RaisePropertyChanging(PropertyChangingEventArgs args) => this.UIThreadSync(() => PropertyChanging?.Invoke(this, args));
+ public void RaisePropertyChanging(string propertyName) => RaisePropertyChanging(new PropertyChangingEventArgs(propertyName));
+ public void RaisePropertyChanged(PropertyChangedEventArgs args) => this.UIThreadSync(() => PropertyChanged?.Invoke(this, args));
+ public void RaisePropertyChanged(string propertyName) => RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
+
+ public TRet RaiseAndSetIfChanged(ref TRet backingField, TRet newValue, [CallerMemberName] string? propertyName = null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(propertyName, nameof(propertyName));
+
+ if (!EqualityComparer.Default.Equals(backingField, newValue))
+ {
+ RaisePropertyChanging(propertyName);
+ backingField = newValue;
+ RaisePropertyChanged(propertyName!);
+ }
+
+ return newValue;
+ }
+}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs b/Source/LibationWinForms/ProcessQueue/ProcessBook.cs
deleted file mode 100644
index ce09658a..00000000
--- a/Source/LibationWinForms/ProcessQueue/ProcessBook.cs
+++ /dev/null
@@ -1,410 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Drawing;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Threading.Tasks;
-using System.Windows.Forms;
-using ApplicationServices;
-using AudibleApi.Common;
-using AudibleApi;
-using DataLayer;
-using Dinah.Core;
-using Dinah.Core.ErrorHandling;
-using FileLiberator;
-using LibationFileManager;
-using LibationUiBase;
-
-namespace LibationWinForms.ProcessQueue
-{
- public enum ProcessBookResult
- {
- None,
- Success,
- Cancelled,
- ValidationFail,
- FailedRetry,
- FailedSkip,
- FailedAbort,
- LicenseDenied,
- LicenseDeniedPossibleOutage
- }
-
- public enum ProcessBookStatus
- {
- Queued,
- Cancelled,
- Working,
- Completed,
- Failed
- }
-
- ///
- /// This is the viewmodel for queued processables
- ///
- public class ProcessBook : INotifyPropertyChanged
- {
- public event EventHandler Completed;
- public event PropertyChangedEventHandler PropertyChanged;
-
- private ProcessBookResult _result = ProcessBookResult.None;
- private ProcessBookStatus _status = ProcessBookStatus.Queued;
- private string _bookText;
- private int _progress;
- private TimeSpan _timeRemaining;
- private Image _cover;
-
- public ProcessBookResult Result { get => _result; private set { _result = value; NotifyPropertyChanged(); } }
- public ProcessBookStatus Status { get => _status; private set { _status = value; NotifyPropertyChanged(); } }
- public string BookText { get => _bookText; private set { _bookText = value; NotifyPropertyChanged(); } }
- public int Progress { get => _progress; private set { _progress = value; NotifyPropertyChanged(); } }
- public TimeSpan TimeRemaining { get => _timeRemaining; private set { _timeRemaining = value; NotifyPropertyChanged(); } }
- public Image Cover { get => _cover; private set { _cover = value; NotifyPropertyChanged(); } }
-
- public LibraryBook LibraryBook { get; private set; }
- private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
- private Processable NextProcessable() => _currentProcessable = null;
- private Processable _currentProcessable;
- private readonly Queue> Processes = new();
- private readonly LogMe Logger;
-
- public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
- => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
-
- public ProcessBook(LibraryBook libraryBook, LogMe logme)
- {
- LibraryBook = libraryBook;
- Logger = logme;
-
- title = LibraryBook.Book.TitleWithSubtitle;
- authorNames = LibraryBook.Book.AuthorNames();
- narratorNames = LibraryBook.Book.NarratorNames();
- _bookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}";
-
- (bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._80x80));
-
- if (isDefault)
- PictureStorage.PictureCached += PictureStorage_PictureCached;
- _cover = WinFormsUtil.TryLoadImageOrDefault(picture, PictureSize._80x80); ;
-
- }
-
- private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
- {
- if (e.Definition.PictureId == LibraryBook.Book.PictureId)
- {
- Cover = WinFormsUtil.TryLoadImageOrDefault(e.Picture, PictureSize._80x80);
- PictureStorage.PictureCached -= PictureStorage_PictureCached;
- }
- }
-
- public async Task ProcessOneAsync()
- {
- string procName = CurrentProcessable.Name;
- try
- {
- LinkProcessable(CurrentProcessable);
-
- var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
-
- if (statusHandler.IsSuccess)
- return Result = ProcessBookResult.Success;
- else if (statusHandler.Errors.Contains("Cancelled"))
- {
- Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
- return Result = ProcessBookResult.Cancelled;
- }
- else if (statusHandler.Errors.Contains("Validation failed"))
- {
- Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
- return Result = ProcessBookResult.ValidationFail;
- }
-
- foreach (var errorMessage in statusHandler.Errors)
- Logger.Error($"{procName}: {errorMessage}");
- }
- catch (ContentLicenseDeniedException ldex)
- {
- if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
- {
- Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
- return Result = ProcessBookResult.LicenseDeniedPossibleOutage;
- }
- else
- {
- Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
- return Result = ProcessBookResult.LicenseDenied;
- }
- }
- catch (Exception ex)
- {
- Logger.Error(ex, procName);
- }
- finally
- {
- if (Result == ProcessBookResult.None)
- Result = showRetry(LibraryBook);
-
- Status = Result switch
- {
- ProcessBookResult.Success => ProcessBookStatus.Completed,
- ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
- _ => ProcessBookStatus.Failed,
- };
- }
-
- return Result;
- }
-
- public async Task CancelAsync()
- {
- try
- {
- if (CurrentProcessable is AudioDecodable audioDecodable)
- await audioDecodable.CancelAsync();
- }
- catch (Exception ex)
- {
- Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
- }
- }
-
- public void AddDownloadPdf() => AddProcessable();
- public void AddDownloadDecryptBook() => AddProcessable();
- public void AddConvertToMp3() => AddProcessable();
-
- private void AddProcessable() where T : Processable, new()
- {
- Processes.Enqueue(() => new T());
- }
-
- public override string ToString() => LibraryBook.ToString();
-
- #region Subscribers and Unsubscribers
-
- private void LinkProcessable(Processable processable)
- {
- processable.Begin += Processable_Begin;
- processable.Completed += Processable_Completed;
- processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
- processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
-
- if (processable is AudioDecodable audioDecodable)
- {
- audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
- audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
- audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
- audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
- audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
- }
- }
-
- private void UnlinkProcessable(Processable processable)
- {
- processable.Begin -= Processable_Begin;
- processable.Completed -= Processable_Completed;
- processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
- processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
-
- if (processable is AudioDecodable audioDecodable)
- {
- audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
- audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
- audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
- audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
- audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
- }
- }
-
- #endregion
-
- #region AudioDecodable event handlers
-
- private string title;
- private string authorNames;
- private string narratorNames;
- private void AudioDecodable_TitleDiscovered(object sender, string title)
- {
- this.title = title;
- updateBookInfo();
- }
-
- private void AudioDecodable_AuthorsDiscovered(object sender, string authors)
- {
- authorNames = authors;
- updateBookInfo();
- }
-
- private void AudioDecodable_NarratorsDiscovered(object sender, string narrators)
- {
- narratorNames = narrators;
- updateBookInfo();
- }
-
- private void updateBookInfo()
- {
- BookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}";
- }
-
- private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
- {
- var quality
- = Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
- ? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
- : new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
-
- byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
-
- AudioDecodable_CoverImageDiscovered(this, coverData);
- return coverData;
- }
-
- private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
- {
- Cover = WinFormsUtil.TryLoadImageOrDefault(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)
- return;
-
- if (downloadProgress.ProgressPercentage == 0)
- TimeRemaining = TimeSpan.Zero;
- else
- Progress = (int)downloadProgress.ProgressPercentage;
- }
-
- #endregion
-
- #region Processable event handlers
-
- private void Processable_Begin(object sender, LibraryBook libraryBook)
- {
- Status = ProcessBookStatus.Working;
-
- Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
-
- title = libraryBook.Book.TitleWithSubtitle;
- authorNames = libraryBook.Book.AuthorNames();
- narratorNames = libraryBook.Book.NarratorNames();
- updateBookInfo();
- }
-
- private async void Processable_Completed(object sender, LibraryBook libraryBook)
- {
- Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
- UnlinkProcessable((Processable)sender);
-
- if (Processes.Count == 0)
- {
- Completed?.Invoke(this, EventArgs.Empty);
- return;
- }
-
- NextProcessable();
- LinkProcessable(CurrentProcessable);
-
- StatusHandler result;
- try
- {
- result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
- }
- catch (Exception ex)
- {
- Serilog.Log.Logger.Error(ex, $"{nameof(Processable_Completed)} error");
-
- result = new StatusHandler();
- result.AddError($"{nameof(Processable_Completed)} error. See log for details. Error summary: {ex.Message}");
- }
-
- if (result.HasErrors)
- {
- foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
- Logger.Error(errorMessage);
-
- Completed?.Invoke(this, EventArgs.Empty);
- }
- }
-
- #endregion
-
- #region Failure Handler
-
- private ProcessBookResult showRetry(LibraryBook libraryBook)
- {
- Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
-
- 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
- };
-
- string details;
- try
- {
- static string trunc(string str)
- => string.IsNullOrWhiteSpace(str) ? "[empty]"
- : (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())}";
- }
- catch
- {
- details = "[Error retrieving details]";
- }
-
- // if null then ask user
- dialogResult ??= MessageBox.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 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 MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
- private MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
- private DialogResult SkipResult => DialogResult.Ignore;
- }
-
- #endregion
-}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs
index 9f840338..07d24012 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs
@@ -1,4 +1,5 @@
-using System;
+using LibationUiBase.ProcessQueue;
+using System;
using System.Drawing;
using System.Windows.Forms;
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs
new file mode 100644
index 00000000..80d4f405
--- /dev/null
+++ b/Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs
@@ -0,0 +1,16 @@
+using DataLayer;
+using LibationFileManager;
+using LibationUiBase;
+using LibationUiBase.ProcessQueue;
+
+namespace LibationWinForms.ProcessQueue;
+
+public class ProcessBookViewModel : ProcessBookViewModelBase
+{
+ public ProcessBookViewModel(LibraryBook libraryBook, LogMe logme) : base(libraryBook, logme) { }
+
+ protected override object LoadImageFromBytes(byte[] bytes, PictureSize pictureSize)
+ => WinFormsUtil.TryLoadImageOrDefault(bytes, PictureSize._80x80);
+
+ public string BookText => $"{Title}\r\nBy {Author}\r\nNarrated by {Narrator}";
+}
diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
index fce5420a..2251ee37 100644
--- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
+++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs
@@ -2,18 +2,20 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using LibationFileManager;
using LibationUiBase;
+using LibationUiBase.ProcessQueue;
namespace LibationWinForms.ProcessQueue
{
internal partial class ProcessQueueControl : UserControl, ILogForm, IProcessQueue
{
- private TrackedQueue Queue = new();
+ private TrackedQueue Queue = new();
private readonly LogMe Logger;
private int QueuedCount
{
@@ -93,19 +95,19 @@ namespace LibationWinForms.ProcessQueue
}
public bool RemoveCompleted(DataLayer.LibraryBook libraryBook)
- => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBook entry
+ => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModel entry
&& entry.Status is ProcessBookStatus.Completed
&& Queue.RemoveCompleted(entry);
public void AddDownloadPdf(IEnumerable entries)
{
- List procs = new();
+ List procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
- ProcessBook pbook = new(entry, Logger);
+ ProcessBookViewModel pbook = new(entry, Logger);
pbook.PropertyChanged += Pbook_DataAvailable;
pbook.AddDownloadPdf();
procs.Add(pbook);
@@ -117,13 +119,13 @@ namespace LibationWinForms.ProcessQueue
public void AddDownloadDecrypt(IEnumerable entries)
{
- List procs = new();
+ List procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
- ProcessBook pbook = new(entry, Logger);
+ ProcessBookViewModel pbook = new(entry, Logger);
pbook.PropertyChanged += Pbook_DataAvailable;
pbook.AddDownloadDecryptBook();
pbook.AddDownloadPdf();
@@ -136,13 +138,13 @@ namespace LibationWinForms.ProcessQueue
public void AddConvertMp3(IEnumerable entries)
{
- List procs = new();
+ List procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
- ProcessBook pbook = new(entry, Logger);
+ ProcessBookViewModel pbook = new(entry, Logger);
pbook.PropertyChanged += Pbook_DataAvailable;
pbook.AddConvertToMp3();
procs.Add(pbook);
@@ -151,7 +153,7 @@ namespace LibationWinForms.ProcessQueue
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
- private void AddToQueue(IEnumerable pbook)
+ private void AddToQueue(IEnumerable pbook)
{
if (!IsHandleCreated)
CreateControl();
@@ -303,22 +305,22 @@ This error appears to be caused by a temporary interruption of service that some
#region View-Model update event handling
///
- /// Index of the first visible in the
+ /// Index of the first visible in the
///
private int FirstVisible = 0;
///
- /// Number of visible in the
+ /// Number of visible in the
///
private int NumVisible = 0;
///
- /// Controls displaying the state, starting with
+ /// Controls displaying the state, starting with
///
private IReadOnlyList Panels;
///
/// Updates the display of a single at within
///
- /// index of the within the
+ /// index of the within the
/// The nme of the property that needs updating. If null, all properties are updated.
private void UpdateControl(int queueIndex, string propertyName = null)
{
@@ -334,8 +336,8 @@ This error appears to be caused by a temporary interruption of service that some
{
Panels[i].SuspendLayout();
if (propertyName is null or nameof(proc.Cover))
- Panels[i].SetCover(proc.Cover);
- if (propertyName is null or nameof(proc.BookText))
+ 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.BookText);
if (proc.Result != ProcessBookResult.None)
@@ -371,13 +373,13 @@ This error appears to be caused by a temporary interruption of service that some
///
/// View notified the model that a botton was clicked
///
- /// index of the within
+ /// index of the within
/// The clicked control to update
private async void VirtualFlowControl2_ButtonClicked(int queueIndex, string buttonName, ProcessBookControl panelClicked)
{
try
{
- ProcessBook item = Queue[queueIndex];
+ ProcessBookViewModel item = Queue[queueIndex];
if (buttonName == nameof(panelClicked.cancelBtn))
{
if (item is not null)
@@ -432,7 +434,7 @@ This error appears to be caused by a temporary interruption of service that some
///
private void Pbook_DataAvailable(object sender, PropertyChangedEventArgs e)
{
- int index = Queue.IndexOf((ProcessBook)sender);
+ int index = Queue.IndexOf((ProcessBookViewModel)sender);
UpdateControl(index, e.PropertyName);
}