mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-12 21:57:19 -04:00
fix(queue): explain a multi-book download that queues nothing
The multi-book branch of QueueDownloadDecryptAsync returned false with no log entry and no message whenever UnLiberated() came back empty, so a request Libation understood and declined looked exactly like a dead button. Callers that pre-filter with UnLiberated() land here with an empty list, which is the common way to hit it. Classify each title that cannot be queued and report the breakdown: already downloaded, previously failed, absent from the last scan, or a series parent with no audio of its own. Log it either way; show it only when a person is waiting, so the automatic post-scan download stays silent. Fixes #1940 Co-authored-by: rmcrackan <rmcrackan@gmail.com>
This commit is contained in:
6 files changed
+173
-17
No files matched your search
@@ -90,7 +90,7 @@ partial class MainVM
|
||||
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
|
||||
{
|
||||
// RunWorkerCompleted has no SynchronizationContext; queue items require the UI thread.
|
||||
await Dispatcher.UIThread.InvokeAsync(async () => await BackupAllBooksAsync(stats.LibraryBooks));
|
||||
await Dispatcher.UIThread.InvokeAsync(async () => await BackupAllBooksAsync(stats.LibraryBooks, notifyIfNothingQueued: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,14 @@ partial class MainVM
|
||||
await BackupAllBooksAsync(books);
|
||||
}
|
||||
|
||||
private async Task BackupAllBooksAsync(IEnumerable<LibraryBook> books)
|
||||
/// <param name="notifyIfNothingQueued">False for the automatic post-scan download, which runs unattended.</param>
|
||||
private async Task BackupAllBooksAsync(IEnumerable<LibraryBook> books, bool notifyIfNothingQueued = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var unliberated = books.UnLiberated().ToArray();
|
||||
|
||||
if (await ProcessQueue.QueueDownloadDecryptAsync(unliberated))
|
||||
if (await ProcessQueue.QueueDownloadDecryptAsync(unliberated, notifyIfNothingQueued: notifyIfNothingQueued))
|
||||
setQueueCollapseState(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace LibationUiBase.ProcessQueue;
|
||||
|
||||
/// <summary>Why a title in a backup request cannot be queued for download.</summary>
|
||||
internal enum BackupSkipReason
|
||||
{
|
||||
AlreadyDownloaded,
|
||||
PreviousError,
|
||||
AbsentFromLastScan,
|
||||
NoAudioOfItsOwn
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a multi-book backup request into the titles that can be queued and the reason each of the rest
|
||||
/// was left out, so a request Libation understands and declines can be explained instead of ignored.
|
||||
/// </summary>
|
||||
internal sealed class BackupRequest
|
||||
{
|
||||
public const string NothingQueuedCaption = "Download not queued";
|
||||
|
||||
/// <summary>Reasons are reported in this order, which runs from the most to the least common.</summary>
|
||||
private static readonly BackupSkipReason[] ReasonOrder =
|
||||
[
|
||||
BackupSkipReason.AlreadyDownloaded,
|
||||
BackupSkipReason.PreviousError,
|
||||
BackupSkipReason.AbsentFromLastScan,
|
||||
BackupSkipReason.NoAudioOfItsOwn
|
||||
];
|
||||
|
||||
/// <summary>The titles the caller asked to back up, including the ones that cannot be queued.</summary>
|
||||
public int RequestedCount { get; }
|
||||
public LibraryBook[] Queueable { get; }
|
||||
public IReadOnlyDictionary<BackupSkipReason, int> SkippedByReason { get; }
|
||||
public int SkippedCount => RequestedCount - Queueable.Length;
|
||||
|
||||
private BackupRequest(int requestedCount, LibraryBook[] queueable, IReadOnlyDictionary<BackupSkipReason, int> skippedByReason)
|
||||
{
|
||||
RequestedCount = requestedCount;
|
||||
Queueable = queueable;
|
||||
SkippedByReason = skippedByReason;
|
||||
}
|
||||
|
||||
public static BackupRequest Create(IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
var requestedCount = 0;
|
||||
var queueable = new List<LibraryBook>();
|
||||
var skipped = new Dictionary<BackupSkipReason, int>();
|
||||
|
||||
foreach (var libraryBook in libraryBooks)
|
||||
{
|
||||
requestedCount++;
|
||||
|
||||
if (GetSkipReason(libraryBook) is not BackupSkipReason reason)
|
||||
queueable.Add(libraryBook);
|
||||
else
|
||||
skipped[reason] = skipped.GetValueOrDefault(reason) + 1;
|
||||
}
|
||||
|
||||
return new BackupRequest(requestedCount, [.. queueable], skipped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null when the title can be queued. The order of the checks mirrors LibraryBook.Downloadable: a title
|
||||
/// absent from the last scan is reported as such no matter what its download status says.
|
||||
/// </summary>
|
||||
private static BackupSkipReason? GetSkipReason(LibraryBook libraryBook)
|
||||
=> libraryBook.NeedsBookDownload || libraryBook.NeedsPdfDownload ? null
|
||||
: libraryBook.AbsentFromLastScan ? BackupSkipReason.AbsentFromLastScan
|
||||
: libraryBook.Book.ContentType is not (ContentType.Product or ContentType.Episode) ? BackupSkipReason.NoAudioOfItsOwn
|
||||
: libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.Error ? BackupSkipReason.PreviousError
|
||||
: BackupSkipReason.AlreadyDownloaded;
|
||||
|
||||
/// <summary>A compact breakdown for the log, eg: "already downloaded: 3, absent from last scan: 1".</summary>
|
||||
public string BuildSkippedLogSummary()
|
||||
=> SkippedCount == 0
|
||||
? "none"
|
||||
: string.Join(", ", OrderedReasons().Select(r => $"{LogName(r.Reason)}: {r.Count}"));
|
||||
|
||||
/// <summary>The dialog body shown when a backup request produced nothing to queue.</summary>
|
||||
public string BuildNothingQueuedBody()
|
||||
{
|
||||
if (RequestedCount == 0)
|
||||
return """
|
||||
Libation found no titles that need downloading.
|
||||
|
||||
Titles that are already downloaded, and titles that were absent from your last library scan, are not queued.
|
||||
""";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"None of the {"title".PluralizeWithCount(RequestedCount)} could be queued for download.");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var (reason, count) in OrderedReasons())
|
||||
sb.AppendLine($"{Describe(reason)}: {count}");
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private IEnumerable<(BackupSkipReason Reason, int Count)> OrderedReasons()
|
||||
=> ReasonOrder
|
||||
.Where(SkippedByReason.ContainsKey)
|
||||
.Select(reason => (reason, SkippedByReason[reason]));
|
||||
|
||||
private static string LogName(BackupSkipReason reason)
|
||||
=> reason switch
|
||||
{
|
||||
BackupSkipReason.AlreadyDownloaded => "already downloaded",
|
||||
BackupSkipReason.PreviousError => "previous error",
|
||||
BackupSkipReason.AbsentFromLastScan => "absent from last scan",
|
||||
_ => "no audio of its own"
|
||||
};
|
||||
|
||||
private static string Describe(BackupSkipReason reason)
|
||||
=> reason switch
|
||||
{
|
||||
BackupSkipReason.AlreadyDownloaded => "Already downloaded",
|
||||
BackupSkipReason.PreviousError => "Previously failed to download (set the download status to 'Not Downloaded' to try again)",
|
||||
BackupSkipReason.AbsentFromLastScan => "Absent from your last library scan (run Scan, or `libationcli scan`, then try again)",
|
||||
_ => "Series or podcast parent, which has no audio of its own"
|
||||
};
|
||||
}
|
||||
@@ -186,7 +186,11 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
AddToQueue(procs);
|
||||
}
|
||||
|
||||
public async Task<bool> QueueDownloadDecryptAsync(IList<LibraryBook> libraryBooks, Configuration? config = null)
|
||||
/// <param name="notifyIfNothingQueued">
|
||||
/// Whether to tell the user when a multi-book request queued nothing. Always logged either way. Automated
|
||||
/// callers (auto-download after a scan) pass false so a routine no-op cannot put a dialog on screen.
|
||||
/// </param>
|
||||
public async Task<bool> QueueDownloadDecryptAsync(IList<LibraryBook> libraryBooks, Configuration? config = null, bool notifyIfNothingQueued = true)
|
||||
{
|
||||
config ??= Configuration.Instance;
|
||||
if (!await IsBooksDirectoryValidAsync(config))
|
||||
@@ -237,20 +241,42 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
else
|
||||
{
|
||||
var toLiberate = libraryBooks.UnLiberated().ToArray();
|
||||
var request = BackupRequest.Create(libraryBooks);
|
||||
|
||||
if (toLiberate.Length > 0)
|
||||
if (request.Queueable.Length == 0)
|
||||
{
|
||||
// May no-op when free space is unknown (common on UNC); see DiskSpaceBackupPreflight.
|
||||
if (!await DiskSpaceBackupPreflight.ConfirmBulkBackupAsync(toLiberate.Length, config, backupQueueAlreadyRunning: Running))
|
||||
return false;
|
||||
// This branch used to return with no log entry and no message, so a request Libation had
|
||||
// understood and declined was indistinguishable from a dead button.
|
||||
Serilog.Log.Logger.Information(
|
||||
"Download not queued: none of the {requested} requested titles need downloading. Skipped: {skipped}",
|
||||
request.RequestedCount,
|
||||
request.BuildSkippedLogSummary());
|
||||
|
||||
Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length);
|
||||
AddDownloadDecrypt(toLiberate, config);
|
||||
return true;
|
||||
if (notifyIfNothingQueued)
|
||||
await MessageBoxBase.Show(
|
||||
request.BuildNothingQueuedBody(),
|
||||
BackupRequest.NothingQueuedCaption,
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.SkippedCount > 0)
|
||||
Serilog.Log.Logger.Information(
|
||||
"Skipping {skippedCount} of {requested} requested titles. Skipped: {skipped}",
|
||||
request.SkippedCount,
|
||||
request.RequestedCount,
|
||||
request.BuildSkippedLogSummary());
|
||||
|
||||
// May no-op when free space is unknown (common on UNC); see DiskSpaceBackupPreflight.
|
||||
if (!await DiskSpaceBackupPreflight.ConfirmBulkBackupAsync(request.Queueable.Length, config, backupQueueAlreadyRunning: Running))
|
||||
return false;
|
||||
|
||||
Serilog.Log.Logger.Information("Begin backup of {count} library books", request.Queueable.Length);
|
||||
AddDownloadDecrypt(request.Queueable, config);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> IsBooksDirectoryValidAsync(Configuration config)
|
||||
@@ -334,7 +360,9 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
private void addDownloadDecryptCore(IList<LibraryBook> entries, Configuration config)
|
||||
{
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
|
||||
Serilog.Log.Logger.Information("Queueing {count} books for download/decrypt", procs.Length);
|
||||
if (procs.Length < entries.Count)
|
||||
Serilog.Log.Logger.Information("{count} of the requested books are already in the queue and were not added again", entries.Count - procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModel Create(LibraryBook entry)
|
||||
|
||||
@@ -19,12 +19,13 @@ public partial class Form1
|
||||
await BackupAllBooksAsync(library);
|
||||
}
|
||||
|
||||
private async Task BackupAllBooksAsync(IEnumerable<LibraryBook> books)
|
||||
/// <param name="notifyIfNothingQueued">False for the automatic post-scan download, which runs unattended.</param>
|
||||
private async Task BackupAllBooksAsync(IEnumerable<LibraryBook> books, bool notifyIfNothingQueued = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var unliberated = books.UnLiberated().ToArray();
|
||||
if (await processBookQueue1.ViewModel.QueueDownloadDecryptAsync(unliberated))
|
||||
if (await processBookQueue1.ViewModel.QueueDownloadDecryptAsync(unliberated, notifyIfNothingQueued: notifyIfNothingQueued))
|
||||
SetQueueCollapseState(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -44,7 +44,7 @@ public partial class Form1
|
||||
return;
|
||||
|
||||
// RunWorkerCompleted has no SynchronizationContext; queue items require the UI thread.
|
||||
this.UIThreadAsync(() => _ = BackupAllBooksAsync(libraryStats.LibraryBooks));
|
||||
this.UIThreadAsync(() => _ = BackupAllBooksAsync(libraryStats.LibraryBooks, notifyIfNothingQueued: false));
|
||||
}
|
||||
|
||||
private void AudibleApiStorage_LoadError(object? sender, AccountSettingsLoadErrorEventArgs e)
|
||||
|
||||
Reference in new issue
Block a user