diff --git a/Source/AudibleUtilities/AudibleUtilities.csproj b/Source/AudibleUtilities/AudibleUtilities.csproj index 09a933ce..6788ed92 100644 --- a/Source/AudibleUtilities/AudibleUtilities.csproj +++ b/Source/AudibleUtilities/AudibleUtilities.csproj @@ -6,7 +6,7 @@ - + diff --git a/Source/AudibleUtilities/NonJsonResponseExceptionExtensions.cs b/Source/AudibleUtilities/NonJsonResponseExceptionExtensions.cs new file mode 100644 index 00000000..32d7179f --- /dev/null +++ b/Source/AudibleUtilities/NonJsonResponseExceptionExtensions.cs @@ -0,0 +1,69 @@ +using AudibleApi; +using System; +using System.Collections.Generic; + +namespace AudibleUtilities; + +/// +/// Libation-facing helpers when Audible returns HTML instead of JSON (library scan, catalog, etc.). +/// +public static class NonJsonResponseExceptionExtensions +{ + public const string LibraryScanFailedCaption = "Library scan failed"; + + private static readonly string[] ThingsToTryBullets = + [ + "Scan again after a few minutes.", + "Sign in to Audible in a browser on the same network.", + "Disable VPN or proxy and scan again.", + "Remove and re-add the account in Libation.", + "If it still fails, check the log for Full body: near the error and attach it to a bug report.", + ]; + + public static IEnumerable GetExplainerLines(this NonJsonResponseException ex) + { + ArgumentNullException.ThrowIfNull(ex); + yield return getIntro(ex); + yield return string.Empty; + yield return "Things to try:"; + foreach (var bullet in ThingsToTryBullets) + yield return "• " + bullet; + } + + public static string GetExplainerBody(this NonJsonResponseException ex) + => string.Join("\r\n", ex.GetExplainerLines()); + + public static bool TryFindInTree(Exception ex, out NonJsonResponseException? match) + { + ArgumentNullException.ThrowIfNull(ex); + NonJsonResponseException? found = null; + walk(ex); + match = found; + return found is not null; + + void walk(Exception? e) + { + if (e is null || found is not null) + return; + + if (e is NonJsonResponseException nonJson) + found = nonJson; + + if (found is not null) + return; + + if (e is AggregateException agg) + { + foreach (var inner in agg.InnerExceptions) + walk(inner); + } + + walk(e.InnerException); + } + } + + private static string getIntro(NonJsonResponseException ex) + => ex.HtmlTitle is null + ? "Audible returned an HTML page instead of the expected library data." + : $"Audible returned an HTML page ({ex.HtmlTitle}) instead of the expected library data."; +} diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs index 9b68385d..991bfe46 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs @@ -231,6 +231,14 @@ public partial class MainVM WebView2LoginErrorMessage.Caption, webViewEx); } + else if (NonJsonResponseExceptionExtensions.TryFindInTree(ex, out var htmlEx) && htmlEx is not null) + { + await MessageBox.ShowAdminAlert( + MainWindow, + htmlEx.GetExplainerBody(), + NonJsonResponseExceptionExtensions.LibraryScanFailedCaption, + htmlEx); + } else { await MessageBox.ShowAdminAlert( diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs index 3a96a0c5..779543b5 100644 --- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs @@ -443,6 +443,14 @@ public class ProductsDisplayViewModel : ViewModelBase WebView2LoginErrorMessage.Caption, webViewEx); } + else if (NonJsonResponseExceptionExtensions.TryFindInTree(ex, out var htmlEx) && htmlEx is not null) + { + await MessageBox.ShowAdminAlert( + null, + htmlEx.GetExplainerBody(), + NonJsonResponseExceptionExtensions.LibraryScanFailedCaption, + htmlEx); + } else { await MessageBox.ShowAdminAlert( diff --git a/Source/LibationCli/Options/_OptionsBase.cs b/Source/LibationCli/Options/_OptionsBase.cs index 1c8566b7..8610e134 100644 --- a/Source/LibationCli/Options/_OptionsBase.cs +++ b/Source/LibationCli/Options/_OptionsBase.cs @@ -1,6 +1,7 @@ using CommandLine; using Dinah.Core; using FileManager; +using AudibleUtilities; using LibationFileManager; using System; using System.Collections.Generic; @@ -44,6 +45,15 @@ public abstract class OptionsBase catch (Exception ex) { Environment.ExitCode = (int)ExitCode.RunTimeError; + + if (NonJsonResponseExceptionExtensions.TryFindInTree(ex, out var htmlEx) && htmlEx is not null) + { + foreach (var line in htmlEx.GetExplainerLines()) + Console.Error.WriteLine(line); + Serilog.Log.Logger.Error(htmlEx, "Audible returned HTML instead of JSON"); + return; + } + PrintVerbUsage( "ERROR", "=====", diff --git a/Source/LibationFileManager/LibationFileManager.csproj b/Source/LibationFileManager/LibationFileManager.csproj index 86f6c0db..db207737 100644 --- a/Source/LibationFileManager/LibationFileManager.csproj +++ b/Source/LibationFileManager/LibationFileManager.csproj @@ -6,7 +6,7 @@ - + diff --git a/Source/LibationWinForms/Form1.ScanManual.cs b/Source/LibationWinForms/Form1.ScanManual.cs index 44e65a9d..93e1e984 100644 --- a/Source/LibationWinForms/Form1.ScanManual.cs +++ b/Source/LibationWinForms/Form1.ScanManual.cs @@ -96,6 +96,14 @@ public partial class Form1 WebView2LoginErrorMessage.Caption, webViewEx); } + else if (NonJsonResponseExceptionExtensions.TryFindInTree(ex, out var htmlEx) && htmlEx is not null) + { + MessageBoxLib.ShowAdminAlert( + this, + htmlEx.GetExplainerBody(), + NonJsonResponseExceptionExtensions.LibraryScanFailedCaption, + htmlEx); + } else { MessageBoxLib.ShowAdminAlert( diff --git a/docs/advanced/troubleshoot.md b/docs/advanced/troubleshoot.md index 804f6da6..96cd1e79 100644 --- a/docs/advanced/troubleshoot.md +++ b/docs/advanced/troubleshoot.md @@ -14,6 +14,15 @@ There are two possible causes of this error. 1. [Run hangover](#how-to-run-the-hangover-app) and execute the following command in the "Database" tab: `PRAGMA journal_mode=DELETE` 2. run this command in your terminal: `sqlite3 "path/to/libation/files/LibationContext.db" "PRAGMA journal_mode=DELETE;"` +## Library scan fails ("Unexpected character" or "HTML instead of JSON") + +Audible returned an HTML page instead of JSON. Common causes: transient outage, expired login, VPN/proxy, or rate limiting. What to try: + +1. Scan again after a few minutes. +2. Sign in to Audible in a browser on the same network. +3. Disable VPN/proxy and scan again. +4. Remove and re-add the account in Libation. + ## How to run the Hangover App When troubleshooting, you may be asked to run 'Hangover'. Hangover is a debugging app to help diagnose and solve some problems with Libation.