diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs
index babc6c5f..dee07a9c 100644
--- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs
+++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs
@@ -133,18 +133,6 @@ public class ProcessQueueViewModel : ReactiveObject
private void AddQueueLogEntry(string logMessage)
=> Invoke(() => LogEntries.Add(new(DateTime.Now, logMessage.Trim())));
- ///
- /// Clears the queue and cancels the book being processed. Also ends a pause on the daily download limit,
- /// which is why both UIs call this instead of manipulating the queue directly.
- ///
- public async Task CancelAllAsync()
- {
- cancelAllRequested = true;
- Queue.ClearQueue();
- if (Queue.Current is ProcessBookViewModel current)
- await current.CancelAsync();
- }
-
#region Add Books to Queue
public async Task QueueDownloadPdfAsync(IList libraryBooks, Configuration? config = null)
@@ -528,7 +516,10 @@ public class ProcessQueueViewModel : ReactiveObject
/// Moves the book being held back to the end of the queue without counting it as completed.
private void RequeueLast(ProcessBookViewModel book)
{
- Queue.ClearCurrent();
+ // Removes this book, not the first active one. ClearCurrent() drops Active[0], which with
+ // several books in flight is some other book's download: deferring the second of three
+ // active books would silently evict the first instead.
+ Queue.RemoveActive(book);
Queue.Enqueue([book]);
}
@@ -578,12 +569,23 @@ public class ProcessQueueViewModel : ReactiveObject
/// A book that has already finished processing and so does not need cancelling - typically
/// the one whose result triggered the abort.
///
+ ///
+ /// Clears the queue and cancels every book currently downloading. Also ends a pause on the daily
+ /// download limit, which is why both UIs call this instead of manipulating the queue directly.
+ ///
+ ///
+ /// A book calling this from its own completion path (abort, disk full). It has already finished
+ /// and must not be asked to cancel itself.
+ ///
public async Task CancelAllAsync(ProcessBookViewModel? except = null)
{
+ // Still set here, not only in the sequential path this replaced: a queue paused on the daily
+ // download limit is waiting inside WaitForDailyLimitAsync and this flag is how it learns to stop.
+ cancelAllRequested = true;
Queue.ClearQueue();
// Snapshot before cancelling: Active is mutated as each book unwinds.
- var inFlight = Queue.Active.Where(b => b != except).ToArray();
+ var inFlight = Queue.GetActive().Where(b => b != except).ToArray();
await Task.WhenAll(inFlight.Select(b => b.CancelAsync()));
}
@@ -608,6 +610,9 @@ public class ProcessQueueViewModel : ReactiveObject
var _resultLock = new object();
using var abortCts = new CancellationTokenSource();
var activeTasks = new HashSet();
+ // Bounds the daily-limit deferral rotation, so a book can never be shuffled to the back
+ // forever. Counted since the last book that actually started, not since the run began.
+ int consecutiveDeferrals = 0;
using var counterTimer = new Timer(_ => RunningTime = timeToStr(DateTime.Now - startingTime), null, 0, 500);
@@ -707,6 +712,32 @@ public class ProcessQueueViewModel : ReactiveObject
if (Queue.TryDequeueNext(out var nextBook))
{
+ // Checked as a book is about to start rather than at queueing time, so the queue keeps
+ // its contents and the user can raise or turn off the limit mid-run. It belongs in this
+ // single dispatch loop and not inside the book task: the gate decides whether a book may
+ // start at all, and a per-book wait would have every blocked book polling at once.
+ // Books already in flight keep running while the loop is held here.
+ var gate = await WaitForDailyLimitAsync(nextBook, consecutiveDeferrals);
+
+ if (gate is DailyLimitGate.Defer)
+ {
+ consecutiveDeferrals++;
+ RequeueLast(nextBook);
+ continue;
+ }
+
+ if (gate is DailyLimitGate.Cancelled)
+ {
+ Serilog.Log.Logger.Information("Queue was cancelled while waiting on the daily download limit.");
+ nextBook.Result = ProcessBookResult.Cancelled;
+ nextBook.Status = ProcessBookStatus.Cancelled;
+ // The sequential loop left this to the next MoveNext(). Nothing moves this book
+ // off the active list now, so it is retired here.
+ Queue.MarkCompleted(nextBook);
+ continue;
+ }
+
+ consecutiveDeferrals = 0;
activeTasks.Add(ProcessBookAsync(nextBook));
continue;
}