mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-06-25 16:02:36 -04:00
Merge pull request #1874 from rmcrackan/rmcrackan/1872-widevine
#1872 : Suggest Widevine when ADRM licenserequest fails with Sable acr:null
This commit is contained in:
59
Source/FileLiberator/WidevineRecommendation.cs
Normal file
59
Source/FileLiberator/WidevineRecommendation.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using AudibleApi;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
|
||||
namespace FileLiberator;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class WidevineRecommendation
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public const string SableAssetErrorCode = "000307";
|
||||
|
||||
/// <summary>
|
||||
/// Substring in the Sable error message when no ADRM content reference exists for the title.
|
||||
/// Together with <see cref="SableAssetErrorCode"/> on licenserequest, this is the best signal we have.
|
||||
/// </summary>
|
||||
public const string SableAcrNullMarker = "acr:null";
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="ex"/> matches the rare ADRM licenserequest / Sable acr:null pattern.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when Libation should suggest enabling Widevine for this failure.
|
||||
/// </summary>
|
||||
public static bool ShouldRecommendWidevine(ApiErrorException ex, Configuration config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ex);
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
|
||||
return !config.UseWidevine && IsAdrmLicenseUnavailableError(ex);
|
||||
}
|
||||
|
||||
/// <summary>One line for logs and CLI when <see cref="ShouldRecommendWidevine"/> applies.</summary>
|
||||
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.";
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -25,6 +25,8 @@ public enum ProcessBookResult
|
||||
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
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
|
||||
30
Source/LibationUiBase/WidevineRecommendationUserMessage.cs
Normal file
30
Source/LibationUiBase/WidevineRecommendationUserMessage.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using FileLiberator;
|
||||
|
||||
namespace LibationUiBase;
|
||||
|
||||
/// <summary>
|
||||
/// User-facing copy when ADRM licenserequest fails with Sable acr:null (rare; Widevine may work).
|
||||
/// Shared by WinForms, Avalonia, and the process queue.
|
||||
/// </summary>
|
||||
public static class WidevineRecommendationUserMessage
|
||||
{
|
||||
public const string DialogCaption = "Download license unavailable";
|
||||
|
||||
public const string QueueStatusText = "License unavailable; try Widevine DRM";
|
||||
|
||||
/// <summary>One line for the per-book process log in the queue UI.</summary>
|
||||
public static string BuildLogSummary(string bookTitleWithSubtitle)
|
||||
=> WidevineRecommendation.BuildLogSummary(bookTitleWithSubtitle);
|
||||
|
||||
/// <summary>Shown once per queue run when the pattern is detected and Widevine is off.</summary>
|
||||
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.
|
||||
""";
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\FileLiberator\FileLiberator.csproj" />
|
||||
<ProjectReference Include="..\..\LibationFileManager\LibationFileManager.csproj" />
|
||||
<ProjectReference Include="..\AssertionHelper\AssertionHelper.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user