Move ProcessQueueViewModel logic into LibationUiBase
Fix UI bug in classic when queue is in popped-out mode.
This commit is contained in:
parent
1cf889eed7
commit
4dab16837e
@ -1,282 +1,40 @@
|
|||||||
using ApplicationServices;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Collections;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationUiBase;
|
|
||||||
using LibationUiBase.Forms;
|
|
||||||
using LibationUiBase.ProcessQueue;
|
using LibationUiBase.ProcessQueue;
|
||||||
using ReactiveUI;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels;
|
||||||
|
|
||||||
|
public class ProcessQueueViewModel : ProcessQueueViewModelBase
|
||||||
{
|
{
|
||||||
|
public override void WriteLine(string text)
|
||||||
public class ProcessQueueViewModel : ViewModelBase, ILogForm, IProcessQueue
|
|
||||||
{
|
{
|
||||||
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
Dispatcher.UIThread.Invoke(() =>
|
||||||
public AvaloniaList<ProcessBookViewModel> Items { get; } = new();
|
LogEntries.Add(new()
|
||||||
public TrackedQueue<ProcessBookViewModel> Queue { get; }
|
|
||||||
public ProcessBookViewModel? SelectedItem { get; set; }
|
|
||||||
public Task? QueueRunner { get; private set; }
|
|
||||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
|
||||||
|
|
||||||
private readonly LogMe Logger;
|
|
||||||
|
|
||||||
public ProcessQueueViewModel()
|
|
||||||
{
|
|
||||||
Logger = LogMe.RegisterForm(this);
|
|
||||||
Queue = new(Items);
|
|
||||||
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
|
|
||||||
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
|
||||||
|
|
||||||
if (Design.IsDesignMode)
|
|
||||||
_ = Configuration.Instance.LibationFiles;
|
|
||||||
|
|
||||||
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int _completedCount;
|
|
||||||
private int _errorCount;
|
|
||||||
private int _queuedCount;
|
|
||||||
private string? _runningTime;
|
|
||||||
private bool _progressBarVisible;
|
|
||||||
private decimal _speedLimit;
|
|
||||||
|
|
||||||
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
|
|
||||||
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
|
|
||||||
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
|
|
||||||
public string? RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
|
||||||
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
|
|
||||||
public bool AnyCompleted => CompletedCount > 0;
|
|
||||||
public bool AnyQueued => QueuedCount > 0;
|
|
||||||
public bool AnyErrors => ErrorCount > 0;
|
|
||||||
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
|
|
||||||
|
|
||||||
public decimal SpeedLimit
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
{
|
||||||
return _speedLimit;
|
LogDate = DateTime.Now,
|
||||||
}
|
LogMessage = text.Trim()
|
||||||
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;
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Invoke(() =>
|
|
||||||
{
|
|
||||||
this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
|
||||||
this.RaisePropertyChanged();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
|
|
||||||
|
|
||||||
ErrorCount = errCount;
|
|
||||||
CompletedCount = completeCount;
|
|
||||||
Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
|
|
||||||
}
|
|
||||||
private void Queue_QueuededCountChanged(object? sender, int cueCount)
|
|
||||||
{
|
|
||||||
QueuedCount = cueCount;
|
|
||||||
Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteLine(string text)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Invoke(() =>
|
|
||||||
LogEntries.Add(new()
|
|
||||||
{
|
|
||||||
LogDate = DateTime.Now,
|
|
||||||
LogMessage = text.Trim()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#region Add Books to Queue
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool RemoveCompleted(LibraryBook libraryBook)
|
|
||||||
=> 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<LibraryBook> entries)
|
|
||||||
{
|
|
||||||
List<ProcessBookViewModel> procs = new();
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
if (isBookInQueue(entry))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ProcessBookViewModel pbook = new(entry, Logger);
|
|
||||||
pbook.AddDownloadPdf();
|
|
||||||
procs.Add(pbook);
|
|
||||||
}
|
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
|
||||||
AddToQueue(procs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddDownloadDecrypt(IEnumerable<LibraryBook> entries)
|
|
||||||
{
|
|
||||||
List<ProcessBookViewModel> procs = new();
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
if (isBookInQueue(entry))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ProcessBookViewModel pbook = new(entry, Logger);
|
|
||||||
pbook.AddDownloadDecryptBook();
|
|
||||||
pbook.AddDownloadPdf();
|
|
||||||
procs.Add(pbook);
|
|
||||||
}
|
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
|
||||||
AddToQueue(procs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddConvertMp3(IEnumerable<LibraryBook> entries)
|
|
||||||
{
|
|
||||||
List<ProcessBookViewModel> procs = new();
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
if (isBookInQueue(entry))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ProcessBookViewModel pbook = new(entry, Logger);
|
|
||||||
pbook.AddConvertToMp3();
|
|
||||||
procs.Add(pbook);
|
|
||||||
}
|
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
|
||||||
AddToQueue(procs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddToQueue(IEnumerable<ProcessBookViewModel> pbook)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Invoke(() =>
|
|
||||||
{
|
|
||||||
Queue.Enqueue(pbook);
|
|
||||||
if (!Running)
|
|
||||||
QueueRunner = QueueLoop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
DateTime StartingTime;
|
|
||||||
private async Task QueueLoop()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Information("Begin processing queue");
|
|
||||||
|
|
||||||
RunningTime = string.Empty;
|
|
||||||
ProgressBarVisible = true;
|
|
||||||
StartingTime = DateTime.Now;
|
|
||||||
|
|
||||||
using var counterTimer = new System.Threading.Timer(CounterTimer_Tick, null, 0, 500);
|
|
||||||
|
|
||||||
bool shownServiceOutageMessage = false;
|
|
||||||
|
|
||||||
while (Queue.MoveNext())
|
|
||||||
{
|
|
||||||
if (Queue.Current is not ProcessBookViewModel nextBook)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Information("Current queue item is empty.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (result == ProcessBookResult.ValidationFail)
|
|
||||||
Queue.ClearCurrent();
|
|
||||||
else if (result == ProcessBookResult.FailedAbort)
|
|
||||||
Queue.ClearQueue();
|
|
||||||
else if (result == ProcessBookResult.FailedSkip)
|
|
||||||
nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error);
|
|
||||||
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
|
|
||||||
{
|
|
||||||
await MessageBox.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.
|
|
||||||
",
|
|
||||||
"Possible Interruption of Service",
|
|
||||||
MessageBoxButtons.OK,
|
|
||||||
MessageBoxIcon.Asterisk);
|
|
||||||
shownServiceOutageMessage = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Serilog.Log.Logger.Information("Completed processing queue");
|
|
||||||
|
|
||||||
Queue_CompletedCountChanged(this, 0);
|
|
||||||
ProgressBarVisible = false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LogEntry
|
public ProcessQueueViewModel() : base(CreateEmptyList())
|
||||||
{
|
{
|
||||||
public DateTime LogDate { get; init; }
|
Items = Queue.UnderlyingList as AvaloniaList<ProcessBookViewModelBase>
|
||||||
public string LogDateString => LogDate.ToShortTimeString();
|
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
|
||||||
public string? LogMessage { get; init; }
|
}
|
||||||
|
|
||||||
|
public AvaloniaList<ProcessBookViewModelBase> Items { get; }
|
||||||
|
protected override ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook)
|
||||||
|
=> new ProcessBookViewModel(libraryBook, Logger);
|
||||||
|
|
||||||
|
private static AvaloniaList<ProcessBookViewModelBase> CreateEmptyList()
|
||||||
|
{
|
||||||
|
if (Design.IsDesignMode)
|
||||||
|
_ = Configuration.Instance.LibationFiles;
|
||||||
|
return new AvaloniaList<ProcessBookViewModelBase>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ namespace LibationAvalonia.Views
|
|||||||
{
|
{
|
||||||
public partial class ProcessQueueControl : UserControl
|
public partial class ProcessQueueControl : UserControl
|
||||||
{
|
{
|
||||||
private TrackedQueue<ProcessBookViewModel>? Queue => _viewModel?.Queue;
|
private TrackedQueue<ProcessBookViewModelBase>? Queue => _viewModel?.Queue;
|
||||||
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
|
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
|
||||||
|
|
||||||
public ProcessQueueControl()
|
public ProcessQueueControl()
|
||||||
|
|||||||
@ -1,82 +0,0 @@
|
|||||||
using DataLayer;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace LibationUiBase;
|
|
||||||
|
|
||||||
public interface IProcessQueue
|
|
||||||
{
|
|
||||||
bool RemoveCompleted(LibraryBook libraryBook);
|
|
||||||
void AddDownloadPdf(IEnumerable<LibraryBook> entries);
|
|
||||||
void AddConvertMp3(IEnumerable<LibraryBook> entries);
|
|
||||||
void AddDownloadDecrypt(IEnumerable<LibraryBook> entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ProcessQueueExtensions
|
|
||||||
{
|
|
||||||
|
|
||||||
public static bool QueueDownloadPdf(this IProcessQueue queue, IList<LibraryBook> libraryBooks)
|
|
||||||
{
|
|
||||||
var needsPdf = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated).ToArray();
|
|
||||||
if (needsPdf.Length > 0)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
|
|
||||||
queue.AddDownloadPdf(needsPdf);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool QueueConvertToMp3(this IProcessQueue queue, IList<LibraryBook> libraryBooks)
|
|
||||||
{
|
|
||||||
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
|
|
||||||
var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray();
|
|
||||||
if (preLiberated.Length > 0)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
|
|
||||||
queue.AddConvertMp3(preLiberated);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool QueueDownloadDecrypt(this IProcessQueue queue, IList<LibraryBook> libraryBooks)
|
|
||||||
{
|
|
||||||
if (libraryBooks.Count == 1)
|
|
||||||
{
|
|
||||||
var item = libraryBooks[0];
|
|
||||||
|
|
||||||
if (item.AbsentFromLastScan)
|
|
||||||
return false;
|
|
||||||
else if(item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
|
|
||||||
{
|
|
||||||
queue.RemoveCompleted(item);
|
|
||||||
Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item);
|
|
||||||
queue.AddDownloadDecrypt([item]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
|
|
||||||
{
|
|
||||||
queue.RemoveCompleted(item);
|
|
||||||
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
|
|
||||||
queue.AddDownloadPdf([item]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
|
|
||||||
if (toLiberate.Length > 0)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length);
|
|
||||||
queue.AddDownloadDecrypt(toLiberate);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
324
Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs
Normal file
324
Source/LibationUiBase/ProcessQueue/ProcessQueueViewModelBase.cs
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
namespace LibationUiBase.ProcessQueue;
|
||||||
|
|
||||||
|
public abstract class ProcessQueueViewModelBase : ReactiveObject, ILogForm
|
||||||
|
{
|
||||||
|
public abstract void WriteLine(string text);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public ProcessQueueViewModelBase(ICollection<ProcessBookViewModelBase>? underlyingList)
|
||||||
|
{
|
||||||
|
Logger = LogMe.RegisterForm(this);
|
||||||
|
Queue = new(underlyingList);
|
||||||
|
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
|
||||||
|
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
||||||
|
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _completedCount;
|
||||||
|
private int _errorCount;
|
||||||
|
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)); } }
|
||||||
|
public int ErrorCount { get => _errorCount; private set { RaiseAndSetIfChanged(ref _errorCount, value); RaisePropertyChanged(nameof(AnyErrors)); } }
|
||||||
|
public string? RunningTime { get => _runningTime; set => RaiseAndSetIfChanged(ref _runningTime, value); }
|
||||||
|
public bool ProgressBarVisible { get => _progressBarVisible; set => RaiseAndSetIfChanged(ref _progressBarVisible, value); }
|
||||||
|
public bool AnyCompleted => CompletedCount > 0;
|
||||||
|
public bool AnyQueued => QueuedCount > 0;
|
||||||
|
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);
|
||||||
|
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
|
||||||
|
|
||||||
|
ErrorCount = errCount;
|
||||||
|
CompletedCount = completeCount;
|
||||||
|
RaisePropertyChanged(nameof(Progress));
|
||||||
|
}
|
||||||
|
private void Queue_QueuededCountChanged(object? sender, int cueCount)
|
||||||
|
{
|
||||||
|
QueuedCount = cueCount;
|
||||||
|
RaisePropertyChanged(nameof(Progress));
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Add Books to Queue
|
||||||
|
|
||||||
|
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
|
||||||
|
{
|
||||||
|
var needsPdf = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated).ToArray();
|
||||||
|
if (needsPdf.Length > 0)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
|
||||||
|
AddDownloadPdf(needsPdf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks)
|
||||||
|
{
|
||||||
|
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
|
||||||
|
var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray();
|
||||||
|
if (preLiberated.Length > 0)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
|
||||||
|
AddConvertMp3(preLiberated);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks)
|
||||||
|
{
|
||||||
|
if (libraryBooks.Count == 1)
|
||||||
|
{
|
||||||
|
var item = libraryBooks[0];
|
||||||
|
|
||||||
|
if (item.AbsentFromLastScan)
|
||||||
|
return false;
|
||||||
|
else if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
RemoveCompleted(item);
|
||||||
|
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
|
||||||
|
AddDownloadPdf([item]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (toLiberate.Length > 0)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length);
|
||||||
|
AddDownloadDecrypt(toLiberate);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 RemoveCompleted(LibraryBook libraryBook)
|
||||||
|
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is ProcessBookViewModelBase entry
|
||||||
|
&& entry.Status is ProcessBookStatus.Completed
|
||||||
|
&& Queue.RemoveCompleted(entry);
|
||||||
|
|
||||||
|
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);
|
||||||
|
AddToQueue(procs);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
AddToQueue(procs);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
AddToQueue(procs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToQueue(IEnumerable<ProcessBookViewModelBase> pbook)
|
||||||
|
{
|
||||||
|
Invoke(() =>
|
||||||
|
{
|
||||||
|
Queue.Enqueue(pbook);
|
||||||
|
if (!Running)
|
||||||
|
QueueRunner = QueueLoop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private DateTime StartingTime;
|
||||||
|
private async Task QueueLoop()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("Begin processing queue");
|
||||||
|
|
||||||
|
RunningTime = string.Empty;
|
||||||
|
ProgressBarVisible = true;
|
||||||
|
StartingTime = DateTime.Now;
|
||||||
|
|
||||||
|
using var counterTimer = new System.Threading.Timer(CounterTimer_Tick, null, 0, 500);
|
||||||
|
|
||||||
|
bool shownServiceOutageMessage = false;
|
||||||
|
|
||||||
|
while (Queue.MoveNext())
|
||||||
|
{
|
||||||
|
if (Queue.Current is not ProcessBookViewModelBase nextBook)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("Current queue item is empty.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (result == ProcessBookResult.ValidationFail)
|
||||||
|
Queue.ClearCurrent();
|
||||||
|
else if (result == ProcessBookResult.FailedAbort)
|
||||||
|
Queue.ClearQueue();
|
||||||
|
else if (result == ProcessBookResult.FailedSkip)
|
||||||
|
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}
|
||||||
|
|
||||||
|
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);
|
||||||
|
shownServiceOutageMessage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Serilog.Log.Logger.Information("Completed processing queue");
|
||||||
|
|
||||||
|
Queue_CompletedCountChanged(this, 0);
|
||||||
|
ProgressBarVisible = false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LogEntry
|
||||||
|
{
|
||||||
|
public DateTime LogDate { get; init; }
|
||||||
|
public string LogDateString => LogDate.ToShortTimeString();
|
||||||
|
public string? LogMessage { get; init; }
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationUiBase
|
namespace LibationUiBase
|
||||||
{
|
{
|
||||||
public enum QueuePosition
|
public enum QueuePosition
|
||||||
@ -34,10 +35,10 @@ namespace LibationUiBase
|
|||||||
*/
|
*/
|
||||||
public class TrackedQueue<T> where T : class
|
public class TrackedQueue<T> where T : class
|
||||||
{
|
{
|
||||||
public event EventHandler<int> CompletedCountChanged;
|
public event EventHandler<int>? CompletedCountChanged;
|
||||||
public event EventHandler<int> QueuededCountChanged;
|
public event EventHandler<int>? QueuededCountChanged;
|
||||||
|
|
||||||
public T Current { get; private set; }
|
public T? Current { get; private set; }
|
||||||
|
|
||||||
public IReadOnlyList<T> Queued => _queued;
|
public IReadOnlyList<T> Queued => _queued;
|
||||||
public IReadOnlyList<T> Completed => _completed;
|
public IReadOnlyList<T> Completed => _completed;
|
||||||
@ -46,9 +47,10 @@ namespace LibationUiBase
|
|||||||
private readonly List<T> _completed = new();
|
private readonly List<T> _completed = new();
|
||||||
private readonly object lockObject = new();
|
private readonly object lockObject = new();
|
||||||
|
|
||||||
private readonly ICollection<T> _underlyingList;
|
private readonly ICollection<T>? _underlyingList;
|
||||||
|
public ICollection<T>? UnderlyingList => _underlyingList;
|
||||||
|
|
||||||
public TrackedQueue(ICollection<T> underlyingList = null)
|
public TrackedQueue(ICollection<T>? underlyingList = null)
|
||||||
{
|
{
|
||||||
_underlyingList = underlyingList;
|
_underlyingList = underlyingList;
|
||||||
}
|
}
|
||||||
@ -169,7 +171,7 @@ namespace LibationUiBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public T FirstOrDefault(Func<T, bool> predicate)
|
public T? FirstOrDefault(Func<T, bool> predicate)
|
||||||
{
|
{
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -17,7 +17,7 @@ namespace LibationWinForms
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var unliberated = await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
|
var unliberated = await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
|
||||||
if (processBookQueue1.QueueDownloadDecrypt(unliberated))
|
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated))
|
||||||
SetQueueCollapseState(false);
|
SetQueueCollapseState(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -28,7 +28,7 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (processBookQueue1.QueueDownloadPdf(await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking())))
|
if (processBookQueue1.ViewModel.QueueDownloadPdf(await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking())))
|
||||||
SetQueueCollapseState(false);
|
SetQueueCollapseState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ namespace LibationWinForms
|
|||||||
"Convert all M4b => Mp3?",
|
"Convert all M4b => Mp3?",
|
||||||
MessageBoxButtons.YesNo,
|
MessageBoxButtons.YesNo,
|
||||||
MessageBoxIcon.Warning);
|
MessageBoxIcon.Warning);
|
||||||
if (result == DialogResult.Yes && processBookQueue1.QueueConvertToMp3(await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking())))
|
if (result == DialogResult.Yes && processBookQueue1.ViewModel.QueueConvertToMp3(await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking())))
|
||||||
SetQueueCollapseState(false);
|
SetQueueCollapseState(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ namespace LibationWinForms
|
|||||||
int WidthChange = 0;
|
int WidthChange = 0;
|
||||||
private void Configure_ProcessQueue()
|
private void Configure_ProcessQueue()
|
||||||
{
|
{
|
||||||
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
|
processBookQueue1.PopoutButton.Click += ProcessBookQueue1_PopOut;
|
||||||
|
|
||||||
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
|
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
|
||||||
int width = this.Width;
|
int width = this.Width;
|
||||||
@ -29,7 +29,7 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (processBookQueue1.QueueDownloadDecrypt(libraryBooks))
|
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks))
|
||||||
SetQueueCollapseState(false);
|
SetQueueCollapseState(false);
|
||||||
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.Audio_Exists())
|
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.Audio_Exists())
|
||||||
{
|
{
|
||||||
@ -54,7 +54,7 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
Serilog.Log.Logger.Information("Begin backing up all {series} episodes", series.LibraryBook);
|
Serilog.Log.Logger.Information("Begin backing up all {series} episodes", series.LibraryBook);
|
||||||
|
|
||||||
if (processBookQueue1.QueueDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated().ToArray()))
|
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated().ToArray()))
|
||||||
SetQueueCollapseState(false);
|
SetQueueCollapseState(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -67,7 +67,7 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (processBookQueue1.QueueConvertToMp3(libraryBooks))
|
if (processBookQueue1.ViewModel.QueueConvertToMp3(libraryBooks))
|
||||||
SetQueueCollapseState(false);
|
SetQueueCollapseState(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -87,10 +87,14 @@ namespace LibationWinForms
|
|||||||
}
|
}
|
||||||
else if (!collapsed && splitContainer1.Panel2Collapsed)
|
else if (!collapsed && splitContainer1.Panel2Collapsed)
|
||||||
{
|
{
|
||||||
|
if (!processBookQueue1.PopoutButton.Visible)
|
||||||
|
//Queue is in popout mode. Do nothing.
|
||||||
|
return;
|
||||||
|
|
||||||
Width += WidthChange;
|
Width += WidthChange;
|
||||||
splitContainer1.Panel2.Controls.Add(processBookQueue1);
|
splitContainer1.Panel2.Controls.Add(processBookQueue1);
|
||||||
splitContainer1.Panel2Collapsed = false;
|
splitContainer1.Panel2Collapsed = false;
|
||||||
processBookQueue1.popoutBtn.Visible = true;
|
processBookQueue1.PopoutButton.Visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed));
|
Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed));
|
||||||
@ -110,7 +114,7 @@ namespace LibationWinForms
|
|||||||
dockForm.FormClosing += DockForm_FormClosing;
|
dockForm.FormClosing += DockForm_FormClosing;
|
||||||
splitContainer1.Panel2.Controls.Remove(processBookQueue1);
|
splitContainer1.Panel2.Controls.Remove(processBookQueue1);
|
||||||
splitContainer1.Panel2Collapsed = true;
|
splitContainer1.Panel2Collapsed = true;
|
||||||
processBookQueue1.popoutBtn.Visible = false;
|
processBookQueue1.PopoutButton.Visible = false;
|
||||||
dockForm.PassControl(processBookQueue1);
|
dockForm.PassControl(processBookQueue1);
|
||||||
dockForm.Show();
|
dockForm.Show();
|
||||||
this.Width -= dockForm.WidthChange;
|
this.Width -= dockForm.WidthChange;
|
||||||
@ -127,7 +131,7 @@ namespace LibationWinForms
|
|||||||
this.Width += dockForm.WidthChange;
|
this.Width += dockForm.WidthChange;
|
||||||
splitContainer1.Panel2.Controls.Add(dockForm.RegainControl());
|
splitContainer1.Panel2.Controls.Add(dockForm.RegainControl());
|
||||||
splitContainer1.Panel2Collapsed = false;
|
splitContainer1.Panel2Collapsed = false;
|
||||||
processBookQueue1.popoutBtn.Visible = true;
|
processBookQueue1.PopoutButton.Visible = true;
|
||||||
dockForm.SaveSizeAndLocation(Configuration.Instance);
|
dockForm.SaveSizeAndLocation(Configuration.Instance);
|
||||||
this.Focus();
|
this.Focus();
|
||||||
toggleQueueHideBtn.Visible = true;
|
toggleQueueHideBtn.Visible = true;
|
||||||
|
|||||||
@ -59,7 +59,7 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (processBookQueue1.QueueDownloadDecrypt(productsDisplay.GetVisible().UnLiberated().ToArray()))
|
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(productsDisplay.GetVisible().UnLiberated().ToArray()))
|
||||||
SetQueueCollapseState(false);
|
SetQueueCollapseState(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@ -11,6 +11,4 @@ public class ProcessBookViewModel : ProcessBookViewModelBase
|
|||||||
|
|
||||||
protected override object LoadImageFromBytes(byte[] bytes, PictureSize pictureSize)
|
protected override object LoadImageFromBytes(byte[] bytes, PictureSize pictureSize)
|
||||||
=> WinFormsUtil.TryLoadImageOrDefault(bytes, PictureSize._80x80);
|
=> WinFormsUtil.TryLoadImageOrDefault(bytes, PictureSize._80x80);
|
||||||
|
|
||||||
public string BookText => $"{Title}\r\nBy {Author}\r\nNarrated by {Narrator}";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,7 +55,6 @@
|
|||||||
this.panel2 = new System.Windows.Forms.Panel();
|
this.panel2 = new System.Windows.Forms.Panel();
|
||||||
this.logCopyBtn = new System.Windows.Forms.Button();
|
this.logCopyBtn = new System.Windows.Forms.Button();
|
||||||
this.clearLogBtn = new System.Windows.Forms.Button();
|
this.clearLogBtn = new System.Windows.Forms.Button();
|
||||||
this.counterTimer = new System.Windows.Forms.Timer(this.components);
|
|
||||||
this.statusStrip1.SuspendLayout();
|
this.statusStrip1.SuspendLayout();
|
||||||
this.tabControl1.SuspendLayout();
|
this.tabControl1.SuspendLayout();
|
||||||
this.tabPage1.SuspendLayout();
|
this.tabPage1.SuspendLayout();
|
||||||
@ -329,11 +328,6 @@
|
|||||||
this.clearLogBtn.UseVisualStyleBackColor = true;
|
this.clearLogBtn.UseVisualStyleBackColor = true;
|
||||||
this.clearLogBtn.Click += new System.EventHandler(this.clearLogBtn_Click);
|
this.clearLogBtn.Click += new System.EventHandler(this.clearLogBtn_Click);
|
||||||
//
|
//
|
||||||
// counterTimer
|
|
||||||
//
|
|
||||||
this.counterTimer.Interval = 950;
|
|
||||||
this.counterTimer.Tick += new System.EventHandler(this.CounterTimer_Tick);
|
|
||||||
//
|
|
||||||
// ProcessQueueControl
|
// ProcessQueueControl
|
||||||
//
|
//
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||||
@ -377,7 +371,6 @@
|
|||||||
private System.Windows.Forms.Panel panel3;
|
private System.Windows.Forms.Panel panel3;
|
||||||
private System.Windows.Forms.Panel panel4;
|
private System.Windows.Forms.Panel panel4;
|
||||||
private System.Windows.Forms.ToolStripStatusLabel runningTimeLbl;
|
private System.Windows.Forms.ToolStripStatusLabel runningTimeLbl;
|
||||||
private System.Windows.Forms.Timer counterTimer;
|
|
||||||
private System.Windows.Forms.DataGridView logDGV;
|
private System.Windows.Forms.DataGridView logDGV;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn timestampColumn;
|
private System.Windows.Forms.DataGridViewTextBoxColumn timestampColumn;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn logEntryColumn;
|
private System.Windows.Forms.DataGridViewTextBoxColumn logEntryColumn;
|
||||||
|
|||||||
@ -1,495 +1,319 @@
|
|||||||
using System;
|
using LibationFileManager;
|
||||||
|
using LibationUiBase;
|
||||||
|
using LibationUiBase.ProcessQueue;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using ApplicationServices;
|
|
||||||
using LibationFileManager;
|
|
||||||
using LibationUiBase;
|
|
||||||
using LibationUiBase.ProcessQueue;
|
|
||||||
|
|
||||||
namespace LibationWinForms.ProcessQueue
|
#nullable enable
|
||||||
|
namespace LibationWinForms.ProcessQueue;
|
||||||
|
|
||||||
|
internal partial class ProcessQueueControl : UserControl
|
||||||
{
|
{
|
||||||
internal partial class ProcessQueueControl : UserControl, ILogForm, IProcessQueue
|
public ProcessQueueViewModel ViewModel { get; } = new();
|
||||||
|
public ToolStripButton PopoutButton { get; } = new()
|
||||||
{
|
{
|
||||||
private TrackedQueue<ProcessBookViewModel> Queue = new();
|
DisplayStyle = ToolStripItemDisplayStyle.Text,
|
||||||
private readonly LogMe Logger;
|
Name = nameof(PopoutButton),
|
||||||
private int QueuedCount
|
Text = "Pop Out",
|
||||||
{
|
TextAlign = ContentAlignment.MiddleCenter,
|
||||||
set
|
Alignment = ToolStripItemAlignment.Right,
|
||||||
{
|
Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
|
||||||
queueNumberLbl.Text = value.ToString();
|
};
|
||||||
queueNumberLbl.Visible = value > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private int ErrorCount
|
|
||||||
{
|
|
||||||
set
|
|
||||||
{
|
|
||||||
errorNumberLbl.Text = value.ToString();
|
|
||||||
errorNumberLbl.Visible = value > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int CompletedCount
|
public ProcessQueueControl()
|
||||||
{
|
{
|
||||||
set
|
InitializeComponent();
|
||||||
{
|
|
||||||
completedNumberLbl.Text = value.ToString();
|
|
||||||
completedNumberLbl.Visible = value > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task QueueRunner { get; private set; }
|
var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
|
||||||
public ToolStripButton popoutBtn = new();
|
statusStrip1.Items.Add(PopoutButton);
|
||||||
|
|
||||||
public ProcessQueueControl()
|
virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData;
|
||||||
{
|
virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
var speedLimitMBps = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
ViewModel.LogWritten += (_, text) => WriteLine(text);
|
||||||
numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
|
ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
|
||||||
|
ViewModel.BookPropertyChanged += ProcessBook_PropertyChanged;
|
||||||
|
Load += ProcessQueueControl_Load;
|
||||||
|
}
|
||||||
|
|
||||||
popoutBtn.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
private void ProcessQueueControl_Load(object? sender, EventArgs e)
|
||||||
popoutBtn.Name = "popoutBtn";
|
{
|
||||||
popoutBtn.Text = "Pop Out";
|
if (DesignMode) return;
|
||||||
popoutBtn.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null));
|
||||||
popoutBtn.Alignment = ToolStripItemAlignment.Right;
|
}
|
||||||
popoutBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
|
|
||||||
statusStrip1.Items.Add(popoutBtn);
|
|
||||||
|
|
||||||
Logger = LogMe.RegisterForm(this);
|
public void WriteLine(string text)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
logDGV.Rows.Add(DateTime.Now, text.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData;
|
private async void cancelAllBtn_Click(object? sender, EventArgs e)
|
||||||
virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
|
{
|
||||||
|
ViewModel.Queue.ClearQueue();
|
||||||
|
if (ViewModel.Queue.Current is not null)
|
||||||
|
await ViewModel.Queue.Current.CancelAsync();
|
||||||
|
virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count;
|
||||||
|
UpdateAllControls();
|
||||||
|
}
|
||||||
|
|
||||||
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
|
private void btnClearFinished_Click(object? sender, EventArgs e)
|
||||||
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
{
|
||||||
|
ViewModel.Queue.ClearCompleted();
|
||||||
Load += ProcessQueueControl_Load;
|
virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count;
|
||||||
}
|
UpdateAllControls();
|
||||||
|
|
||||||
private void ProcessQueueControl_Load(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (DesignMode) return;
|
|
||||||
|
|
||||||
|
if (!ViewModel.Running)
|
||||||
runningTimeLbl.Text = string.Empty;
|
runningTimeLbl.Text = string.Empty;
|
||||||
QueuedCount = 0;
|
}
|
||||||
ErrorCount = 0;
|
|
||||||
CompletedCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool isBookInQueue(DataLayer.LibraryBook libraryBook)
|
private void clearLogBtn_Click(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
logDGV.Rows.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogCopyBtn_Click(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
string logText = string.Join("\r\n", logDGV.Rows.Cast<DataGridViewRow>().Select(r => $"{r.Cells[0].Value}\t{r.Cells[1].Value}"));
|
||||||
|
Clipboard.SetDataObject(logText, false, 5, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogDGV_Resize(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
logDGV.Columns[1].Width = logDGV.Width - logDGV.Columns[0].Width;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region View-Model update event handling
|
||||||
|
|
||||||
|
private void ProcessBook_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not ProcessBookViewModel pbvm)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int index = ViewModel.Queue.IndexOf(pbvm);
|
||||||
|
UpdateControl(index, e.PropertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessQueue_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName is null or nameof(ViewModel.QueuedCount))
|
||||||
{
|
{
|
||||||
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
queueNumberLbl.Text = ViewModel.QueuedCount.ToString();
|
||||||
if (entry == null)
|
queueNumberLbl.Visible = ViewModel.QueuedCount > 0;
|
||||||
return false;
|
virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count;
|
||||||
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
|
|
||||||
return !Queue.RemoveCompleted(entry);
|
|
||||||
else
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
if (e.PropertyName is null or nameof(ViewModel.ErrorCount))
|
||||||
public bool RemoveCompleted(DataLayer.LibraryBook libraryBook)
|
|
||||||
=> 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<ProcessBookViewModel> procs = new();
|
errorNumberLbl.Text = ViewModel.ErrorCount.ToString();
|
||||||
foreach (var entry in entries)
|
errorNumberLbl.Visible = ViewModel.ErrorCount > 0;
|
||||||
{
|
|
||||||
if (isBookInQueue(entry))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ProcessBookViewModel pbook = new(entry, Logger);
|
|
||||||
pbook.PropertyChanged += Pbook_DataAvailable;
|
|
||||||
pbook.AddDownloadPdf();
|
|
||||||
procs.Add(pbook);
|
|
||||||
}
|
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
|
||||||
AddToQueue(procs);
|
|
||||||
}
|
}
|
||||||
|
if (e.PropertyName is null or nameof(ViewModel.CompletedCount))
|
||||||
public void AddDownloadDecrypt(IEnumerable<DataLayer.LibraryBook> entries)
|
|
||||||
{
|
{
|
||||||
List<ProcessBookViewModel> procs = new();
|
completedNumberLbl.Text = ViewModel.CompletedCount.ToString();
|
||||||
foreach (var entry in entries)
|
completedNumberLbl.Visible = ViewModel.CompletedCount > 0;
|
||||||
{
|
|
||||||
if (isBookInQueue(entry))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ProcessBookViewModel pbook = new(entry, Logger);
|
|
||||||
pbook.PropertyChanged += Pbook_DataAvailable;
|
|
||||||
pbook.AddDownloadDecryptBook();
|
|
||||||
pbook.AddDownloadPdf();
|
|
||||||
procs.Add(pbook);
|
|
||||||
}
|
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
|
||||||
AddToQueue(procs);
|
|
||||||
}
|
}
|
||||||
|
if (e.PropertyName is null or nameof(ViewModel.Progress))
|
||||||
public void AddConvertMp3(IEnumerable<DataLayer.LibraryBook> entries)
|
|
||||||
{
|
{
|
||||||
List<ProcessBookViewModel> procs = new();
|
toolStripProgressBar1.Maximum = ViewModel.Queue.Count;
|
||||||
foreach (var entry in entries)
|
toolStripProgressBar1.Value = ViewModel.Queue.Completed.Count;
|
||||||
{
|
|
||||||
if (isBookInQueue(entry))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ProcessBookViewModel pbook = new(entry, Logger);
|
|
||||||
pbook.PropertyChanged += Pbook_DataAvailable;
|
|
||||||
pbook.AddConvertToMp3();
|
|
||||||
procs.Add(pbook);
|
|
||||||
}
|
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
|
|
||||||
AddToQueue(procs);
|
|
||||||
}
|
}
|
||||||
private void AddToQueue(IEnumerable<ProcessBookViewModel> pbook)
|
if (e.PropertyName is null or nameof(ViewModel.ProgressBarVisible))
|
||||||
{
|
{
|
||||||
if (!IsHandleCreated)
|
toolStripProgressBar1.Visible = ViewModel.ProgressBarVisible;
|
||||||
CreateControl();
|
|
||||||
|
|
||||||
BeginInvoke(() =>
|
|
||||||
{
|
|
||||||
Queue.Enqueue(pbook);
|
|
||||||
if (!Running)
|
|
||||||
QueueRunner = QueueLoop();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
if (e.PropertyName is null or nameof(ViewModel.RunningTime))
|
||||||
DateTime StartingTime;
|
|
||||||
private async Task QueueLoop()
|
|
||||||
{
|
{
|
||||||
try
|
runningTimeLbl.Text = ViewModel.RunningTime;
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Information("Begin processing queue");
|
|
||||||
|
|
||||||
StartingTime = DateTime.Now;
|
|
||||||
counterTimer.Start();
|
|
||||||
|
|
||||||
bool shownServiceOutageMessage = false;
|
|
||||||
|
|
||||||
while (Queue.MoveNext())
|
|
||||||
{
|
|
||||||
var nextBook = Queue.Current;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (result == ProcessBookResult.ValidationFail)
|
|
||||||
Queue.ClearCurrent();
|
|
||||||
else if (result == ProcessBookResult.FailedAbort)
|
|
||||||
Queue.ClearQueue();
|
|
||||||
else if (result == ProcessBookResult.FailedSkip)
|
|
||||||
nextBook.LibraryBook.UpdateBookStatus(DataLayer.LiberatedStatus.Error);
|
|
||||||
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
|
|
||||||
{
|
|
||||||
MessageBox.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.
|
|
||||||
",
|
|
||||||
"Possible Interruption of Service",
|
|
||||||
MessageBoxButtons.OK,
|
|
||||||
MessageBoxIcon.Asterisk);
|
|
||||||
shownServiceOutageMessage = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Serilog.Log.Logger.Information("Completed processing queue");
|
|
||||||
|
|
||||||
Queue_CompletedCountChanged(this, 0);
|
|
||||||
counterTimer.Stop();
|
|
||||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
|
||||||
UpdateAllControls();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteLine(string text)
|
|
||||||
{
|
|
||||||
if (IsDisposed) return;
|
|
||||||
|
|
||||||
var timeStamp = DateTime.Now;
|
|
||||||
Invoke(() => logDGV.Rows.Add(timeStamp, text.Trim()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Control event handlers
|
|
||||||
|
|
||||||
private void Queue_CompletedCountChanged(object sender, int e)
|
|
||||||
{
|
|
||||||
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
|
|
||||||
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
|
|
||||||
|
|
||||||
ErrorCount = errCount;
|
|
||||||
CompletedCount = completeCount;
|
|
||||||
UpdateProgressBar();
|
|
||||||
}
|
|
||||||
private void Queue_QueuededCountChanged(object sender, int cueCount)
|
|
||||||
{
|
|
||||||
QueuedCount = cueCount;
|
|
||||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
|
||||||
UpdateProgressBar();
|
|
||||||
}
|
|
||||||
private void UpdateProgressBar()
|
|
||||||
{
|
|
||||||
toolStripProgressBar1.Maximum = Queue.Count;
|
|
||||||
toolStripProgressBar1.Value = Queue.Completed.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void cancelAllBtn_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
Queue.ClearQueue();
|
|
||||||
if (Queue.Current is not null)
|
|
||||||
await Queue.Current.CancelAsync();
|
|
||||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
|
||||||
UpdateAllControls();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void btnClearFinished_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
Queue.ClearCompleted();
|
|
||||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
|
||||||
UpdateAllControls();
|
|
||||||
|
|
||||||
if (!Running)
|
|
||||||
runningTimeLbl.Text = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CounterTimer_Tick(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
string timeToStr(TimeSpan time)
|
|
||||||
{
|
|
||||||
string minsSecs = $"{time:mm\\:ss}";
|
|
||||||
if (time.TotalHours >= 1)
|
|
||||||
return $"{time.TotalHours:F0}:{minsSecs}";
|
|
||||||
return minsSecs;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Running)
|
|
||||||
runningTimeLbl.Text = timeToStr(DateTime.Now - StartingTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearLogBtn_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
logDGV.Rows.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LogCopyBtn_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
string logText = string.Join("\r\n", logDGV.Rows.Cast<DataGridViewRow>().Select(r => $"{r.Cells[0].Value}\t{r.Cells[1].Value}"));
|
|
||||||
Clipboard.SetDataObject(logText, false, 5, 150);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LogDGV_Resize(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
logDGV.Columns[1].Width = logDGV.Width - logDGV.Columns[0].Width;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region View-Model update event handling
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Index of the first <see cref="ProcessBookViewModel"/> visible in the <see cref="VirtualFlowControl"/>
|
|
||||||
/// </summary>
|
|
||||||
private int FirstVisible = 0;
|
|
||||||
/// <summary>
|
|
||||||
/// Number of <see cref="ProcessBookViewModel"/> visible in the <see cref="VirtualFlowControl"/>
|
|
||||||
/// </summary>
|
|
||||||
private int NumVisible = 0;
|
|
||||||
/// <summary>
|
|
||||||
/// 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="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)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int i = queueIndex - FirstVisible;
|
|
||||||
|
|
||||||
if (i > NumVisible || i < 0) return;
|
|
||||||
|
|
||||||
var proc = Queue[queueIndex];
|
|
||||||
|
|
||||||
Invoke(() =>
|
|
||||||
{
|
|
||||||
Panels[i].SuspendLayout();
|
|
||||||
if (propertyName is null or nameof(proc.Cover))
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
Panels[i].SetResult(proc.Result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (propertyName is null or nameof(proc.Status))
|
|
||||||
Panels[i].SetStatus(proc.Status);
|
|
||||||
if (propertyName is null or nameof(proc.Progress))
|
|
||||||
Panels[i].SetProgrss(proc.Progress);
|
|
||||||
if (propertyName is null or nameof(proc.TimeRemaining))
|
|
||||||
Panels[i].SetRemainingTime(proc.TimeRemaining);
|
|
||||||
Panels[i].ResumeLayout();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, "Error updating the queued item's display.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAllControls()
|
|
||||||
{
|
|
||||||
int numToShow = Math.Min(NumVisible, Queue.Count - FirstVisible);
|
|
||||||
|
|
||||||
for (int i = 0; i < numToShow; i++)
|
|
||||||
UpdateControl(FirstVisible + i);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// View notified the model that a botton was clicked
|
|
||||||
/// </summary>
|
|
||||||
/// <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
|
|
||||||
{
|
|
||||||
ProcessBookViewModel item = Queue[queueIndex];
|
|
||||||
if (buttonName == nameof(panelClicked.cancelBtn))
|
|
||||||
{
|
|
||||||
if (item is not null)
|
|
||||||
await item.CancelAsync();
|
|
||||||
Queue.RemoveQueued(item);
|
|
||||||
virtualFlowControl2.VirtualControlCount = Queue.Count;
|
|
||||||
}
|
|
||||||
else if (buttonName == nameof(panelClicked.moveFirstBtn))
|
|
||||||
{
|
|
||||||
Queue.MoveQueuePosition(item, QueuePosition.Fisrt);
|
|
||||||
UpdateAllControls();
|
|
||||||
}
|
|
||||||
else if (buttonName == nameof(panelClicked.moveUpBtn))
|
|
||||||
{
|
|
||||||
Queue.MoveQueuePosition(item, QueuePosition.OneUp);
|
|
||||||
UpdateControl(queueIndex);
|
|
||||||
if (queueIndex > 0)
|
|
||||||
UpdateControl(queueIndex - 1);
|
|
||||||
}
|
|
||||||
else if (buttonName == nameof(panelClicked.moveDownBtn))
|
|
||||||
{
|
|
||||||
Queue.MoveQueuePosition(item, QueuePosition.OneDown);
|
|
||||||
UpdateControl(queueIndex);
|
|
||||||
if (queueIndex + 1 < Queue.Count)
|
|
||||||
UpdateControl(queueIndex + 1);
|
|
||||||
}
|
|
||||||
else if (buttonName == nameof(panelClicked.moveLastBtn))
|
|
||||||
{
|
|
||||||
Queue.MoveQueuePosition(item, QueuePosition.Last);
|
|
||||||
UpdateAllControls();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, "Error handling button click from queued item");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// View needs updating
|
|
||||||
/// </summary>
|
|
||||||
private void VirtualFlowControl1_RequestData(int firstIndex, int numVisible, IReadOnlyList<ProcessBookControl> panelsToFill)
|
|
||||||
{
|
|
||||||
FirstVisible = firstIndex;
|
|
||||||
NumVisible = numVisible;
|
|
||||||
Panels = panelsToFill;
|
|
||||||
UpdateAllControls();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Model updates the view
|
|
||||||
/// </summary>
|
|
||||||
private void Pbook_DataAvailable(object sender, PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
int index = Queue.IndexOf((ProcessBookViewModel)sender);
|
|
||||||
UpdateControl(index, e.PropertyName);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var newValue = (long)(numericUpDown1.Value * 1024 * 1024);
|
|
||||||
|
|
||||||
var config = Configuration.Instance;
|
|
||||||
config.DownloadSpeedLimit = newValue;
|
|
||||||
if (config.DownloadSpeedLimit > newValue)
|
|
||||||
numericUpDown1.Value =
|
|
||||||
numericUpDown1.Value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
numericUpDown1.Increment =
|
|
||||||
numericUpDown1.Value > 100 ? 10
|
|
||||||
: numericUpDown1.Value > 10 ? 1
|
|
||||||
: numericUpDown1.Value > 1 ? 0.1m
|
|
||||||
: 0.01m;
|
|
||||||
|
|
||||||
numericUpDown1.DecimalPlaces =
|
|
||||||
numericUpDown1.Value >= 10 ? 0
|
|
||||||
: numericUpDown1.Value >= 1 ? 1
|
|
||||||
: 2;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public class NumericUpDownSuffix : NumericUpDown
|
|
||||||
|
/// <summary>
|
||||||
|
/// Index of the first <see cref="ProcessBookViewModel"/> visible in the <see cref="VirtualFlowControl"/>
|
||||||
|
/// </summary>
|
||||||
|
private int FirstVisible = 0;
|
||||||
|
/// <summary>
|
||||||
|
/// Number of <see cref="ProcessBookViewModel"/> visible in the <see cref="VirtualFlowControl"/>
|
||||||
|
/// </summary>
|
||||||
|
private int NumVisible = 0;
|
||||||
|
/// <summary>
|
||||||
|
/// 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="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)
|
||||||
{
|
{
|
||||||
[Description("Suffix displayed after numeric value."), Category("Data")]
|
try
|
||||||
[Browsable(true)]
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Always)]
|
|
||||||
[DisallowNull]
|
|
||||||
public string Suffix
|
|
||||||
{
|
{
|
||||||
get => _suffix;
|
int i = queueIndex - FirstVisible;
|
||||||
set
|
|
||||||
|
if (Panels is null || i > NumVisible || i < 0) return;
|
||||||
|
|
||||||
|
var proc = ViewModel.Queue[queueIndex];
|
||||||
|
|
||||||
|
Invoke(() =>
|
||||||
{
|
{
|
||||||
base.Text = string.IsNullOrEmpty(_suffix) ? base.Text : base.Text.Replace(_suffix, value);
|
Panels[i].SuspendLayout();
|
||||||
_suffix = value;
|
if (propertyName is null or nameof(proc.Cover))
|
||||||
ChangingText = true;
|
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.Progress))
|
||||||
|
Panels[i].SetProgrss(proc.Progress);
|
||||||
|
if (propertyName is null or nameof(proc.TimeRemaining))
|
||||||
|
Panels[i].SetRemainingTime(proc.TimeRemaining);
|
||||||
|
Panels[i].ResumeLayout();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error(ex, "Error updating the queued item's display.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAllControls()
|
||||||
|
{
|
||||||
|
int numToShow = Math.Min(NumVisible, ViewModel.Queue.Count - FirstVisible);
|
||||||
|
|
||||||
|
for (int i = 0; i < numToShow; i++)
|
||||||
|
UpdateControl(FirstVisible + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// View notified the model that a botton was clicked
|
||||||
|
/// </summary>
|
||||||
|
/// <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
|
||||||
|
{
|
||||||
|
var item = ViewModel.Queue[queueIndex];
|
||||||
|
if (buttonName == nameof(panelClicked.cancelBtn))
|
||||||
|
{
|
||||||
|
if (item is not null)
|
||||||
|
{
|
||||||
|
await item.CancelAsync();
|
||||||
|
if (ViewModel.Queue.RemoveQueued(item))
|
||||||
|
virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (buttonName == nameof(panelClicked.moveFirstBtn))
|
||||||
|
{
|
||||||
|
ViewModel.Queue.MoveQueuePosition(item, QueuePosition.Fisrt);
|
||||||
|
UpdateAllControls();
|
||||||
|
}
|
||||||
|
else if (buttonName == nameof(panelClicked.moveUpBtn))
|
||||||
|
{
|
||||||
|
ViewModel.Queue.MoveQueuePosition(item, QueuePosition.OneUp);
|
||||||
|
UpdateControl(queueIndex);
|
||||||
|
if (queueIndex > 0)
|
||||||
|
UpdateControl(queueIndex - 1);
|
||||||
|
}
|
||||||
|
else if (buttonName == nameof(panelClicked.moveDownBtn))
|
||||||
|
{
|
||||||
|
ViewModel.Queue.MoveQueuePosition(item, QueuePosition.OneDown);
|
||||||
|
UpdateControl(queueIndex);
|
||||||
|
if (queueIndex + 1 < ViewModel.Queue.Count)
|
||||||
|
UpdateControl(queueIndex + 1);
|
||||||
|
}
|
||||||
|
else if (buttonName == nameof(panelClicked.moveLastBtn))
|
||||||
|
{
|
||||||
|
ViewModel.Queue.MoveQueuePosition(item, QueuePosition.Last);
|
||||||
|
UpdateAllControls();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private string _suffix = string.Empty;
|
catch(Exception ex)
|
||||||
public override string Text
|
|
||||||
{
|
{
|
||||||
get => string.IsNullOrEmpty(Suffix) ? base.Text : base.Text.Replace(Suffix, string.Empty);
|
Serilog.Log.Logger.Error(ex, "Error handling button click from queued item");
|
||||||
set
|
}
|
||||||
{
|
}
|
||||||
if (Value == Minimum)
|
|
||||||
base.Text = "∞";
|
/// <summary>
|
||||||
else
|
/// View needs updating
|
||||||
base.Text = value + Suffix;
|
/// </summary>
|
||||||
}
|
private void VirtualFlowControl1_RequestData(int firstIndex, int numVisible, IReadOnlyList<ProcessBookControl> panelsToFill)
|
||||||
|
{
|
||||||
|
FirstVisible = firstIndex;
|
||||||
|
NumVisible = numVisible;
|
||||||
|
Panels = panelsToFill;
|
||||||
|
UpdateAllControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void numericUpDown1_ValueChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var newValue = (long)(numericUpDown1.Value * 1024 * 1024);
|
||||||
|
|
||||||
|
var config = Configuration.Instance;
|
||||||
|
config.DownloadSpeedLimit = newValue;
|
||||||
|
if (config.DownloadSpeedLimit > newValue)
|
||||||
|
numericUpDown1.Value =
|
||||||
|
numericUpDown1.Value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
numericUpDown1.Increment =
|
||||||
|
numericUpDown1.Value > 100 ? 10
|
||||||
|
: numericUpDown1.Value > 10 ? 1
|
||||||
|
: numericUpDown1.Value > 1 ? 0.1m
|
||||||
|
: 0.01m;
|
||||||
|
|
||||||
|
numericUpDown1.DecimalPlaces =
|
||||||
|
numericUpDown1.Value >= 10 ? 0
|
||||||
|
: numericUpDown1.Value >= 1 ? 1
|
||||||
|
: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NumericUpDownSuffix : NumericUpDown
|
||||||
|
{
|
||||||
|
[Description("Suffix displayed after numeric value."), Category("Data")]
|
||||||
|
[Browsable(true)]
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Always)]
|
||||||
|
[DisallowNull]
|
||||||
|
public string Suffix
|
||||||
|
{
|
||||||
|
get => _suffix;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.Text = string.IsNullOrEmpty(_suffix) ? base.Text : base.Text.Replace(_suffix, value);
|
||||||
|
_suffix = value;
|
||||||
|
ChangingText = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private string _suffix = string.Empty;
|
||||||
|
|
||||||
|
[AllowNull]
|
||||||
|
public override string Text
|
||||||
|
{
|
||||||
|
get => string.IsNullOrEmpty(Suffix) ? base.Text : base.Text.Replace(Suffix, string.Empty);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (Value == Minimum)
|
||||||
|
base.Text = "∞";
|
||||||
|
else
|
||||||
|
base.Text = value + Suffix;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,74 @@
|
|||||||
|
using DataLayer;
|
||||||
|
using LibationUiBase.ProcessQueue;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
namespace LibationWinForms.ProcessQueue;
|
||||||
|
|
||||||
|
internal class ProcessQueueViewModel : ProcessQueueViewModelBase
|
||||||
|
{
|
||||||
|
public event EventHandler<string>? LogWritten;
|
||||||
|
/// <summary>
|
||||||
|
/// Fires when a ProcessBookViewModelBase in the queue has a property changed
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<PropertyChangedEventArgs>? BookPropertyChanged;
|
||||||
|
private ObservableCollection<ProcessBookViewModelBase> Items { get; }
|
||||||
|
|
||||||
|
public ProcessQueueViewModel() : base(CreateEmptyList())
|
||||||
|
{
|
||||||
|
Items = Queue.UnderlyingList as ObservableCollection<ProcessBookViewModelBase>
|
||||||
|
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
|
||||||
|
Items.CollectionChanged += Items_CollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
subscribe(e.NewItems);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
unubscribe(e.OldItems);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
void subscribe(IList? items)
|
||||||
|
{
|
||||||
|
foreach (var item in e.NewItems?.OfType<ProcessBookViewModel>() ?? [])
|
||||||
|
item.PropertyChanged += Item_PropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
void unubscribe(IList? items)
|
||||||
|
{
|
||||||
|
foreach (var item in e.NewItems?.OfType<ProcessBookViewModel>() ?? [])
|
||||||
|
item.PropertyChanged -= Item_PropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Item_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
=> BookPropertyChanged?.Invoke(sender, e);
|
||||||
|
|
||||||
|
public override void WriteLine(string text) => Invoke(() => LogWritten?.Invoke(this, text.Trim()));
|
||||||
|
|
||||||
|
protected override ProcessBookViewModelBase CreateNewBook(LibraryBook libraryBook)
|
||||||
|
=> new ProcessBookViewModel(libraryBook, Logger);
|
||||||
|
|
||||||
|
private static ObservableCollection<ProcessBookViewModelBase> CreateEmptyList()
|
||||||
|
=> new ProcessBookCollection();
|
||||||
|
|
||||||
|
private class ProcessBookCollection : ObservableCollection<ProcessBookViewModelBase>
|
||||||
|
{
|
||||||
|
protected override void ClearItems()
|
||||||
|
{
|
||||||
|
//ObservableCollection doesn't raise Remove for each item on Clear, so we need to do it ourselves.
|
||||||
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, this));
|
||||||
|
base.ClearItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user