mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-13 06:07:30 -04:00
'Not Downloaded' reads as a claim about the file on disk, but the status it names is really an instruction about the future. Telling someone with a finished audiobook to set it 'Not Downloaded' asks them to assert something they know is false, which is why the question keeps coming up. 'Download Pending' says the same thing about intent without saying anything untrue about the file, and stands alone in a dropdown or a support reply where bare 'Pending' would not. The context menu carriers move from "Set Download status to 'X'" to "Mark as 'X'" so the word does not land twice in one breath, with the accelerator on P to stay clear of Downloaded's D. The persisted enum, the --not-downloaded CLI flag and the IsLiberated search tags are unchanged, so scripts and saved quick filters keep working. WinForms status combo boxes grow from 121 to 150px and the better-quality Mark button from 210 to 240px to fit the longer label. Docs carry 'previously "Not Downloaded"' on first mention, since years of Reddit and GitHub answers use the old name. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
145 lines
5.9 KiB
C#
145 lines
5.9 KiB
C#
using ApplicationServices;
|
|
using DataLayer;
|
|
using Dinah.Core;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
|
|
namespace LibationUiBase.ProcessQueue;
|
|
|
|
/// <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>Why a title cannot be queued. Reported in declaration order.</summary>
|
|
internal sealed record SkipReason(string Label, string Advice = "")
|
|
{
|
|
public static readonly SkipReason AlreadyDownloaded = new("Already downloaded");
|
|
public static readonly SkipReason PreviousError = new("Previously failed to download", "mark it 'Download Pending' to try again");
|
|
public static readonly SkipReason AbsentFromLastScan = new(AbsentFromLastScanUserMessage.Label, AbsentFromLastScanUserMessage.Advice);
|
|
public static readonly SkipReason WaitingToRetry = new("Waiting before trying again after a recent failure", "download the title on its own to try it now");
|
|
|
|
public static readonly SkipReason[] All = [AlreadyDownloaded, PreviousError, AbsentFromLastScan, WaitingToRetry];
|
|
}
|
|
|
|
/// <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 int SkippedCount => RequestedCount - Queueable.Length;
|
|
public int Skipped(SkipReason reason) => skipped.GetValueOrDefault(reason);
|
|
|
|
/// <summary>The titles left out because Libation is waiting before attempting them again.</summary>
|
|
public IReadOnlyList<DeferredDownload> Deferred { get; }
|
|
|
|
private readonly Dictionary<SkipReason, int> skipped;
|
|
|
|
private BackupRequest(int requestedCount, LibraryBook[] queueable, Dictionary<SkipReason, int> skipped, IReadOnlyList<DeferredDownload> deferred)
|
|
{
|
|
RequestedCount = requestedCount;
|
|
Queueable = queueable;
|
|
this.skipped = skipped;
|
|
Deferred = deferred;
|
|
}
|
|
|
|
/// <param name="deferrals">
|
|
/// The titles to leave alone for now. Pass <see cref="DownloadDeferrals.None"/> for a request the user
|
|
/// made about specific titles, which must always be attempted.
|
|
/// </param>
|
|
public static BackupRequest Create(IEnumerable<LibraryBook> libraryBooks, DownloadDeferrals? deferrals = null)
|
|
{
|
|
deferrals ??= DownloadDeferrals.None;
|
|
|
|
var requestedCount = 0;
|
|
var queueable = new List<LibraryBook>();
|
|
var skipped = new Dictionary<SkipReason, int>();
|
|
var deferred = new List<DeferredDownload>();
|
|
|
|
foreach (var libraryBook in libraryBooks)
|
|
{
|
|
requestedCount++;
|
|
|
|
// Asked before the wait, so that a title needing nothing is reported as already downloaded rather
|
|
// than as waiting on a record left over from when it did need something.
|
|
if (GetSkipReason(libraryBook) is SkipReason reason)
|
|
skipped[reason] = skipped.GetValueOrDefault(reason) + 1;
|
|
// Whatever the title needs is waited on. A PDF is fetched through the same license request as the
|
|
// audiobook, so a title needing only its PDF would be refused for that exactly as its audio was.
|
|
else if (deferrals.Find(libraryBook) is DeferredDownload waiting)
|
|
{
|
|
deferred.Add(waiting);
|
|
skipped[SkipReason.WaitingToRetry] = skipped.GetValueOrDefault(SkipReason.WaitingToRetry) + 1;
|
|
}
|
|
else
|
|
queueable.Add(libraryBook);
|
|
}
|
|
|
|
return new BackupRequest(requestedCount, [.. queueable], skipped, deferred);
|
|
}
|
|
|
|
/// <summary>Null when the title can be queued. Absent outranks status: Downloadable is false either way.</summary>
|
|
private static SkipReason? GetSkipReason(LibraryBook libraryBook)
|
|
=> libraryBook.NeedsBookDownload || libraryBook.NeedsPdfDownload ? null
|
|
: libraryBook.AbsentFromLastScan ? SkipReason.AbsentFromLastScan
|
|
: libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.Error ? SkipReason.PreviousError
|
|
: SkipReason.AlreadyDownloaded;
|
|
|
|
/// <summary>A compact breakdown for the log, eg: "already downloaded: 3, absent from your last library scan: 1".</summary>
|
|
public string BuildSkippedLogSummary()
|
|
=> SkippedCount == 0
|
|
? "none"
|
|
: string.Join(", ", Breakdown().Select(b => $"{b.Reason.Label.ToLowerInvariant()}: {b.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();
|
|
|
|
//the count comes before the advice so the numbers stay scannable
|
|
foreach (var (reason, count) in Breakdown())
|
|
sb.AppendLine($"{reason.Label}: {count}{(reason.Advice is "" ? "" : $" ({reason.Advice})")}");
|
|
|
|
if (Deferred.Count > 0)
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine(BuildDeferredDetail(DateTimeOffset.Now));
|
|
}
|
|
|
|
return sb.ToString().TrimEnd();
|
|
}
|
|
|
|
/// <summary>
|
|
/// What Audible said about the waited-on titles and when they come back. Without this the dialog reports a
|
|
/// wait with no way to find out its cause short of reading the log.
|
|
/// </summary>
|
|
public string BuildDeferredDetail(DateTimeOffset now)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("Why Libation is waiting:");
|
|
|
|
foreach (var group in Deferred.GroupBy(d => d.Kind).OrderBy(g => g.Key))
|
|
{
|
|
sb.AppendLine($"- {group.First().KindLabel} ({"title".PluralizeWithCount(group.Count())}). "
|
|
+ $"Next attempt {DeferredDownloadUserMessage.DescribeWhen(group.Min(d => d.RetryAfter), now)}.");
|
|
}
|
|
|
|
return sb.ToString().TrimEnd();
|
|
}
|
|
|
|
private IEnumerable<(SkipReason Reason, int Count)> Breakdown()
|
|
=> SkipReason.All.Where(skipped.ContainsKey).Select(reason => (reason, skipped[reason]));
|
|
}
|