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.