Move ProcessBookViewModel logic into LiationUiBase
This commit is contained in:
parent
b65b1e819b
commit
1cf889eed7
@ -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;
|
||||
|
||||
@ -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) { }
|
||||
|
||||
/// <summary>
|
||||
/// This is the viewmodel for queued processables
|
||||
/// </summary>
|
||||
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<Func<Processable>> 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<ProcessBookResult> 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<DownloadPdf>();
|
||||
public void AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
||||
public void AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
||||
|
||||
private void AddProcessable<T>() 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<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 ??= 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
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.Forms;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@ -4,6 +4,7 @@ using Avalonia.Controls;
|
||||
using DataLayer;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
|
||||
namespace LibationAvalonia.Views
|
||||
{
|
||||
|
||||
@ -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;
|
||||
|
||||
420
Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs
Normal file
420
Source/LibationUiBase/ProcessQueue/ProcessBookViewModelBase.cs
Normal file
@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the viewmodel for queued processables
|
||||
/// </summary>
|
||||
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<Func<Processable>> 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<ProcessBookResult> 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<DownloadPdf>();
|
||||
public void AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
||||
public void AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
||||
|
||||
private void AddProcessable<T>() 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<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 ??= 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
|
||||
|
||||
}
|
||||
33
Source/LibationUiBase/ReactiveObject.cs
Normal file
33
Source/LibationUiBase/ReactiveObject.cs
Normal file
@ -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<TRet>(ref TRet backingField, TRet newValue, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(propertyName, nameof(propertyName));
|
||||
|
||||
if (!EqualityComparer<TRet>.Default.Equals(backingField, newValue))
|
||||
{
|
||||
RaisePropertyChanging(propertyName);
|
||||
backingField = newValue;
|
||||
RaisePropertyChanged(propertyName!);
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the viewmodel for queued processables
|
||||
/// </summary>
|
||||
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<Func<Processable>> 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<ProcessBookResult> 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<DownloadPdf>();
|
||||
public void AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
||||
public void AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
||||
|
||||
private void AddProcessable<T>() 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
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
|
||||
16
Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs
Normal file
16
Source/LibationWinForms/ProcessQueue/ProcessBookViewModel.cs
Normal file
@ -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}";
|
||||
}
|
||||
@ -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<ProcessBook> Queue = new();
|
||||
private TrackedQueue<ProcessBookViewModel> 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<DataLayer.LibraryBook> entries)
|
||||
{
|
||||
List<ProcessBook> procs = new();
|
||||
List<ProcessBookViewModel> 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<DataLayer.LibraryBook> entries)
|
||||
{
|
||||
List<ProcessBook> procs = new();
|
||||
List<ProcessBookViewModel> 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<DataLayer.LibraryBook> entries)
|
||||
{
|
||||
List<ProcessBook> procs = new();
|
||||
List<ProcessBookViewModel> 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<ProcessBook> pbook)
|
||||
private void AddToQueue(IEnumerable<ProcessBookViewModel> 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
|
||||
|
||||
/// <summary>
|
||||
/// Index of the first <see cref="ProcessBook"/> visible in the <see cref="VirtualFlowControl"/>
|
||||
/// Index of the first <see cref="ProcessBookViewModel"/> visible in the <see cref="VirtualFlowControl"/>
|
||||
/// </summary>
|
||||
private int FirstVisible = 0;
|
||||
/// <summary>
|
||||
/// Number of <see cref="ProcessBook"/> visible in the <see cref="VirtualFlowControl"/>
|
||||
/// Number of <see cref="ProcessBookViewModel"/> visible in the <see cref="VirtualFlowControl"/>
|
||||
/// </summary>
|
||||
private int NumVisible = 0;
|
||||
/// <summary>
|
||||
/// Controls displaying the <see cref="ProcessBook"/> state, starting with <see cref="FirstVisible"/>
|
||||
/// Controls displaying the <see cref="ProcessBookViewModel"/> state, starting with <see cref="FirstVisible"/>
|
||||
/// </summary>
|
||||
private IReadOnlyList<ProcessBookControl> Panels;
|
||||
|
||||
/// <summary>
|
||||
/// Updates the display of a single <see cref="ProcessBookControl"/> at <paramref name="queueIndex"/> within <see cref="Queue"/>
|
||||
/// </summary>
|
||||
/// <param name="queueIndex">index of the <see cref="ProcessBook"/> within the <see cref="Queue"/></param>
|
||||
/// <param name="queueIndex">index of the <see cref="ProcessBookViewModel"/> within the <see cref="Queue"/></param>
|
||||
/// <param name="propertyName">The nme of the property that needs updating. If null, all properties are updated.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// View notified the model that a botton was clicked
|
||||
/// </summary>
|
||||
/// <param name="queueIndex">index of the <see cref="ProcessBook"/> within <see cref="Queue"/></param>
|
||||
/// <param name="queueIndex">index of the <see cref="ProcessBookViewModel"/> within <see cref="Queue"/></param>
|
||||
/// <param name="panelClicked">The clicked control to update</param>
|
||||
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
|
||||
/// </summary>
|
||||
private void Pbook_DataAvailable(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
int index = Queue.IndexOf((ProcessBook)sender);
|
||||
int index = Queue.IndexOf((ProcessBookViewModel)sender);
|
||||
UpdateControl(index, e.PropertyName);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user