Code cleanup and refactoring for clarity
This commit is contained in:
parent
a3734c76b1
commit
4b7939541a
@ -103,13 +103,11 @@ namespace DataLayer
|
||||
) == true
|
||||
).ToList();
|
||||
|
||||
public static IEnumerable<LibraryBook> UnLiberated(this IEnumerable<LibraryBook> bookList)
|
||||
=> bookList
|
||||
.Where(
|
||||
lb =>
|
||||
!lb.AbsentFromLastScan &&
|
||||
(lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload
|
||||
|| lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
|
||||
);
|
||||
public static bool NeedsPdfDownload(this LibraryBook libraryBook)
|
||||
=> !libraryBook.AbsentFromLastScan && libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated;
|
||||
public static bool NeedsBookDownload(this LibraryBook libraryBook)
|
||||
=> !libraryBook.AbsentFromLastScan && libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload;
|
||||
public static IEnumerable<LibraryBook> UnLiberated(this IEnumerable<LibraryBook> bookList)
|
||||
=> bookList.Where(lb => lb.NeedsPdfDownload() || lb.NeedsBookDownload());
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,41 +20,39 @@ namespace LibationAvalonia
|
||||
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
|
||||
=> ShowCoreAsync(null, text, caption, buttons, icon, defaultButton);
|
||||
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, bool saveAndRestorePosition = true)
|
||||
=> ShowCoreAsync(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1, saveAndRestorePosition);
|
||||
=> ShowCoreAsync(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1, saveAndRestorePosition);
|
||||
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons)
|
||||
=> ShowCoreAsync(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
=> ShowCoreAsync(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(string text, string caption)
|
||||
=> ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
=> ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(string text)
|
||||
=> ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
=> ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton, saveAndRestorePosition);
|
||||
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton, saveAndRestorePosition);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons)
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption)
|
||||
=> ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
=> ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text)
|
||||
=> ShowCoreAsync(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
|
||||
|
||||
public static async Task VerboseLoggingWarning_ShowIfTrue()
|
||||
{
|
||||
// when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured
|
||||
if (Serilog.Log.Logger.IsVerboseEnabled())
|
||||
await Show(@"
|
||||
Warning: verbose logging is enabled.
|
||||
await Show("""
|
||||
Warning: verbose logging is enabled.
|
||||
|
||||
This should be used for debugging only. It creates many
|
||||
more logs and debug files, neither of which are as
|
||||
strictly anonymous.
|
||||
This should be used for debugging only. It creates many
|
||||
more logs and debug files, neither of which are as
|
||||
strictly anonymous.
|
||||
|
||||
When you are finished debugging, it's highly recommended
|
||||
to set your debug MinimumLevel to Information and restart
|
||||
Libation.
|
||||
".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
When you are finished debugging, it's highly recommended
|
||||
to set your debug MinimumLevel to Information and restart
|
||||
Libation.
|
||||
""", "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -94,7 +92,8 @@ Libation.
|
||||
{
|
||||
// for development and debugging, show me what broke!
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
throw exception;
|
||||
//Wrap the exception to preserve its stack trace.
|
||||
throw new Exception("An unhandled exception was encountered", exception);
|
||||
|
||||
try
|
||||
{
|
||||
@ -131,7 +130,6 @@ Libation.
|
||||
tbx.MinWidth = vm.TextBlockMinWidth;
|
||||
tbx.Text = message;
|
||||
|
||||
|
||||
var thisScreen = owner.Screens?.ScreenFromVisual(owner);
|
||||
|
||||
var maxSize
|
||||
@ -185,6 +183,5 @@ Libation.
|
||||
return await toDisplay.ShowDialog<DialogResult>(owner);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,30 +5,64 @@ using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels;
|
||||
|
||||
public record LogEntry(DateTime LogDate, string? LogMessage)
|
||||
{
|
||||
public string LogDateString => LogDate.ToShortTimeString();
|
||||
}
|
||||
|
||||
public class ProcessQueueViewModel : ProcessQueueViewModelBase
|
||||
{
|
||||
public override void WriteLine(string text)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
LogEntries.Add(new()
|
||||
{
|
||||
LogDate = DateTime.Now,
|
||||
LogMessage = text.Trim()
|
||||
}));
|
||||
}
|
||||
|
||||
public ProcessQueueViewModel() : base(CreateEmptyList())
|
||||
{
|
||||
Items = Queue.UnderlyingList as AvaloniaList<ProcessBookViewModelBase>
|
||||
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
|
||||
|
||||
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
}
|
||||
|
||||
private decimal _speedLimit;
|
||||
public decimal SpeedLimitIncrement { get; private set; }
|
||||
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||
public AvaloniaList<ProcessBookViewModelBase> Items { get; }
|
||||
protected override ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook)
|
||||
|
||||
public decimal SpeedLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
return _speedLimit;
|
||||
}
|
||||
set
|
||||
{
|
||||
var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024));
|
||||
var config = Configuration.Instance;
|
||||
config.DownloadSpeedLimit = newValue;
|
||||
|
||||
_speedLimit
|
||||
= config.DownloadSpeedLimit <= newValue ? value
|
||||
: value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
|
||||
: 0;
|
||||
|
||||
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
|
||||
|
||||
SpeedLimitIncrement = _speedLimit > 100 ? 10
|
||||
: _speedLimit > 10 ? 1
|
||||
: _speedLimit > 1 ? 0.1m
|
||||
: 0.01m;
|
||||
|
||||
RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
||||
RaisePropertyChanged(nameof(SpeedLimit));
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteLine(string text)
|
||||
=> Dispatcher.UIThread.Invoke(() => LogEntries.Add(new(DateTime.Now, text.Trim())));
|
||||
|
||||
protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook)
|
||||
=> new ProcessBookViewModel(libraryBook, Logger);
|
||||
|
||||
private static AvaloniaList<ProcessBookViewModelBase> CreateEmptyList()
|
||||
|
||||
@ -42,8 +42,6 @@ public enum ProcessBookStatus
|
||||
/// </summary>
|
||||
public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
{
|
||||
public event EventHandler? Completed;
|
||||
|
||||
private readonly LogMe Logger;
|
||||
public LibraryBook LibraryBook { get; protected set; }
|
||||
|
||||
@ -86,12 +84,12 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
||||
protected Processable? NextProcessable() => _currentProcessable = null;
|
||||
protected void NextProcessable() => _currentProcessable = null;
|
||||
private Processable? _currentProcessable;
|
||||
protected readonly Queue<Func<Processable>> Processes = new();
|
||||
|
||||
/// <summary> A series of Processable actions to perform on this book </summary>
|
||||
protected Queue<Func<Processable>> Processes { get; } = new();
|
||||
|
||||
protected ProcessBookViewModelBase(LibraryBook libraryBook, LogMe logme)
|
||||
{
|
||||
@ -120,6 +118,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ProcessBookResult> ProcessOneAsync()
|
||||
{
|
||||
string procName = CurrentProcessable.Name;
|
||||
@ -168,7 +167,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
finally
|
||||
{
|
||||
if (result == ProcessBookResult.None)
|
||||
result = await showRetry(LibraryBook);
|
||||
result = await GetFailureActionAsync(LibraryBook);
|
||||
|
||||
var status = result switch
|
||||
{
|
||||
@ -197,13 +196,14 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
}
|
||||
}
|
||||
|
||||
public void AddDownloadPdf() => AddProcessable<DownloadPdf>();
|
||||
public void AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
||||
public void AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
||||
public ProcessBookViewModelBase AddDownloadPdf() => AddProcessable<DownloadPdf>();
|
||||
public ProcessBookViewModelBase AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
||||
public ProcessBookViewModelBase AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
||||
|
||||
private void AddProcessable<T>() where T : Processable, new()
|
||||
private ProcessBookViewModelBase AddProcessable<T>() where T : Processable, new()
|
||||
{
|
||||
Processes.Enqueue(() => new T());
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() => LibraryBook.ToString();
|
||||
@ -246,16 +246,13 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region AudioDecodable event handlers
|
||||
|
||||
private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title;
|
||||
|
||||
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
|
||||
|
||||
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
|
||||
|
||||
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
|
||||
=> Cover = LoadImageFromBytes(coverArt, PictureSize._80x80);
|
||||
|
||||
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
||||
{
|
||||
@ -270,17 +267,11 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
return coverData;
|
||||
}
|
||||
|
||||
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
|
||||
{
|
||||
Cover = LoadImageFromBytes(coverArt, PictureSize._80x80);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Streamable event handlers
|
||||
|
||||
private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
|
||||
|
||||
|
||||
private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
|
||||
{
|
||||
if (!downloadProgress.ProgressPercentage.HasValue)
|
||||
@ -317,10 +308,7 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
}
|
||||
|
||||
if (Processes.Count == 0)
|
||||
{
|
||||
Completed?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
NextProcessable();
|
||||
LinkProcessable(CurrentProcessable);
|
||||
@ -342,28 +330,39 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
{
|
||||
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
|
||||
Logger.Error(errorMessage);
|
||||
|
||||
Completed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Failure Handler
|
||||
#region Failure Handler
|
||||
|
||||
protected async Task<ProcessBookResult> showRetry(LibraryBook libraryBook)
|
||||
protected async Task<ProcessBookResult> GetFailureActionAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
|
||||
const DialogResult SkipResult = DialogResult.Ignore;
|
||||
Logger.Error($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
|
||||
|
||||
DialogResult? dialogResult = Configuration.Instance.BadBook switch
|
||||
{
|
||||
Configuration.BadBookAction.Abort => DialogResult.Abort,
|
||||
Configuration.BadBookAction.Retry => DialogResult.Retry,
|
||||
Configuration.BadBookAction.Ignore => DialogResult.Ignore,
|
||||
Configuration.BadBookAction.Ask => null,
|
||||
_ => null
|
||||
Configuration.BadBookAction.Ask or _ => await ShowRetryDialogAsync(libraryBook)
|
||||
};
|
||||
|
||||
if (dialogResult == SkipResult)
|
||||
{
|
||||
libraryBook.UpdateBookStatus(LiberatedStatus.Error);
|
||||
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
|
||||
}
|
||||
|
||||
return dialogResult is SkipResult ? ProcessBookResult.FailedSkip
|
||||
: dialogResult is DialogResult.Abort ? ProcessBookResult.FailedAbort
|
||||
: ProcessBookResult.FailedRetry;
|
||||
}
|
||||
|
||||
protected async Task<DialogResult> ShowRetryDialogAsync(LibraryBook libraryBook)
|
||||
{
|
||||
string details;
|
||||
try
|
||||
{
|
||||
@ -372,49 +371,36 @@ public abstract class ProcessBookViewModelBase : ReactiveObject
|
||||
: (str.Length > 50) ? $"{str.Truncate(47)}..."
|
||||
: str;
|
||||
|
||||
details =
|
||||
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
||||
ID: {libraryBook.Book.AudibleProductId}
|
||||
Author: {trunc(libraryBook.Book.AuthorNames())}
|
||||
Narr: {trunc(libraryBook.Book.NarratorNames())}";
|
||||
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);
|
||||
var skipDialogText = $"""
|
||||
An error occurred while trying to process this book.
|
||||
{details}
|
||||
|
||||
- ABORT: Stop processing books.
|
||||
|
||||
- RETRY: Skip this book for now, but retry if it is requeued. Continue processing the queued books.
|
||||
|
||||
- IGNORE: Permanently ignore this book. Continue processing the queued books. (Will not try this book again later.)
|
||||
|
||||
if (dialogResult == DialogResult.Abort)
|
||||
return ProcessBookResult.FailedAbort;
|
||||
See Settings in the Download/Decrypt tab to avoid this box in the future.
|
||||
""";
|
||||
|
||||
if (dialogResult == SkipResult)
|
||||
{
|
||||
libraryBook.UpdateBookStatus(LiberatedStatus.Error);
|
||||
const MessageBoxButtons SkipDialogButtons = MessageBoxButtons.AbortRetryIgnore;
|
||||
const MessageBoxDefaultButton SkipDialogDefaultButton = MessageBoxDefaultButton.Button1;
|
||||
|
||||
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
|
||||
|
||||
return ProcessBookResult.FailedSkip;
|
||||
}
|
||||
|
||||
return ProcessBookResult.FailedRetry;
|
||||
return await MessageBoxBase.Show(skipDialogText, "Skip this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.Forms;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using ApplicationServices;
|
||||
using System.Threading.Tasks;
|
||||
@ -14,24 +12,19 @@ namespace LibationUiBase.ProcessQueue;
|
||||
public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
{
|
||||
public abstract void WriteLine(string text);
|
||||
protected abstract ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook);
|
||||
|
||||
protected abstract ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook);
|
||||
|
||||
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||
public TrackedQueue<ProcessBookViewModelBase> Queue { get; }
|
||||
public ProcessBookViewModelBase? SelectedItem { get; set; }
|
||||
public Task? QueueRunner { get; private set; }
|
||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
||||
|
||||
protected readonly LogMe Logger;
|
||||
protected LogMe Logger { get; }
|
||||
|
||||
public ProcessQueueViewModelBase(ICollection<ProcessBookViewModelBase>? underlyingList)
|
||||
{
|
||||
Logger = LogMe.RegisterForm(this);
|
||||
Queue = new(underlyingList);
|
||||
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
|
||||
Queue.QueuedCountChanged += Queue_QueuedCountChanged;
|
||||
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
||||
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
}
|
||||
|
||||
private int _completedCount;
|
||||
@ -39,7 +32,6 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
private int _queuedCount;
|
||||
private string? _runningTime;
|
||||
private bool _progressBarVisible;
|
||||
private decimal _speedLimit;
|
||||
|
||||
public int CompletedCount { get => _completedCount; private set { RaiseAndSetIfChanged(ref _completedCount, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
|
||||
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); RaisePropertyChanged(nameof(AnyQueued)); } }
|
||||
@ -51,37 +43,6 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
public bool AnyErrors => ErrorCount > 0;
|
||||
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
|
||||
|
||||
public decimal SpeedLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
return _speedLimit;
|
||||
}
|
||||
set
|
||||
{
|
||||
var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024));
|
||||
var config = Configuration.Instance;
|
||||
config.DownloadSpeedLimit = newValue;
|
||||
|
||||
_speedLimit
|
||||
= config.DownloadSpeedLimit <= newValue ? value
|
||||
: value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
|
||||
: 0;
|
||||
|
||||
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
|
||||
|
||||
SpeedLimitIncrement = _speedLimit > 100 ? 10
|
||||
: _speedLimit > 10 ? 1
|
||||
: _speedLimit > 1 ? 0.1m
|
||||
: 0.01m;
|
||||
|
||||
RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
||||
RaisePropertyChanged(nameof(SpeedLimit));
|
||||
}
|
||||
}
|
||||
|
||||
public decimal SpeedLimitIncrement { get; private set; }
|
||||
|
||||
private void Queue_CompletedCountChanged(object? sender, int e)
|
||||
{
|
||||
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
|
||||
@ -91,7 +52,8 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
CompletedCount = completeCount;
|
||||
RaisePropertyChanged(nameof(Progress));
|
||||
}
|
||||
private void Queue_QueuededCountChanged(object? sender, int cueCount)
|
||||
|
||||
private void Queue_QueuedCountChanged(object? sender, int cueCount)
|
||||
{
|
||||
QueuedCount = cueCount;
|
||||
RaisePropertyChanged(nameof(Progress));
|
||||
@ -101,7 +63,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
|
||||
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
|
||||
{
|
||||
var needsPdf = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated).ToArray();
|
||||
var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray();
|
||||
if (needsPdf.Length > 0)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
|
||||
@ -132,14 +94,14 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
|
||||
if (item.AbsentFromLastScan)
|
||||
return false;
|
||||
else if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
|
||||
else if (item.NeedsBookDownload())
|
||||
{
|
||||
RemoveCompleted(item);
|
||||
Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item);
|
||||
AddDownloadDecrypt([item]);
|
||||
return true;
|
||||
}
|
||||
else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
|
||||
else if (item.NeedsPdfDownload())
|
||||
{
|
||||
RemoveCompleted(item);
|
||||
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
|
||||
@ -149,10 +111,7 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
}
|
||||
else
|
||||
{
|
||||
var toLiberate
|
||||
= libraryBooks
|
||||
.Where(x => !x.AbsentFromLastScan && x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
|
||||
.ToArray();
|
||||
var toLiberate = libraryBooks.UnLiberated().ToArray();
|
||||
|
||||
if (toLiberate.Length > 0)
|
||||
{
|
||||
@ -164,16 +123,10 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool isBookInQueue(LibraryBook libraryBook)
|
||||
{
|
||||
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||
if (entry == null)
|
||||
return false;
|
||||
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
|
||||
return !Queue.RemoveCompleted(entry);
|
||||
else
|
||||
return true;
|
||||
}
|
||||
private bool IsBookInQueue(LibraryBook libraryBook)
|
||||
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModelBase entry ? false
|
||||
: entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry)
|
||||
: true;
|
||||
|
||||
private bool RemoveCompleted(LibraryBook libraryBook)
|
||||
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModelBase entry
|
||||
@ -182,69 +135,43 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
|
||||
private void AddDownloadPdf(IEnumerable<LibraryBook> entries)
|
||||
{
|
||||
List<ProcessBookViewModelBase> procs = new();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (isBookInQueue(entry))
|
||||
continue;
|
||||
|
||||
var pbook = CreateNewBook(entry);
|
||||
pbook.AddDownloadPdf();
|
||||
procs.Add(pbook);
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModelBase Create(LibraryBook entry)
|
||||
=> CreateNewProcessBook(entry).AddDownloadPdf();
|
||||
}
|
||||
|
||||
private void AddDownloadDecrypt(IEnumerable<LibraryBook> entries)
|
||||
{
|
||||
List<ProcessBookViewModelBase> procs = new();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (isBookInQueue(entry))
|
||||
continue;
|
||||
|
||||
var pbook = CreateNewBook(entry);
|
||||
pbook.AddDownloadDecryptBook();
|
||||
pbook.AddDownloadPdf();
|
||||
procs.Add(pbook);
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModelBase Create(LibraryBook entry)
|
||||
=> CreateNewProcessBook(entry).AddDownloadDecryptBook().AddDownloadPdf();
|
||||
}
|
||||
|
||||
private void AddConvertMp3(IEnumerable<LibraryBook> entries)
|
||||
{
|
||||
List<ProcessBookViewModelBase> procs = new();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (isBookInQueue(entry))
|
||||
continue;
|
||||
|
||||
var pbook = CreateNewBook(entry);
|
||||
pbook.AddConvertToMp3();
|
||||
procs.Add(pbook);
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModelBase Create(LibraryBook entry)
|
||||
=> CreateNewProcessBook(entry).AddConvertToMp3();
|
||||
}
|
||||
|
||||
private void AddToQueue(IEnumerable<ProcessBookViewModelBase> pbook)
|
||||
{
|
||||
Invoke(() =>
|
||||
{
|
||||
Queue.Enqueue(pbook);
|
||||
if (!Running)
|
||||
QueueRunner = QueueLoop();
|
||||
});
|
||||
Queue.Enqueue(pbook);
|
||||
if (!Running)
|
||||
QueueRunner = Task.Run(QueueLoop);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private DateTime StartingTime;
|
||||
private async Task QueueLoop()
|
||||
{
|
||||
try
|
||||
@ -253,12 +180,11 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
|
||||
RunningTime = string.Empty;
|
||||
ProgressBarVisible = true;
|
||||
StartingTime = DateTime.Now;
|
||||
|
||||
using var counterTimer = new System.Threading.Timer(CounterTimer_Tick, null, 0, 500);
|
||||
|
||||
var startingTime = DateTime.Now;
|
||||
bool shownServiceOutageMessage = false;
|
||||
|
||||
using var counterTimer = new System.Threading.Timer(_ => RunningTime = timeToStr(DateTime.Now - startingTime), null, 0, 500);
|
||||
|
||||
while (Queue.MoveNext())
|
||||
{
|
||||
if (Queue.Current is not ProcessBookViewModelBase nextBook)
|
||||
@ -267,11 +193,11 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
continue;
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook.LibraryBook);
|
||||
Serilog.Log.Logger.Information("Begin processing queued item: '{item_LibraryBook}'", nextBook.LibraryBook);
|
||||
|
||||
var result = await nextBook.ProcessOneAsync();
|
||||
|
||||
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook.LibraryBook, result);
|
||||
Serilog.Log.Logger.Information("Completed processing queued item: '{item_LibraryBook}' with result: {result}", nextBook.LibraryBook, result);
|
||||
|
||||
if (result == ProcessBookResult.ValidationFail)
|
||||
Queue.ClearCurrent();
|
||||
@ -281,11 +207,11 @@ public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||
nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error);
|
||||
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
|
||||
{
|
||||
await MessageBoxBase.Show(@$"
|
||||
You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle}
|
||||
await MessageBoxBase.Show($"""
|
||||
You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle}
|
||||
|
||||
This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
|
||||
",
|
||||
This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
|
||||
""",
|
||||
"Possible Interruption of Service",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Asterisk);
|
||||
@ -301,24 +227,9 @@ This error appears to be caused by a temporary interruption of service that some
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
|
||||
}
|
||||
}
|
||||
|
||||
private void CounterTimer_Tick(object? state)
|
||||
{
|
||||
string timeToStr(TimeSpan time)
|
||||
{
|
||||
string minsSecs = $"{time:mm\\:ss}";
|
||||
if (time.TotalHours >= 1)
|
||||
return $"{time.TotalHours:F0}:{minsSecs}";
|
||||
return minsSecs;
|
||||
}
|
||||
RunningTime = timeToStr(DateTime.Now - StartingTime);
|
||||
=> time.TotalHours < 1 ? $"{time:mm\\:ss}"
|
||||
: $"{time.TotalHours:F0}:{time:mm\\:ss}";
|
||||
}
|
||||
}
|
||||
|
||||
public class LogEntry
|
||||
{
|
||||
public DateTime LogDate { get; init; }
|
||||
public string LogDateString => LogDate.ToShortTimeString();
|
||||
public string? LogMessage { get; init; }
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ namespace LibationUiBase
|
||||
public class TrackedQueue<T> where T : class
|
||||
{
|
||||
public event EventHandler<int>? CompletedCountChanged;
|
||||
public event EventHandler<int>? QueuededCountChanged;
|
||||
public event EventHandler<int>? QueuedCountChanged;
|
||||
|
||||
public T? Current { get; private set; }
|
||||
|
||||
@ -115,7 +115,7 @@ namespace LibationUiBase
|
||||
|
||||
if (itemsRemoved)
|
||||
{
|
||||
QueuededCountChanged?.Invoke(this, queuedCount);
|
||||
QueuedCountChanged?.Invoke(this, queuedCount);
|
||||
RebuildSecondary();
|
||||
}
|
||||
return itemsRemoved;
|
||||
@ -151,7 +151,7 @@ namespace LibationUiBase
|
||||
{
|
||||
lock (lockObject)
|
||||
_queued.Clear();
|
||||
QueuededCountChanged?.Invoke(this, 0);
|
||||
QueuedCountChanged?.Invoke(this, 0);
|
||||
RebuildSecondary();
|
||||
}
|
||||
|
||||
@ -248,7 +248,7 @@ namespace LibationUiBase
|
||||
{
|
||||
if (completedChanged)
|
||||
CompletedCountChanged?.Invoke(this, completedCount);
|
||||
QueuededCountChanged?.Invoke(this, queuedCount);
|
||||
QueuedCountChanged?.Invoke(this, queuedCount);
|
||||
RebuildSecondary();
|
||||
}
|
||||
}
|
||||
@ -263,7 +263,7 @@ namespace LibationUiBase
|
||||
}
|
||||
foreach (var i in item)
|
||||
_underlyingList?.Add(i);
|
||||
QueuededCountChanged?.Invoke(this, queueCount);
|
||||
QueuedCountChanged?.Invoke(this, queueCount);
|
||||
}
|
||||
|
||||
private void RebuildSecondary()
|
||||
|
||||
@ -23,7 +23,8 @@ namespace LibationWinForms
|
||||
{
|
||||
// for development and debugging, show me what broke!
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
throw exception;
|
||||
//Wrap the exception to preserve its stack trace.
|
||||
throw new Exception("An unhandled exception was encountered", exception);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@ -10,7 +10,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
private static int ControlNumberCounter = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The contol's position within <see cref="VirtualFlowControl"/>
|
||||
/// The control's position within <see cref="VirtualFlowControl"/>
|
||||
/// </summary>
|
||||
public int ControlNumber { get; }
|
||||
private ProcessBookStatus Status { get; set; } = ProcessBookStatus.Queued;
|
||||
@ -25,7 +25,6 @@ namespace LibationWinForms.ProcessQueue
|
||||
public ProcessBookControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
statusLbl.Text = "Queued";
|
||||
remainingTimeLbl.Visible = false;
|
||||
progressBar1.Visible = false;
|
||||
etaLbl.Visible = false;
|
||||
@ -45,7 +44,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
bookInfoLbl.Text = title;
|
||||
}
|
||||
|
||||
public void SetProgrss(int progress)
|
||||
public void SetProgress(int progress)
|
||||
{
|
||||
//Disable slow fill
|
||||
//https://stackoverflow.com/a/5332770/3335599
|
||||
@ -59,25 +58,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
remainingTimeLbl.Text = $"{remaining:mm\\:ss}";
|
||||
}
|
||||
|
||||
public void SetResult(ProcessBookResult result)
|
||||
{
|
||||
(string statusText, ProcessBookStatus status) = result switch
|
||||
{
|
||||
ProcessBookResult.Success => ("Finished", ProcessBookStatus.Completed),
|
||||
ProcessBookResult.Cancelled => ("Cancelled", ProcessBookStatus.Cancelled),
|
||||
ProcessBookResult.FailedRetry => ("Error, will retry later", ProcessBookStatus.Failed),
|
||||
ProcessBookResult.FailedSkip => ("Error, Skipping", ProcessBookStatus.Failed),
|
||||
ProcessBookResult.FailedAbort => ("Error, Abort", ProcessBookStatus.Failed),
|
||||
ProcessBookResult.ValidationFail => ("Validation fail", ProcessBookStatus.Failed),
|
||||
ProcessBookResult.LicenseDenied => ("License Denied", ProcessBookStatus.Failed),
|
||||
ProcessBookResult.LicenseDeniedPossibleOutage => ("Possible Service Interruption", ProcessBookStatus.Failed),
|
||||
_ => ("UNKNOWN", ProcessBookStatus.Failed),
|
||||
};
|
||||
|
||||
SetStatus(status, statusText);
|
||||
}
|
||||
|
||||
public void SetStatus(ProcessBookStatus status, string statusText = null)
|
||||
public void SetStatus(ProcessBookStatus status, string statusText)
|
||||
{
|
||||
Status = status;
|
||||
|
||||
@ -101,7 +82,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
progressBar1.Visible = Status == ProcessBookStatus.Working;
|
||||
etaLbl.Visible = Status == ProcessBookStatus.Working;
|
||||
statusLbl.Visible = Status != ProcessBookStatus.Working;
|
||||
statusLbl.Text = statusText ?? Status.ToString();
|
||||
statusLbl.Text = statusText;
|
||||
BackColor = backColor;
|
||||
|
||||
int deltaX = Width - cancelBtn.Location.X - CancelBtnDistanceFromEdge;
|
||||
@ -110,7 +91,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
{
|
||||
//If the last book to occupy this control before resizing was not
|
||||
//queued, the buttons were not Visible so the Anchor property was
|
||||
//ignored. Manually resize and reposition everyhting
|
||||
//ignored. Manually resize and reposition everything
|
||||
|
||||
cancelBtn.Location = new Point(cancelBtn.Location.X + deltaX, cancelBtn.Location.Y);
|
||||
moveFirstBtn.Location = new Point(moveFirstBtn.Location.X + deltaX, moveFirstBtn.Location.Y);
|
||||
|
||||
@ -169,17 +169,10 @@ internal partial class ProcessQueueControl : UserControl
|
||||
Panels[i].SetCover(proc.Cover as Image);
|
||||
if (propertyName is null or nameof(proc.Title) or nameof(proc.Author) or nameof(proc.Narrator))
|
||||
Panels[i].SetBookInfo($"{proc.Title}\r\nBy {proc.Author}\r\nNarrated by {proc.Narrator}");
|
||||
|
||||
if (proc.Result != ProcessBookResult.None)
|
||||
{
|
||||
Panels[i].SetResult(proc.Result);
|
||||
return;
|
||||
}
|
||||
|
||||
if (propertyName is null or nameof(proc.Status))
|
||||
Panels[i].SetStatus(proc.Status);
|
||||
if (propertyName is null or nameof(proc.Status) or nameof(proc.StatusText))
|
||||
Panels[i].SetStatus(proc.Status, proc.StatusText);
|
||||
if (propertyName is null or nameof(proc.Progress))
|
||||
Panels[i].SetProgrss(proc.Progress);
|
||||
Panels[i].SetProgress(proc.Progress);
|
||||
if (propertyName is null or nameof(proc.TimeRemaining))
|
||||
Panels[i].SetRemainingTime(proc.TimeRemaining);
|
||||
Panels[i].ResumeLayout();
|
||||
|
||||
@ -56,7 +56,7 @@ internal class ProcessQueueViewModel : ProcessQueueViewModelBase
|
||||
|
||||
public override void WriteLine(string text) => Invoke(() => LogWritten?.Invoke(this, text.Trim()));
|
||||
|
||||
protected override ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook)
|
||||
protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook)
|
||||
=> new ProcessBookViewModel(libraryBook, Logger);
|
||||
|
||||
private static ObservableCollection<ProcessBookViewModelBase> CreateEmptyList()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user