diff --git a/Source/FileLiberator/WidevineRecommendation.cs b/Source/FileLiberator/WidevineRecommendation.cs
new file mode 100644
index 00000000..fda84346
--- /dev/null
+++ b/Source/FileLiberator/WidevineRecommendation.cs
@@ -0,0 +1,59 @@
+using AudibleApi;
+using LibationFileManager;
+using System;
+
+namespace FileLiberator;
+
+///
+/// Detects a rare Audible API failure mode where ADRM licenserequest returns Sable error 000307
+/// with acr:null. Some titles (e.g. B08XDZCH78 in the US marketplace) only deliver via Widevine.
+/// Keep all detection here so this edge case does not spread through download or UI code.
+///
+public static class WidevineRecommendation
+{
+ ///
+ /// Audible error code seen when Sable cannot resolve asset details for ADRM licenserequest.
+ /// Not diagnostic on its own — also returned for unrelated API issues (e.g. empty response_groups).
+ ///
+ public const string SableAssetErrorCode = "000307";
+
+ ///
+ /// Substring in the Sable error message when no ADRM content reference exists for the title.
+ /// Together with on licenserequest, this is the best signal we have.
+ ///
+ public const string SableAcrNullMarker = "acr:null";
+
+ ///
+ /// True when matches the rare ADRM licenserequest / Sable acr:null pattern.
+ ///
+ public static bool IsAdrmLicenseUnavailableError(ApiErrorException ex)
+ {
+ ArgumentNullException.ThrowIfNull(ex);
+
+ if (ex.RequestUri is not { } requestUri
+ || !requestUri.Contains("/licenserequest", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ if (ex.JsonMessage is not { } json)
+ return false;
+
+ return json.Contains(SableAssetErrorCode, StringComparison.Ordinal)
+ && json.Contains(SableAcrNullMarker, StringComparison.Ordinal);
+ }
+
+ ///
+ /// True when Libation should suggest enabling Widevine for this failure.
+ ///
+ public static bool ShouldRecommendWidevine(ApiErrorException ex, Configuration config)
+ {
+ ArgumentNullException.ThrowIfNull(ex);
+ ArgumentNullException.ThrowIfNull(config);
+
+ return !config.UseWidevine && IsAdrmLicenseUnavailableError(ex);
+ }
+
+ /// One line for logs and CLI when applies.
+ public static string BuildLogSummary(string bookTitleWithSubtitle)
+ => $"{bookTitleWithSubtitle}: Audible returned no ADRM license (Sable acr:null). "
+ + "Some rare titles only download with Widevine — try Settings > Audio File Options > Use Widevine DRM.";
+}
diff --git a/Source/LibationCli/Options/_ProcessableOptionsBase.cs b/Source/LibationCli/Options/_ProcessableOptionsBase.cs
index a55fddf5..f0413033 100644
--- a/Source/LibationCli/Options/_ProcessableOptionsBase.cs
+++ b/Source/LibationCli/Options/_ProcessableOptionsBase.cs
@@ -131,6 +131,11 @@ public abstract class ProcessableOptionsBase : OptionsBase
Serilog.Log.Logger.Error(errorMessage);
}
}
+ catch (ApiErrorException ex) when (WidevineRecommendation.ShouldRecommendWidevine(ex, Configuration.Instance))
+ {
+ Console.Error.WriteLine(WidevineRecommendation.BuildLogSummary(libraryBook.Book.TitleWithSubtitle));
+ Serilog.Log.Logger.Error(ex, "ADRM license unavailable (Sable acr:null) {@DebugInfo}", new { Book = libraryBook.LogFriendly() });
+ }
catch (ContentLicenseDeniedException clEx)
{
foreach (var line in ContentLicenseDeniedCliSummary.Lines(clEx))
diff --git a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs
index 6516a1cc..5fd38edb 100644
--- a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs
+++ b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs
@@ -25,6 +25,8 @@ public enum ProcessBookResult
FailedAbort,
LicenseDenied,
LicenseDeniedPossibleOutage,
+ /// ADRM licenserequest failed with Sable acr:null; Widevine may work (see WidevineRecommendation).
+ WidevineRecommended,
/// Volume full on write; queue should stop (see ProcessQueueViewModel queue loop).
DiskFull
}
@@ -72,6 +74,7 @@ public class ProcessBookViewModel : ReactiveObject
(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(),
};
@@ -209,6 +212,12 @@ public class ProcessBookViewModel : ReactiveObject
}
}
}
+ 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());
diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs
index ac894b8c..4d58ee8c 100644
--- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs
+++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs
@@ -79,6 +79,7 @@ public class ProcessQueueViewModel : ReactiveObject
or ProcessBookResult.FailedSkip
or ProcessBookResult.FailedRetry
or ProcessBookResult.ValidationFail
+ or ProcessBookResult.WidevineRecommended
or ProcessBookResult.DiskFull);
var completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
@@ -353,6 +354,7 @@ public class ProcessQueueViewModel : ReactiveObject
ProgressBarVisible = true;
var startingTime = DateTime.Now;
bool shownLicenseGuidanceMessage = false;
+ bool shownWidevineGuidanceMessage = false;
bool shownDiskFullMessage = false;
using var counterTimer = new System.Threading.Timer(_ => RunningTime = timeToStr(DateTime.Now - startingTime), null, 0, 500);
@@ -406,6 +408,15 @@ public class ProcessQueueViewModel : ReactiveObject
MessageBoxIcon.Asterisk);
shownLicenseGuidanceMessage = true;
}
+ else if (!shownWidevineGuidanceMessage && result == ProcessBookResult.WidevineRecommended)
+ {
+ await MessageBoxBase.Show(
+ WidevineRecommendationUserMessage.BuildDialogBody(nextBook.LibraryBook.Book.TitleWithSubtitle),
+ WidevineRecommendationUserMessage.DialogCaption,
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Asterisk);
+ shownWidevineGuidanceMessage = true;
+ }
ProcessEnd?.Invoke(this, nextBook);
}
Serilog.Log.Logger.Information("Completed processing queue");
diff --git a/Source/LibationUiBase/WidevineRecommendationUserMessage.cs b/Source/LibationUiBase/WidevineRecommendationUserMessage.cs
new file mode 100644
index 00000000..7e71bccb
--- /dev/null
+++ b/Source/LibationUiBase/WidevineRecommendationUserMessage.cs
@@ -0,0 +1,30 @@
+using FileLiberator;
+
+namespace LibationUiBase;
+
+///
+/// User-facing copy when ADRM licenserequest fails with Sable acr:null (rare; Widevine may work).
+/// Shared by WinForms, Avalonia, and the process queue.
+///
+public static class WidevineRecommendationUserMessage
+{
+ public const string DialogCaption = "Download license unavailable";
+
+ public const string QueueStatusText = "License unavailable; try Widevine DRM";
+
+ /// One line for the per-book process log in the queue UI.
+ public static string BuildLogSummary(string bookTitleWithSubtitle)
+ => WidevineRecommendation.BuildLogSummary(bookTitleWithSubtitle);
+
+ /// Shown once per queue run when the pattern is detected and Widevine is off.
+ public static string BuildDialogBody(string bookTitleWithSubtitle)
+ => $"""
+ Libation could not get a download license for {bookTitleWithSubtitle} using the standard (ADRM) format.
+
+ This is uncommon. A small number of titles — including some Audible Plus catalog books — only deliver when Widevine DRM is enabled. Enabling it has fixed this for other users with the same error.
+
+ Try Settings > Audio File Options > Use Widevine DRM, then log into your accounts again when prompted and liberate this title again.
+
+ If it still fails, open an issue on Libation's GitHub and include your logs.
+ """;
+}
diff --git a/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj b/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj
index 20c0dd27..2fe6326d 100644
--- a/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj
+++ b/Source/_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj
@@ -13,6 +13,7 @@
+
diff --git a/Source/_Tests/FileLiberator.Tests/WidevineRecommendationTests.cs b/Source/_Tests/FileLiberator.Tests/WidevineRecommendationTests.cs
new file mode 100644
index 00000000..5a8d9752
--- /dev/null
+++ b/Source/_Tests/FileLiberator.Tests/WidevineRecommendationTests.cs
@@ -0,0 +1,82 @@
+using AudibleApi;
+using LibationFileManager;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json.Linq;
+
+namespace FileLiberator.Tests;
+
+[TestClass]
+public class WidevineRecommendationTests
+{
+ private const string B08XDZCH78_JSON = """
+ {"error_code":"000307","message":"Unable to retrieve asset details from Sable(AssetInfos), for marketplaceId:AF2M0KC94RCEA, asin:B08XDZCH78, acr:null, skuLite:BK_ACX0_239890, version:LATEST, aaaClientId:urn:cdo:AudibleApiExternalRouterService:Prod:Default"}
+ """;
+
+ [TestCleanup]
+ public void RestoreConfiguration() => Configuration.RestoreSingletonInstance();
+
+ [TestMethod]
+ public void IsAdrmLicenseUnavailableError_matches_known_Sable_acr_null_licenserequest()
+ {
+ var ex = new ApiErrorException(
+ "https://api.audible.com/1.0/content/B08XDZCH78/licenserequest",
+ JObject.Parse(B08XDZCH78_JSON),
+ WidevineRecommendation.SableAssetErrorCode);
+
+ Assert.IsTrue(WidevineRecommendation.IsAdrmLicenseUnavailableError(ex));
+ }
+
+ [TestMethod]
+ public void IsAdrmLicenseUnavailableError_rejects_000307_without_acr_null()
+ {
+ var ex = new ApiErrorException(
+ "https://api.audible.com/1.0/content/B00/licenserequest",
+ new JObject
+ {
+ { "error_code", WidevineRecommendation.SableAssetErrorCode },
+ { "message", "No response groups populated." }
+ },
+ WidevineRecommendation.SableAssetErrorCode);
+
+ Assert.IsFalse(WidevineRecommendation.IsAdrmLicenseUnavailableError(ex));
+ }
+
+ [TestMethod]
+ public void IsAdrmLicenseUnavailableError_requires_licenserequest_uri()
+ {
+ var ex = new ApiErrorException(
+ "https://api.audible.com/1.0/content/B08XDZCH78/metadata",
+ JObject.Parse(B08XDZCH78_JSON),
+ WidevineRecommendation.SableAssetErrorCode);
+
+ Assert.IsFalse(WidevineRecommendation.IsAdrmLicenseUnavailableError(ex));
+ }
+
+ [TestMethod]
+ public void ShouldRecommendWidevine_when_pattern_matches_and_widevine_disabled()
+ {
+ var ex = new ApiErrorException(
+ "https://api.audible.com/1.0/content/B08XDZCH78/licenserequest",
+ JObject.Parse(B08XDZCH78_JSON),
+ WidevineRecommendation.SableAssetErrorCode);
+
+ var config = Configuration.CreateMockInstance();
+ config.UseWidevine = false;
+
+ Assert.IsTrue(WidevineRecommendation.ShouldRecommendWidevine(ex, config));
+ }
+
+ [TestMethod]
+ public void ShouldRecommendWidevine_false_when_widevine_already_enabled()
+ {
+ var ex = new ApiErrorException(
+ "https://api.audible.com/1.0/content/B08XDZCH78/licenserequest",
+ JObject.Parse(B08XDZCH78_JSON),
+ WidevineRecommendation.SableAssetErrorCode);
+
+ var config = Configuration.CreateMockInstance();
+ config.UseWidevine = true;
+
+ Assert.IsFalse(WidevineRecommendation.ShouldRecommendWidevine(ex, config));
+ }
+}