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); }