using DataLayer; using Dinah.Core; using System.Collections.Generic; using System.Linq; using System.Text; namespace LibationUiBase.ProcessQueue; /// Why a title in a backup request cannot be queued for download. internal enum BackupSkipReason { AlreadyDownloaded, PreviousError, AbsentFromLastScan, NoAudioOfItsOwn } /// /// 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. /// internal sealed class BackupRequest { public const string NothingQueuedCaption = "Download not queued"; /// Reasons are reported in this order, which runs from the most to the least common. private static readonly BackupSkipReason[] ReasonOrder = [ BackupSkipReason.AlreadyDownloaded, BackupSkipReason.PreviousError, BackupSkipReason.AbsentFromLastScan, BackupSkipReason.NoAudioOfItsOwn ]; /// The titles the caller asked to back up, including the ones that cannot be queued. public int RequestedCount { get; } public LibraryBook[] Queueable { get; } public IReadOnlyDictionary SkippedByReason { get; } public int SkippedCount => RequestedCount - Queueable.Length; private BackupRequest(int requestedCount, LibraryBook[] queueable, IReadOnlyDictionary skippedByReason) { RequestedCount = requestedCount; Queueable = queueable; SkippedByReason = skippedByReason; } public static BackupRequest Create(IEnumerable libraryBooks) { var requestedCount = 0; var queueable = new List(); var skipped = new Dictionary(); 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); } /// /// 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. /// 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; /// A compact breakdown for the log, eg: "already downloaded: 3, absent from last scan: 1". public string BuildSkippedLogSummary() => SkippedCount == 0 ? "none" : string.Join(", ", OrderedReasons().Select(r => $"{LogName(r.Reason)}: {r.Count}")); /// The dialog body shown when a backup request produced nothing to queue. 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" }; }