mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-13 14:17:15 -04:00
Reported in issue #1973: a scheduled liberate run re-requested a content license for the same 59 titles every 15 minutes, 1397 refused requests in six hours, because nothing about a failed PDF was remembered and the PDF step asked Audible afresh every time. The two download paths have been converging for a while - a PDF is now named and placed by the audiobook path's own logic, and verified like one - and every part of this bug lives where that stopped short. Both steps ask Audible for the same license. The request asks for pdf_url alongside the content reference, and LicenseInfo dropped it, so DownloadPdf turned round and requested an identical license to read the field the first response had already returned. Carry PdfUrl on LicenseInfo and give both steps one ILicensedDownload contract: a license may be supplied to a step, and the one a step ended up using is published for the next step for the same title. The CLI and the GUI queue hand it on, so a title costs one license request per run however many steps want something from it. A carried license is retried once with a fresh one if it does not work, since Audible's links are signed and a long decrypt can run between the two steps. Where the audiobook step obtained no license there is nothing to hand on and the supplement step does not run, which deletes a bug rather than guarding it: Completed fires from a finally, so a refused audio download was followed at once by a PDF request that reproduced the refusal. Error now means the same for a PDF as for a book. The audiobook step has always skipped LiberatedStatus.Error through AudioExists, and NeedsPdfDownload agrees, but DownloadPdf selected on PdfExists and so retried an errored PDF forever. A license that is granted and carries no pdf_url - the 'No PDF URL available' in the report - is Audible saying the title has no PDF, and is written off that same way instead of failing identically on every run. It stays resettable by everything that resets a book: --force, a named title, Set PDF Not Downloaded. Refusals now reach ProcessSingleAsync, which has always recorded them for whichever step throws one; DownloadPdf swallowed everything and recorded nothing. It keeps swallowing what the classifier does not recognise, which is what stopped a missing PDF from taking the app down with it. A bulk CLI run leaves alone the titles the last scan did not find, by the same Downloadable rule every multi-title path in the app already uses, and the PDF back-fill pass waits on a refused title just as the first pass does. --force and a named title still attempt everything. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
523 lines
19 KiB
C#
523 lines
19 KiB
C#
using ApplicationServices;
|
|
using AudibleApi;
|
|
using AudibleApi.Common;
|
|
using DataLayer;
|
|
using Dinah.Core;
|
|
using Dinah.Core.ErrorHandling;
|
|
using FileLiberator;
|
|
using LibationFileManager;
|
|
using LibationUiBase.Forms;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace LibationUiBase.ProcessQueue;
|
|
|
|
public enum ProcessBookResult
|
|
{
|
|
None,
|
|
Success,
|
|
Cancelled,
|
|
ValidationFail,
|
|
FailedRetry,
|
|
FailedSkip,
|
|
FailedAbort,
|
|
LicenseDenied,
|
|
LicenseDeniedPossibleOutage,
|
|
/// <summary>ADRM licenserequest failed with Sable acr:null; Widevine may work (see WidevineRecommendation).</summary>
|
|
WidevineRecommended,
|
|
/// <summary>Volume full on write; queue should stop (see ProcessQueueViewModel queue loop).</summary>
|
|
DiskFull
|
|
}
|
|
|
|
public enum ProcessBookStatus
|
|
{
|
|
Queued,
|
|
Cancelled,
|
|
Working,
|
|
Completed,
|
|
Failed
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is the viewmodel for queued processables
|
|
/// </summary>
|
|
public class ProcessBookViewModel : ReactiveObject
|
|
{
|
|
public LibraryBook LibraryBook { get; protected set; }
|
|
public Configuration Configuration { get; }
|
|
private readonly BadBookSessionContext? _badBookSession;
|
|
|
|
#region Properties exposed to the view
|
|
public ProcessBookResult Result { get => field; set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(StatusText)); } }
|
|
public ProcessBookStatus Status { get => field; set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(IsFinished)); RaisePropertyChanged(nameof(IsDownloading)); RaisePropertyChanged(nameof(Queued)); } }
|
|
public string? Narrator { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
|
public string? Author { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
|
public string? Title { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
|
public int Progress { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
|
|
public TimeSpan TimeRemaining { get => field; set { RaiseAndSetIfChanged(ref field, value); ETA = $"ETA: {value:mm\\:ss}"; } }
|
|
public string? ETA { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
|
public object? Cover { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
|
|
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
|
|
public bool IsDownloading => Status is ProcessBookStatus.Working;
|
|
public bool Queued => Status is ProcessBookStatus.Queued;
|
|
|
|
/// <summary>
|
|
/// Transient status shown instead of the usual text, e.g. while the queue waits on the daily download limit.
|
|
/// Null when the book's own state should speak for itself.
|
|
/// </summary>
|
|
public string? StatusOverride { get => field; set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(StatusText)); } }
|
|
|
|
/// <summary>
|
|
/// True when this queue item downloads an audiobook, so the daily download limit applies to it.
|
|
/// PDF-only and mp3-conversion items are never limited.
|
|
/// </summary>
|
|
public bool IncludesBookDownload { get; private set; }
|
|
|
|
public string StatusText => StatusOverride ?? (Result, LibraryBook.IsAudiblePlus) switch
|
|
{
|
|
(ProcessBookResult.Success, _) => "Finished",
|
|
(ProcessBookResult.Cancelled, _) => "Cancelled",
|
|
(ProcessBookResult.ValidationFail, _) => "Validation fail",
|
|
(ProcessBookResult.FailedRetry, _) => "Error, will retry later",
|
|
(ProcessBookResult.FailedSkip, _) => "Error, Skipping",
|
|
(ProcessBookResult.FailedAbort, _) => "Error, Abort",
|
|
(ProcessBookResult.LicenseDenied, true) => "License denied (Plus; often temporary)",
|
|
(ProcessBookResult.LicenseDenied, false) => "License Denied",
|
|
(ProcessBookResult.LicenseDeniedPossibleOutage, _) => "Possible Service Interruption",
|
|
(ProcessBookResult.WidevineRecommended, _) => WidevineRecommendationUserMessage.QueueStatusText,
|
|
(ProcessBookResult.DiskFull, _) => "Disk full, queue stopped",
|
|
_ => Status.ToString(),
|
|
};
|
|
|
|
#endregion
|
|
|
|
#region Process Queue Logging
|
|
|
|
public event EventHandler<string>? LogWritten;
|
|
private void OnLogWritten(string text) => LogWritten?.Invoke(this, text.Trim());
|
|
|
|
private void LogError(string? message, Exception? ex = null)
|
|
{
|
|
OnLogWritten(message ?? "Automated backup: error");
|
|
if (ex is not null)
|
|
OnLogWritten("ERROR: " + ex.Message);
|
|
}
|
|
private void LogInfo(string text) => OnLogWritten(text);
|
|
|
|
#endregion
|
|
|
|
protected Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
|
protected void NextProcessable() => _currentProcessable = null;
|
|
private Processable? _currentProcessable;
|
|
|
|
/// <summary> A series of Processable actions to perform on this book </summary>
|
|
protected Queue<Func<Processable>> Processes { get; } = new();
|
|
|
|
public ProcessBookViewModel(LibraryBook libraryBook, Configuration configuration, BadBookSessionContext? badBookSession = null)
|
|
{
|
|
LibraryBook = libraryBook;
|
|
Configuration = configuration;
|
|
_badBookSession = badBookSession;
|
|
|
|
Title = LibraryBook.Book.TitleWithSubtitle;
|
|
Author = LibraryBook.Book.AuthorNames;
|
|
Narrator = LibraryBook.Book.NarratorNames;
|
|
|
|
var pictureId = LibraryBook.Book.PictureId ?? LibraryBook.Book.PictureLarge;
|
|
if (string.IsNullOrEmpty(pictureId))
|
|
return;
|
|
|
|
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(pictureId, PictureSize._80x80));
|
|
|
|
if (isDefault)
|
|
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
|
|
|
Cover = BaseUtil.LoadImage(picture, PictureSize._80x80);
|
|
}
|
|
|
|
private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
|
|
{
|
|
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
|
{
|
|
Cover = BaseUtil.LoadImage(e.Picture, PictureSize._80x80);
|
|
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
|
}
|
|
}
|
|
|
|
public async Task<ProcessBookResult> ProcessOneAsync()
|
|
{
|
|
ProcessBookResult result = ProcessBookResult.None;
|
|
try
|
|
{
|
|
// Run every queued step (decrypt, PDF, MP3, ...) in order so disk-full and other failures
|
|
// from later steps are returned to the queue loop. Do not chain via Processable.Completed.
|
|
while (Processes.Count > 0)
|
|
{
|
|
var processable = CurrentProcessable;
|
|
result = await ExecuteProcessableAsync(processable);
|
|
|
|
if (processable is ILicensedDownload { ObtainedLicense: { } license })
|
|
obtainedLicense = license;
|
|
|
|
UnlinkProcessable(processable);
|
|
NextProcessable();
|
|
|
|
if (result is not ProcessBookResult.Success)
|
|
break;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// DiskFull skips the per-book Abort/Retry/Ignore dialog; the queue shows one disk-full message instead.
|
|
if (result == ProcessBookResult.None)
|
|
result = await GetFailureActionAsync(LibraryBook);
|
|
|
|
var status = result switch
|
|
{
|
|
ProcessBookResult.Success => ProcessBookStatus.Completed,
|
|
ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
|
|
_ => ProcessBookStatus.Failed,
|
|
};
|
|
|
|
Status = status;
|
|
}
|
|
|
|
Result = result;
|
|
return result;
|
|
}
|
|
|
|
private async Task<ProcessBookResult> ExecuteProcessableAsync(Processable processable)
|
|
{
|
|
string procName = processable.Name;
|
|
ProcessBookResult result = ProcessBookResult.None;
|
|
try
|
|
{
|
|
// Optional steps (e.g. PDF when the book has no supplement) use Validate() to mean "does not apply".
|
|
// Skip them instead of treating ProcessSingleAsync's "Validation failed" as a queue failure.
|
|
if (!processable.Validate(LibraryBook))
|
|
return ProcessBookResult.Success;
|
|
|
|
LinkProcessable(processable);
|
|
|
|
var statusHandler = await processable.ProcessSingleAsync(LibraryBook, validate: true);
|
|
|
|
if (statusHandler.IsSuccess)
|
|
result = ProcessBookResult.Success;
|
|
else if (statusHandler.Errors.Contains("Cancelled"))
|
|
{
|
|
LogInfo($"{procName}: Process was cancelled - {LibraryBook.Book}");
|
|
result = ProcessBookResult.Cancelled;
|
|
}
|
|
else if (statusHandler.Errors.Contains("Validation failed"))
|
|
{
|
|
LogInfo($"{procName}: Validation failed - {LibraryBook.Book}");
|
|
result = ProcessBookResult.ValidationFail;
|
|
}
|
|
else
|
|
{
|
|
foreach (var errorMessage in statusHandler.Errors)
|
|
LogError($"{procName}: {errorMessage}");
|
|
|
|
// Prefer disk-full detection over generic retry; avoids treating truncated .aaxc as a normal failure.
|
|
if (statusHandler.Errors.Any(DiskSpaceHelper.ErrorMessageIndicatesDiskFull))
|
|
{
|
|
LogInfo($"{procName}: Disk is full. Free space or change Books / In progress in Settings. - {LibraryBook.Book}");
|
|
result = ProcessBookResult.DiskFull;
|
|
}
|
|
}
|
|
}
|
|
catch (ApiErrorException ex) when (WidevineRecommendation.ShouldRecommendWidevine(ex, Configuration))
|
|
{
|
|
Serilog.Log.Logger.Error(ex, "ADRM license unavailable (Sable acr:null) for {Book}", LibraryBook.LogFriendly());
|
|
LogInfo($"{procName}: {WidevineRecommendationUserMessage.BuildLogSummary(LibraryBook.Book.TitleWithSubtitle)}");
|
|
result = ProcessBookResult.WidevineRecommended;
|
|
}
|
|
catch (ContentLicenseDeniedException ldex)
|
|
{
|
|
Serilog.Log.Logger.Error(ldex, "Content license was denied for {Book}", LibraryBook.LogFriendly());
|
|
if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
|
|
{
|
|
LogInfo($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
|
|
result = ProcessBookResult.LicenseDeniedPossibleOutage;
|
|
}
|
|
else if (LibraryBook.IsAudiblePlus)
|
|
{
|
|
LogInfo($"{procName}: Content license denied for this Audible Plus catalog title. Audible often throttles license requests after heavy Plus use; try again in 1 to 2 days. If you should not have access, check the Audible app. - {LibraryBook.Book}");
|
|
result = ProcessBookResult.LicenseDenied;
|
|
}
|
|
else
|
|
{
|
|
LogInfo($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
|
|
result = ProcessBookResult.LicenseDenied;
|
|
}
|
|
}
|
|
// HRESULT 0x80070070 / known messages from the OS when a volume is full (including many SMB shares).
|
|
catch (Exception ex) when (DiskSpaceHelper.IsDiskFullException(ex))
|
|
{
|
|
Serilog.Log.Logger.Error(ex, "Disk full during {ProcName} for {{@Book}}", procName, LibraryBook.LogFriendly());
|
|
LogInfo($"{procName}: Disk is full. Free space or change Books / In progress in Settings. - {LibraryBook.Book}");
|
|
result = ProcessBookResult.DiskFull;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Serilog.Log.Logger.Error(ex, $"Unhandled exception in {procName} for {{@Book}}", LibraryBook.LogFriendly());
|
|
LogError(procName, ex);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task CancelAsync()
|
|
{
|
|
try
|
|
{
|
|
if (CurrentProcessable is AudioDecodable audioDecodable)
|
|
await audioDecodable.CancelAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogError($"{CurrentProcessable.Name}: Error while cancelling", ex);
|
|
}
|
|
}
|
|
|
|
public ProcessBookViewModel AddDownloadPdf() => AddProcessable<DownloadPdf>();
|
|
public ProcessBookViewModel AddDownloadDecryptBook()
|
|
{
|
|
IncludesBookDownload = true;
|
|
return AddProcessable<DownloadDecryptBook>();
|
|
}
|
|
public ProcessBookViewModel AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
|
public ProcessBookViewModel AddUploadToAudiobookshelf() => AddProcessable<UploadToAudiobookshelf>();
|
|
public ProcessBookViewModel AddSimulateBadBookFailure() => AddProcessable<SimulateBadBookFailure>();
|
|
|
|
private ProcessBookViewModel AddProcessable<T>() where T : Processable, IProcessable<T>
|
|
{
|
|
Processes.Enqueue(() =>
|
|
{
|
|
var processable = T.Create(Configuration);
|
|
|
|
// Each step is created as it is reached, so by now an earlier step has published whatever license
|
|
// it obtained. Handing it on is what keeps a title to one license request per run.
|
|
if (processable is ILicensedDownload licensed)
|
|
licensed.LicenseInfo = obtainedLicense;
|
|
|
|
return processable;
|
|
});
|
|
return this;
|
|
}
|
|
|
|
/// <summary>The license an earlier step for this book obtained, for the steps that follow it to reuse.</summary>
|
|
private DownloadOptions.LicenseInfo? obtainedLicense;
|
|
|
|
public override string ToString() => LibraryBook.ToString();
|
|
|
|
#region Subscribers and Unsubscribers
|
|
|
|
private void LinkProcessable(Processable processable)
|
|
{
|
|
processable.Begin += Processable_Begin;
|
|
processable.Completed += Processable_Completed;
|
|
processable.StatusUpdate += Processable_StatusUpdate;
|
|
processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
|
|
processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
|
|
|
|
if (processable is AudioDecodable audioDecodable)
|
|
{
|
|
audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
|
|
audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
|
|
audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
|
|
audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
|
|
audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
|
|
}
|
|
}
|
|
|
|
private void UnlinkProcessable(Processable processable)
|
|
{
|
|
processable.Begin -= Processable_Begin;
|
|
processable.Completed -= Processable_Completed;
|
|
processable.StatusUpdate -= Processable_StatusUpdate;
|
|
processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
|
|
processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
|
|
|
|
if (processable is AudioDecodable audioDecodable)
|
|
{
|
|
audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
|
|
audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
|
|
audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
|
|
audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
|
|
audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AudioDecodable event handlers
|
|
|
|
private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title;
|
|
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
|
|
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
|
|
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
|
|
=> Cover = BaseUtil.LoadImage(coverArt, PictureSize._80x80);
|
|
|
|
private byte[]? AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
|
{
|
|
var pictureId = Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High
|
|
? LibraryBook.Book.PictureLarge ?? LibraryBook.Book.PictureId
|
|
: LibraryBook.Book.PictureId;
|
|
|
|
if (pictureId is null)
|
|
return null;
|
|
|
|
byte[] coverData = PictureStorage.GetPictureSynchronously(new PictureDefinition(pictureId, PictureSize.Native));
|
|
AudioDecodable_CoverImageDiscovered(this, coverData);
|
|
return coverData;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Streamable event handlers
|
|
|
|
private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
|
|
private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
|
|
{
|
|
if (!downloadProgress.ProgressPercentage.HasValue)
|
|
return;
|
|
|
|
if (downloadProgress.ProgressPercentage == 0)
|
|
TimeRemaining = TimeSpan.Zero;
|
|
else
|
|
Progress = (int)downloadProgress.ProgressPercentage;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Processable event handlers
|
|
|
|
private void Processable_Begin(object? sender, LibraryBook libraryBook)
|
|
{
|
|
Status = ProcessBookStatus.Working;
|
|
|
|
if (sender is Processable processable)
|
|
LogInfo($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
|
|
|
|
Title = libraryBook.Book.TitleWithSubtitle;
|
|
Author = libraryBook.Book.AuthorNames;
|
|
Narrator = libraryBook.Book.NarratorNames;
|
|
}
|
|
|
|
private void Processable_StatusUpdate(object? sender, string statusUpdate)
|
|
=> LogInfo(statusUpdate);
|
|
|
|
private void Processable_Completed(object? sender, LibraryBook libraryBook)
|
|
{
|
|
if (sender is Processable processable)
|
|
LogInfo($"{processable.Name} Step, Completed: {libraryBook.Book}");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Failure Handler
|
|
|
|
protected async Task<ProcessBookResult> GetFailureActionAsync(LibraryBook libraryBook)
|
|
{
|
|
const DialogResult SkipResult = DialogResult.Ignore;
|
|
LogError($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
|
|
|
|
DialogResult dialogResult = Configuration.BadBook switch
|
|
{
|
|
Configuration.BadBookAction.Abort => DialogResult.Abort,
|
|
Configuration.BadBookAction.Retry => DialogResult.Retry,
|
|
Configuration.BadBookAction.Ignore => DialogResult.Ignore,
|
|
Configuration.BadBookAction.Ask or _ => _badBookSession?.Override is Configuration.BadBookAction sessionOverride
|
|
? ToDialogResult(sessionOverride)
|
|
: await ShowRetryDialogAsync(libraryBook)
|
|
};
|
|
|
|
if (dialogResult == SkipResult)
|
|
{
|
|
await libraryBook.UpdateBookStatusAsync(LiberatedStatus.Error);
|
|
LogInfo($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
|
|
}
|
|
|
|
return dialogResult is SkipResult ? ProcessBookResult.FailedSkip
|
|
: dialogResult is DialogResult.Abort ? ProcessBookResult.FailedAbort
|
|
: ProcessBookResult.FailedRetry;
|
|
}
|
|
|
|
protected async Task<DialogResult> ShowRetryDialogAsync(LibraryBook libraryBook)
|
|
{
|
|
string details;
|
|
try
|
|
{
|
|
static string trunc(string str)
|
|
=> string.IsNullOrWhiteSpace(str) ? "[empty]"
|
|
: (str.Length > 50) ? $"{str.Truncate(47)}..."
|
|
: str;
|
|
|
|
details = $"""
|
|
Title: {libraryBook.Book.TitleWithSubtitle}
|
|
ID: {libraryBook.Book.AudibleProductId}
|
|
Author: {trunc(libraryBook.Book.AuthorNames)}
|
|
Narr: {trunc(libraryBook.Book.NarratorNames)}
|
|
""";
|
|
}
|
|
catch
|
|
{
|
|
details = "[Error retrieving details]";
|
|
}
|
|
|
|
var skipDialogText = $"""
|
|
An error occurred while trying to process this book.
|
|
{details}
|
|
|
|
- ABORT: Stop processing books.
|
|
|
|
- RETRY: Skip this book for now, but retry if it is requeued. Continue processing the queued books.
|
|
|
|
- IGNORE: Permanently ignore this book. Continue processing the queued books. (Will not try this book again later.)
|
|
|
|
Check "Apply to all remaining books" to use your choice for the rest of this queue.
|
|
Check "Remember in Settings" to save your choice in Download/Decrypt settings.
|
|
""";
|
|
|
|
try
|
|
{
|
|
var result = await BadBookActionDialogBase.Show(skipDialogText, "Skip this book?");
|
|
|
|
if (result.ApplyToAll)
|
|
_badBookSession?.Override = ToBadBookAction(result.Action);
|
|
|
|
if (result.RememberInSettings)
|
|
Configuration.BadBook = ToBadBookAction(result.Action);
|
|
|
|
return result.Action;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Serilog.Log.Logger.Error(ex, "Error showing retry dialog. Defaulting to 'Retry'; action.");
|
|
return DialogResult.Retry;
|
|
}
|
|
}
|
|
|
|
private static DialogResult ToDialogResult(Configuration.BadBookAction action)
|
|
=> action switch
|
|
{
|
|
Configuration.BadBookAction.Abort => DialogResult.Abort,
|
|
Configuration.BadBookAction.Retry => DialogResult.Retry,
|
|
Configuration.BadBookAction.Ignore => DialogResult.Ignore,
|
|
_ => DialogResult.Retry
|
|
};
|
|
|
|
private static Configuration.BadBookAction ToBadBookAction(DialogResult action)
|
|
=> action switch
|
|
{
|
|
DialogResult.Abort => Configuration.BadBookAction.Abort,
|
|
DialogResult.Retry => Configuration.BadBookAction.Retry,
|
|
DialogResult.Ignore => Configuration.BadBookAction.Ignore,
|
|
_ => Configuration.BadBookAction.Retry
|
|
};
|
|
|
|
#endregion
|
|
} |