From b0bb60b7db51d5f96225df710f6f20c32c56c2cb Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Wed, 16 Sep 2026 16:10:08 -0400 Subject: [PATCH] Improve throttling recovery guidance in apps and docs --- Source/AppScaffolding/LibationScaffolding.cs | 11 +++ .../AppScaffolding/LicenseRecoveryGuidance.cs | 66 +++++++++++++++++ Source/LibationAvalonia/MessageBox.cs | 24 ++++++ .../ContentLicenseDeniedCliSummary.cs | 11 ++- .../Options/_ProcessableOptionsBase.cs | 2 +- .../Configuration.HelpText.cs | 2 +- .../ContentLicenseDeniedUserMessage.cs | 53 +++++--------- .../DeviceRegistrationSettingsUi.cs | 3 - .../ProcessQueue/ProcessBookViewModel.cs | 8 +- .../ProcessQueue/ProcessQueueViewModel.cs | 8 +- Source/LibationWinForms/MessageBoxLib.cs | 31 ++++++++ Source/LibationWinForms/Program.cs | 7 +- .../ContentLicenseDeniedCliSummaryTests.cs | 22 +++++- .../ContentLicenseDeniedUserMessageTests.cs | 12 +-- .../DeviceRegistrationSettingsUiTests.cs | 5 +- .../LicenseRecoveryGuidanceTests.cs | 73 +++++++++++++++++++ docs/advanced/device-registration.md | 27 +++++-- docs/advanced/troubleshoot.md | 4 +- docs/features/daily-download-limit.md | 2 +- docs/frequently-asked-questions.md | 6 +- 20 files changed, 297 insertions(+), 80 deletions(-) create mode 100644 Source/AppScaffolding/LicenseRecoveryGuidance.cs create mode 100644 Source/_Tests/LibationUiBase.Tests/LicenseRecoveryGuidanceTests.cs diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index d08a7935..359c6cd7 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -384,7 +384,18 @@ public static class LibationScaffolding => SearchEngineCommands.OnBookUserDefinedItemCommitted(books); } + public static VersionCheckOutcome LastVersionCheckOutcome => lastVersionCheckOutcome; + + private static volatile VersionCheckOutcome lastVersionCheckOutcome = VersionCheckOutcome.UnableToDetermine; + public static VersionCheckResult GetLatestRelease() + { + var result = CheckLatestRelease(); + lastVersionCheckOutcome = result.Outcome; + return result; + } + + private static VersionCheckResult CheckLatestRelease() { var (version, latest, zip, checkSucceeded, definitive) = getLatestRelease(TimeSpan.FromSeconds(10)); diff --git a/Source/AppScaffolding/LicenseRecoveryGuidance.cs b/Source/AppScaffolding/LicenseRecoveryGuidance.cs new file mode 100644 index 00000000..c4c6a5f8 --- /dev/null +++ b/Source/AppScaffolding/LicenseRecoveryGuidance.cs @@ -0,0 +1,66 @@ +using AudibleApi; +using AudibleUtilities; +using DataLayer; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AppScaffolding; + +/// Shared, offline recovery instructions for Audible license refusals. +public static class LicenseRecoveryGuidance +{ + public const string ReleasesUrl = "https://github.com/rmcrackan/Libation/releases/latest"; + public const string DocumentationUrl = "https://getlibation.com/docs/advanced/device-registration#how-to-register-an-account-again"; + public const string Explanation = "Audible changed something in early September 2026. This is our recommended workaround; we cannot be certain every step is necessary."; + public const string Fallback = "If it still fails, wait 24 to 48 hours (sometimes a few days): Audible also imposes temporary rate limits and has outages. You may still be able to play the title in Audible's app or website. If problems persist after several days, report an issue on Libation's GitHub with logs."; + + public static Locale? GetRegistrationLocale(LibraryBook book) + { + try + { + using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + return GetRegistrationLocale(persister.AccountsSettings, book.Account, book.Book.Locale); + } + catch (Exception ex) + { + Serilog.Log.Logger.Warning(ex, "Unable to determine account region for license recovery guidance"); + return null; + } + } + + public static Locale? GetRegistrationLocale(AccountsSettings accounts, string accountId, string bookLocale) + => accounts.GetAccount(accountId, bookLocale)?.Locale; + + public static string DeviceManagementUrl(Locale? locale) + => $"https://www.amazon.{(string.IsNullOrWhiteSpace(locale?.CountryCode) || string.IsNullOrWhiteSpace(locale.TopDomain) ? "com" : locale.TopDomain)}/hz/mycd/digital-console/devicedetails?deviceFamily=AUDIBLE_APP"; + + public static string BuildSteps(Locale? locale, VersionCheckOutcome updateStatus, bool cli = false) + { + var steps = new List(); + var unknown = string.IsNullOrWhiteSpace(locale?.CountryCode); + var caveat = unknown + ? " (US link; account region is unknown). For other regions, use your regional Amazon site: Manage Your Content and Devices > Devices > Audible." + : !locale!.CountryCode.Equals("us", StringComparison.OrdinalIgnoreCase) + ? " (regional link is unverified). If it does not work, use your regional Amazon site: Manage Your Content and Devices > Devices > Audible." + : "."; + steps.Add($"Open {DeviceManagementUrl(locale)}{caveat} Deregister entries named 'Libation'."); + if (updateStatus != VersionCheckOutcome.UpToDate) + steps.Add((updateStatus == VersionCheckOutcome.UpdateAvailable + ? "Upgrade to the latest Libation: " + : "If you're not on the latest version, upgrade: ") + ReleasesUrl); + if (cli) + { + steps.Add("Stop Libation and any running CLI/Docker jobs. Back up AccountsSettings.json, then remove only the affected account object from its Accounts array and save. This preserves your library and downloaded files."); + steps.Add("Run list-accounts to verify the old account is absent. Run login-external --account --locale to re-add it and sign in."); + steps.Add("Run scan, then retry with liberate ."); + } + else + { + steps.Add("Go to Settings > Accounts and remove the affected account. This preserves your library and downloaded files. Save the removal, then close Libation."); + steps.Add("Reopen Libation. In Settings > Accounts, verify the old account is absent, then re-add the account."); + steps.Add("Scan and sign in, then retry the download (mark it Download Pending if needed)."); + } + return string.Join(Environment.NewLine + Environment.NewLine, steps.Select((step, index) => $"{index + 1}. {step}")); + } +} diff --git a/Source/LibationAvalonia/MessageBox.cs b/Source/LibationAvalonia/MessageBox.cs index 67b361ce..6571531f 100644 --- a/Source/LibationAvalonia/MessageBox.cs +++ b/Source/LibationAvalonia/MessageBox.cs @@ -117,6 +117,30 @@ public class MessageBox private static MessageBoxWindow CreateMessageBox(TopLevel? owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true) { var dialog = new MessageBoxWindow(saveAndRestorePosition); + if (caption == LibationUiBase.ContentLicenseDeniedUserMessage.DialogCaption && buttons == MessageBoxButtons.OK) + { + // Recovery instructions must remain readable and copyable on small displays. + dialog.Title = caption; + dialog.SaveAndRestorePosition = false; + var screen = owner is null ? dialog.Screens?.Primary : owner.Screens?.ScreenFromTopLevel(owner); + dialog.Width = Math.Min(720, screen is null ? 720 : screen.WorkingArea.Width / screen.Scaling * 0.9); + dialog.Height = Math.Min(620, screen is null ? 620 : screen.WorkingArea.Height / screen.Scaling * 0.9); + dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner; + var panel = new DockPanel { Margin = new Thickness(16) }; + var ok = new Button { Content = "OK", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, Margin = new Thickness(0, 12, 0, 0) }; + ok.Click += (_, _) => dialog.Close(DialogResult.OK); + DockPanel.SetDock(ok, Dock.Bottom); + panel.Children.Add(ok); + panel.Children.Add(new TextBox + { + Text = message, IsReadOnly = true, AcceptsReturn = true, + TextWrapping = Avalonia.Media.TextWrapping.Wrap, + [ScrollViewer.VerticalScrollBarVisibilityProperty] = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto + }); + dialog.Content = panel; + dialog.ControlToFocusOnShow = ok; + return dialog; + } var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton); dialog.DataContext = vm; diff --git a/Source/LibationCli/ContentLicenseDeniedCliSummary.cs b/Source/LibationCli/ContentLicenseDeniedCliSummary.cs index b90a830c..aa157994 100644 --- a/Source/LibationCli/ContentLicenseDeniedCliSummary.cs +++ b/Source/LibationCli/ContentLicenseDeniedCliSummary.cs @@ -9,15 +9,18 @@ namespace LibationCli; internal static class ContentLicenseDeniedCliSummary { /// Short lines for stderr when Audible denies a download license; mirrors log detail without dumping the full JSON. - public static IEnumerable Lines(ContentLicenseDeniedException ex) + public static IEnumerable Lines(ContentLicenseDeniedException ex, Locale? locale = null, AppScaffolding.VersionCheckOutcome updateStatus = AppScaffolding.VersionCheckOutcome.UnableToDetermine) { ArgumentNullException.ThrowIfNull(ex); yield return ex.IsCustomerThrottled - ? "Audible denied a content license because this account is being throttled. Wait 24 to 48 hours before trying again. This is not a Libation bug." + ? "Audible says this account is being throttled. An old device registration can also cause this; try registration recovery first." : "Audible denied a content license (download not allowed for this account/title)."; - if (ex.IsCustomerThrottled) - yield return "If the official Audible app can play this title, try an experimental device registration (--device-registration with login-external after removing the account) or import credentials from audible-cli."; + yield return ex.IsCustomerThrottled + ? AppScaffolding.LicenseRecoveryGuidance.Explanation + : "If Audible's app can play this title, try these registration recovery steps. Otherwise, check whether you still have access to the title."; + yield return AppScaffolding.LicenseRecoveryGuidance.BuildSteps(locale, updateStatus, cli: true); + yield return AppScaffolding.LicenseRecoveryGuidance.Fallback; yield return ex.Message; if (ex.Ownership?.Message is { } own && !string.IsNullOrWhiteSpace(own)) diff --git a/Source/LibationCli/Options/_ProcessableOptionsBase.cs b/Source/LibationCli/Options/_ProcessableOptionsBase.cs index 3fef375c..9c376183 100644 --- a/Source/LibationCli/Options/_ProcessableOptionsBase.cs +++ b/Source/LibationCli/Options/_ProcessableOptionsBase.cs @@ -335,7 +335,7 @@ public abstract class ProcessableOptionsBase : OptionsBase } catch (ContentLicenseDeniedException clEx) { - foreach (var line in ContentLicenseDeniedCliSummary.Lines(clEx)) + foreach (var line in ContentLicenseDeniedCliSummary.Lines(clEx, AppScaffolding.LicenseRecoveryGuidance.GetRegistrationLocale(libraryBook), AppScaffolding.LibationScaffolding.LastVersionCheckOutcome)) Console.Error.WriteLine(line); Serilog.Log.Logger.Error(clEx, "Content license denied {@DebugInfo}", new { Book = libraryBook.LogFriendly() }); ReportNextAttempt(libraryBook); diff --git a/Source/LibationFileManager/Configuration.HelpText.cs b/Source/LibationFileManager/Configuration.HelpText.cs index 9409cd41..3f7fa3b4 100644 --- a/Source/LibationFileManager/Configuration.HelpText.cs +++ b/Source/LibationFileManager/Configuration.HelpText.cs @@ -167,7 +167,7 @@ public partial class Configuration {nameof(ImportPlusTitles), """ When enabled, books from the Audible Plus catalog (titles you stream or borrow under your membership, not purchased) are imported into Libation. - Downloading or liberating many Plus titles in a short time can cause Audible to temporarily deny content licenses ("license denied") for a day or two. That limit is enforced by Audible, not Libation — waiting and retrying usually fixes it. If problems persist after several days, report on Libation's GitHub with logs. + If Audible denies a content license but its app can play the title, try deregistering Libation devices and recreating the account: https://getlibation.com/docs/advanced/device-registration#how-to-register-an-account-again . Heavy Plus use can also cause temporary rate limits: wait 24 to 48 hours, sometimes a few days. If problems persist, report on Libation's GitHub with logs. """ }, {nameof(CheckForUpgradesAtStartup), """ When enabled, Libation asks GitHub whether a newer diff --git a/Source/LibationUiBase/ContentLicenseDeniedUserMessage.cs b/Source/LibationUiBase/ContentLicenseDeniedUserMessage.cs index 08bbe9f9..ee77a7de 100644 --- a/Source/LibationUiBase/ContentLicenseDeniedUserMessage.cs +++ b/Source/LibationUiBase/ContentLicenseDeniedUserMessage.cs @@ -14,45 +14,28 @@ public static class ContentLicenseDeniedUserMessage public const string DialogCaption = "Content license denied"; /// Generic outage / GenericError-style denial: not specific to Plus titles. - public static string BuildDialogBodyForPossibleOutage(string bookTitleWithSubtitle) - => $""" - You were denied a content license for {bookTitleWithSubtitle} - - This error often reflects a temporary interruption of service on Audible's side. It usually resolves within about 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app. - - Heavy use of the Audible Plus catalog in a short time can also produce "license denied" responses; community reports often involve on the order of dozens of titles — Audible does not publish a fixed limit. Waiting 24 to 48 hours before trying again is usually enough. - - If the official Audible app can play this title, {DeviceRegistrationSettingsUi.RemoveSaveReAddAccountSteps} - - If the problem continues after several days, open an issue on Libation's GitHub and include your logs. - """ + AppendSuggestion(); + public static string BuildDialogBodyForPossibleOutage(string bookTitleWithSubtitle, AudibleApi.Locale? locale = null, + AppScaffolding.VersionCheckOutcome updateStatus = AppScaffolding.VersionCheckOutcome.UnableToDetermine) + => BuildBody(bookTitleWithSubtitle, + "This may be a temporary interruption of service. If Audible's app can play the title, try these registration recovery steps.", locale, updateStatus); /// Audible named CustomerThrottled. Shown for any title, Plus or owned. - public static string BuildDialogBodyForThrottling(string bookTitleWithSubtitle) - => $""" - You were denied a content license for {bookTitleWithSubtitle} + public static string BuildDialogBodyForThrottling(string bookTitleWithSubtitle, AudibleApi.Locale? locale = null, + AppScaffolding.VersionCheckOutcome updateStatus = AppScaffolding.VersionCheckOutcome.UnableToDetermine) + => BuildBody(bookTitleWithSubtitle, + "Audible says this account is being throttled. This can also happen with an old device registration. Try these recovery steps first.", locale, updateStatus); - Audible refused this download because your account is being throttled. This is a temporary rate limit on Audible's side, not a Libation bug. + /// Plus denials may indicate registration trouble, rate limits, or lost access. + public static string BuildDialogBodyForPlusCatalog(string bookTitleWithSubtitle, AudibleApi.Locale? locale = null, + AppScaffolding.VersionCheckOutcome updateStatus = AppScaffolding.VersionCheckOutcome.UnableToDetermine) + => BuildBody(bookTitleWithSubtitle, + "This title is from the Audible Plus catalog. It may have left Plus, or Audible may be limiting downloads. Check your access in Audible's app; if it plays, try these recovery steps.", locale, updateStatus); - Wait 24 to 48 hours before trying again. In the meantime you should still be able to play this title in the Audible app or website. - - If it still fails after several days, open an issue on Libation's GitHub and include your logs. - - """ + DeviceRegistrationSettingsUi.ThrottlingWorkaround + AppendSuggestion(); - - /// License denied on an Audible Plus title — often rate limiting, not a Libation defect. - public static string BuildDialogBodyForPlusCatalog(string bookTitleWithSubtitle) - => $""" - You were denied a content license for {bookTitleWithSubtitle} - - This title is from the Audible Plus catalog. Audible sometimes temporarily denies content licenses after heavy Plus use in a short period; community reports often mention on the order of dozens of downloads — Audible does not publish a fixed limit. This is usually not a Libation bug. - - Try waiting 24 to 48 hours and liberate again. If it still fails after several days, open an issue on Libation's GitHub with logs. - - If you should not have access to this title (for example it left Plus before you downloaded), confirm in the Audible app or website. - - If the official Audible app can play this title, {DeviceRegistrationSettingsUi.RemoveSaveReAddAccountSteps} - """ + AppendSuggestion(); + private static string BuildBody(string title, string diagnosis, AudibleApi.Locale? locale, AppScaffolding.VersionCheckOutcome updateStatus) + => $"You were denied a content license for {title}\n\n{diagnosis}\n\n" + + AppScaffolding.LicenseRecoveryGuidance.Explanation + "\n\n" + + AppScaffolding.LicenseRecoveryGuidance.BuildSteps(locale, updateStatus) + "\n\n" + + AppScaffolding.LicenseRecoveryGuidance.Fallback + AppendSuggestion(); /// /// When Audible names CustomerThrottled, the throttling dialog already says so. This extra paragraph is diff --git a/Source/LibationUiBase/DeviceRegistrationSettingsUi.cs b/Source/LibationUiBase/DeviceRegistrationSettingsUi.cs index 0d7a5663..f13f8358 100644 --- a/Source/LibationUiBase/DeviceRegistrationSettingsUi.cs +++ b/Source/LibationUiBase/DeviceRegistrationSettingsUi.cs @@ -21,9 +21,6 @@ public static class DeviceRegistrationSettingsUi public static string RemoveSaveReAddAccountSteps { get; } = "Remove the account, save or close the Accounts dialog, then re-add the account."; - public static string ThrottlingWorkaround { get; } - = "If the official Audible app can play this title, try Settings: pick an experimental device registration, then remove the account, save or close the Accounts dialog, and re-add the account. You can also import credentials from audible-cli."; - public static EnumDisplay Display(DeviceRegistrationKind kind) => Options.FirstOrDefault(o => o.Value.Equals(kind)) ?? Options[0]; } diff --git a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs index 774f5371..0fc62427 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs @@ -242,17 +242,17 @@ public class ProcessBookViewModel : ReactiveObject Serilog.Log.Logger.Error(ldex, "Content license was denied for {Book}", LibraryBook.LogFriendly()); if (ldex.IsCustomerThrottled) { - LogInfo($"{procName}: Content license denied because Audible is throttling this account. Wait 24 to 48 hours, then try again. This is not a Libation bug. - {LibraryBook.Book}"); + LogInfo($"{procName}: Audible reports throttling. Try deregistering Libation devices and recreating the account first. Recovery steps: {AppScaffolding.LicenseRecoveryGuidance.DocumentationUrl}. If it persists, wait 24 to 48 hours, sometimes a few days. - {LibraryBook.Book}"); result = ProcessBookResult.LicenseDeniedThrottled; } else 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}"); + LogInfo($"{procName}: Content license denied. A temporary interruption of service is possible. If Audible's app plays the title, try registration recovery: {AppScaffolding.LicenseRecoveryGuidance.DocumentationUrl}. Otherwise, waiting 24 to 48 hours, sometimes a few days, may help. - {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}"); + LogInfo($"{procName}: Content license denied for this Audible Plus catalog title. Check access in the Audible app. If it plays, try registration recovery: {AppScaffolding.LicenseRecoveryGuidance.DocumentationUrl}. Heavy Plus use can also require waiting 24 to 48 hours, sometimes a few days. - {LibraryBook.Book}"); result = ProcessBookResult.LicenseDenied; } else @@ -604,4 +604,4 @@ public class ProcessBookViewModel : ReactiveObject }; #endregion -} \ No newline at end of file +} diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index 2b5983e8..e03b5fe6 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -822,11 +822,13 @@ public class ProcessQueueViewModel : ReactiveObject if (show) { var title = book.LibraryBook.Book.TitleWithSubtitle; + var locale = AppScaffolding.LicenseRecoveryGuidance.GetRegistrationLocale(book.LibraryBook); + var updateStatus = AppScaffolding.LibationScaffolding.LastVersionCheckOutcome; var body = result switch { - ProcessBookResult.LicenseDeniedPossibleOutage => ContentLicenseDeniedUserMessage.BuildDialogBodyForPossibleOutage(title), - ProcessBookResult.LicenseDeniedThrottled => ContentLicenseDeniedUserMessage.BuildDialogBodyForThrottling(title), - _ => ContentLicenseDeniedUserMessage.BuildDialogBodyForPlusCatalog(title) + ProcessBookResult.LicenseDeniedPossibleOutage => ContentLicenseDeniedUserMessage.BuildDialogBodyForPossibleOutage(title, locale, updateStatus), + ProcessBookResult.LicenseDeniedThrottled => ContentLicenseDeniedUserMessage.BuildDialogBodyForThrottling(title, locale, updateStatus), + _ => ContentLicenseDeniedUserMessage.BuildDialogBodyForPlusCatalog(title, locale, updateStatus) }; await MessageBoxBase.Show( body, diff --git a/Source/LibationWinForms/MessageBoxLib.cs b/Source/LibationWinForms/MessageBoxLib.cs index dfe342c4..3fee7c29 100644 --- a/Source/LibationWinForms/MessageBoxLib.cs +++ b/Source/LibationWinForms/MessageBoxLib.cs @@ -1,5 +1,6 @@ using DataLayer; using Dinah.Core.Logging; +using Dinah.Core; using Dinah.Core.Threading; using LibationWinForms.Dialogs; using Serilog; @@ -12,6 +13,36 @@ namespace LibationWinForms; public static class MessageBoxLib { + internal static DialogResult ShowLicenseRecovery(IWin32Window? owner, string message, string caption) + { + using var dialog = new Form + { + Text = caption, StartPosition = FormStartPosition.CenterParent, + Size = new System.Drawing.Size(740, 660), MinimizeBox = false, MaximizeBox = false, + Padding = new Padding(12) + }; + var area = (owner is null ? Screen.PrimaryScreen : Screen.FromHandle(owner.Handle))?.WorkingArea; + if (area is { } bounds) + dialog.Size = new System.Drawing.Size(Math.Min(740, bounds.Width * 9 / 10), Math.Min(660, bounds.Height * 9 / 10)); + var text = new RichTextBox + { + Text = message, ReadOnly = true, DetectUrls = true, Dock = DockStyle.Fill, + Font = System.Drawing.SystemFonts.MessageBoxFont, BackColor = System.Drawing.SystemColors.Window, + ScrollBars = RichTextBoxScrollBars.Vertical + }; + text.LinkClicked += (_, e) => + { + try { Go.To.Url(e.LinkText); } + catch { MessageBox.Show(dialog, "Could not open the link. Copy it into your browser.", caption); } + }; + var ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Dock = DockStyle.Bottom, Height = 36 }; + dialog.Controls.Add(text); + dialog.Controls.Add(ok); + dialog.AcceptButton = ok; + dialog.CancelButton = ok; + return dialog.ShowDialog(owner); + } + /// /// Logs error. Displays a message box dialog with specified text and caption. /// diff --git a/Source/LibationWinForms/Program.cs b/Source/LibationWinForms/Program.cs index ee599f41..9fe05683 100644 --- a/Source/LibationWinForms/Program.cs +++ b/Source/LibationWinForms/Program.cs @@ -186,7 +186,10 @@ static class Program LibationUiBase.Forms.MessageBoxDefaultButton defaultButton, bool _) { - Func showMessageBox = () => MessageBox.Show( + Func showMessageBox = () => caption == LibationUiBase.ContentLicenseDeniedUserMessage.DialogCaption + && buttons == LibationUiBase.Forms.MessageBoxButtons.OK + ? MessageBoxLib.ShowLicenseRecovery(owner as IWin32Window ?? form1, message, caption) + : MessageBox.Show( owner as IWin32Window ?? form1, message, caption, @@ -302,4 +305,4 @@ static class Program //// I never found a case where including made a difference. I think this enum is default and including it will override app user config file //Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); } -} \ No newline at end of file +} diff --git a/Source/_Tests/LibationCli.Tests/ContentLicenseDeniedCliSummaryTests.cs b/Source/_Tests/LibationCli.Tests/ContentLicenseDeniedCliSummaryTests.cs index 0cd61066..377c61b7 100644 --- a/Source/_Tests/LibationCli.Tests/ContentLicenseDeniedCliSummaryTests.cs +++ b/Source/_Tests/LibationCli.Tests/ContentLicenseDeniedCliSummaryTests.cs @@ -41,12 +41,28 @@ public class ContentLicenseDeniedCliSummaryTests var lines = ContentLicenseDeniedCliSummary.Lines(ex).ToList(); StringAssert.Contains(lines[0], "throttled"); - StringAssert.Contains(lines[0], "24 to 48 hours"); - Assert.IsTrue(lines.Any(l => l.Contains("device-registration", StringComparison.Ordinal))); - Assert.IsTrue(lines.Any(l => l.Contains("audible-cli", StringComparison.Ordinal))); + Assert.IsTrue(lines.Any(l => l.Contains("24 to 48 hours"))); + Assert.IsTrue(lines.Any(l => l.Contains("login-external --account", StringComparison.Ordinal))); + Assert.IsTrue(lines.Any(l => l.Contains("AccountsSettings.json", StringComparison.Ordinal))); + Assert.IsFalse(lines.Any(l => l.Contains("Settings > Accounts"))); + Assert.IsFalse(lines.Any(l => l.Contains("not a Libation bug"))); Assert.IsTrue(lines.Any(l => l.StartsWith("Ownership:", StringComparison.Ordinal))); } + [TestMethod] + [DataRow(AppScaffolding.VersionCheckOutcome.UpToDate, false)] + [DataRow(AppScaffolding.VersionCheckOutcome.UpdateAvailable, true)] + [DataRow(AppScaffolding.VersionCheckOutcome.UnableToDetermine, true)] + public void Recovery_uses_passed_region_and_update_status(AppScaffolding.VersionCheckOutcome status, bool upgrade) + { + var ex = Denied(("Ownership", RejectionReason.CustomerThrottled, "throttled")); + var body = string.Join("\n", ContentLicenseDeniedCliSummary.Lines(ex, Localization.Get("uk"), status)); + StringAssert.Contains(body, "https://www.amazon.co.uk/"); + StringAssert.Contains(body, "regional link is unverified"); + Assert.AreEqual(upgrade, body.Contains(AppScaffolding.LicenseRecoveryGuidance.ReleasesUrl)); + Assert.IsTrue(body.IndexOf("Deregister") < body.IndexOf("24 to 48 hours")); + } + [TestMethod] public void An_eligibility_denial_keeps_the_generic_opener() { diff --git a/Source/_Tests/LibationUiBase.Tests/ContentLicenseDeniedUserMessageTests.cs b/Source/_Tests/LibationUiBase.Tests/ContentLicenseDeniedUserMessageTests.cs index d2bbb287..db2f0178 100644 --- a/Source/_Tests/LibationUiBase.Tests/ContentLicenseDeniedUserMessageTests.cs +++ b/Source/_Tests/LibationUiBase.Tests/ContentLicenseDeniedUserMessageTests.cs @@ -20,9 +20,10 @@ public class ContentLicenseDeniedUserMessageTests StringAssert.Contains(body, "Monster Hunter Alpha"); StringAssert.Contains(body, "throttled"); StringAssert.Contains(body, "24 to 48 hours"); - StringAssert.Contains(body, "not a Libation bug"); - StringAssert.Contains(body, "experimental device registration"); - StringAssert.Contains(body, "audible-cli"); + Assert.IsFalse(body.Contains("not a Libation bug")); + Assert.IsFalse(body.Contains("experimental device registration")); + Assert.IsFalse(body.Contains("audible-cli")); + Assert.IsTrue(body.IndexOf("Deregister") < body.IndexOf("24 to 48 hours")); AssertSuggestsRemoveSaveReAdd(body); } @@ -48,9 +49,8 @@ public class ContentLicenseDeniedUserMessageTests private static void AssertSuggestsRemoveSaveReAdd(string body) { - StringAssert.Contains(body, "remove the account", StringComparison.OrdinalIgnoreCase); - StringAssert.Contains(body, "save or close the Accounts dialog"); + StringAssert.Contains(body, "remove the affected account", StringComparison.OrdinalIgnoreCase); + StringAssert.Contains(body, "Save the removal, then close Libation"); StringAssert.Contains(body, "re-add the account"); } } - diff --git a/Source/_Tests/LibationUiBase.Tests/DeviceRegistrationSettingsUiTests.cs b/Source/_Tests/LibationUiBase.Tests/DeviceRegistrationSettingsUiTests.cs index f30bb6a8..574b926c 100644 --- a/Source/_Tests/LibationUiBase.Tests/DeviceRegistrationSettingsUiTests.cs +++ b/Source/_Tests/LibationUiBase.Tests/DeviceRegistrationSettingsUiTests.cs @@ -21,11 +21,8 @@ public class DeviceRegistrationSettingsUiTests } [TestMethod] - public void Throttling_workaround_names_experimental_relogin_and_audible_cli() + public void Registration_setting_explains_how_to_persist_a_new_registration() { - StringAssert.Contains(DeviceRegistrationSettingsUi.ThrottlingWorkaround, "experimental device registration"); - StringAssert.Contains(DeviceRegistrationSettingsUi.ThrottlingWorkaround, "audible-cli"); - StringAssert.Contains(DeviceRegistrationSettingsUi.ThrottlingWorkaround, "save or close the Accounts dialog"); StringAssert.Contains(DeviceRegistrationSettingsUi.ReLoginNote, "does not convert existing accounts"); StringAssert.Contains(DeviceRegistrationSettingsUi.ReLoginNote, "save or close the Accounts dialog"); StringAssert.Contains(DeviceRegistrationSettingsUi.RemoveSaveReAddAccountSteps, "Remove the account"); diff --git a/Source/_Tests/LibationUiBase.Tests/LicenseRecoveryGuidanceTests.cs b/Source/_Tests/LibationUiBase.Tests/LicenseRecoveryGuidanceTests.cs new file mode 100644 index 00000000..8cfc0f95 --- /dev/null +++ b/Source/_Tests/LibationUiBase.Tests/LicenseRecoveryGuidanceTests.cs @@ -0,0 +1,73 @@ +using AppScaffolding; +using AudibleApi; +using AudibleApi.Authorization; +using AudibleUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LibationUiBase.Tests; + +[TestClass] +public class LicenseRecoveryGuidanceTests +{ + [TestMethod] + [DataRow("us", "com", false)] + [DataRow("uk", "co.uk", true)] + [DataRow("germany", "de", true)] + public void Regional_links_use_locale_metadata_with_non_US_caveat(string name, string domain, bool caveat) + { + var locale = Localization.Get(name); + Assert.AreEqual($"https://www.amazon.{domain}/hz/mycd/digital-console/devicedetails?deviceFamily=AUDIBLE_APP", LicenseRecoveryGuidance.DeviceManagementUrl(locale)); + var body = LicenseRecoveryGuidance.BuildSteps(locale, VersionCheckOutcome.UpToDate); + Assert.AreEqual(caveat, body.Contains("regional link is unverified")); + Assert.AreEqual(caveat, body.Contains("Manage Your Content and Devices")); + } + + [TestMethod] + public void Missing_or_empty_locale_uses_labeled_US_link_and_manual_route() + { + foreach (var locale in new Locale?[] { null, Locale.Empty }) + { + var body = LicenseRecoveryGuidance.BuildSteps(locale, VersionCheckOutcome.UpToDate); + StringAssert.Contains(body, "https://www.amazon.com/"); + StringAssert.Contains(body, "US link; account region is unknown"); + StringAssert.Contains(body, "Manage Your Content and Devices"); + } + } + + [TestMethod] + [DataRow(VersionCheckOutcome.UpToDate, false, false)] + [DataRow(VersionCheckOutcome.UpdateAvailable, true, false)] + [DataRow(VersionCheckOutcome.UnableToDetermine, true, true)] + public void Upgrade_step_reflects_known_status_and_keeps_steps_in_order(VersionCheckOutcome status, bool upgrade, bool conditional) + { + var body = LicenseRecoveryGuidance.BuildSteps(Localization.Get("us"), status); + Assert.AreEqual(upgrade, body.Contains(LicenseRecoveryGuidance.ReleasesUrl)); + Assert.AreEqual(conditional, body.Contains("If you're not on the latest version")); + var ordered = new[] { "Deregister", "remove the affected account", "Save the removal", "close Libation", "Reopen Libation", "verify the old account is absent", "re-add the account", "Scan and sign in" }; + var previous = -1; + foreach (var instruction in ordered) + { + var position = body.IndexOf(instruction, StringComparison.Ordinal); + Assert.IsTrue(position > previous, instruction); + previous = position; + } + StringAssert.Contains(body, "preserves your library and downloaded files"); + StringAssert.Contains(body, upgrade ? "5. Scan" : "4. Scan"); + if (upgrade) + Assert.IsTrue(body.IndexOf(LicenseRecoveryGuidance.ReleasesUrl) < body.IndexOf("remove the affected account")); + } + + [TestMethod] + public void Additional_marketplace_uses_owning_accounts_registration_region() + { + var accounts = new AccountsSettings(); + var account = new Account("person@example.com") { IdentityTokens = new Identity(Localization.Get("uk")) }; + account.AddMarketplace("us"); + accounts.Add(account); + accounts.Add(new Account("other@example.com") { IdentityTokens = new Identity(Localization.Get("us")) }); + var locale = LicenseRecoveryGuidance.GetRegistrationLocale(accounts, "PERSON@example.com", "us"); + Assert.AreEqual(account.Locale, locale); + StringAssert.Contains(LicenseRecoveryGuidance.DeviceManagementUrl(locale), "amazon.co.uk/"); + Assert.IsNull(LicenseRecoveryGuidance.GetRegistrationLocale(accounts, "missing", "us")); + } +} diff --git a/docs/advanced/device-registration.md b/docs/advanced/device-registration.md index 7b92be3c..16342ce0 100644 --- a/docs/advanced/device-registration.md +++ b/docs/advanced/device-registration.md @@ -4,7 +4,7 @@ When you sign in, Libation registers a virtual device with Amazon. Audible then Older Libation versions generated an Android device serial that was twice the expected length. Audible began refusing licenses (`License Denied` / `CustomerThrottled`) for some of those registrations even when the same title still played in the official Audible app. Current versions use the corrected Android registration. -Registration data is stored with the account, so updating Libation or changing this setting does **not** repair an account you already signed in. Remove and re-add the affected account (or run `login-external`) to register it again. Try the corrected Android default first. If Audible still refuses licenses, the experimental iPhone/audible-cli profile is available as an alternative; you can also import credentials from [mkb79's audible-cli](https://github.com/mkb79/audible-cli). +Registration data is stored with the account, so updating Libation alone does **not** repair an existing registration. For throttling or license refusals, follow the [full recovery procedure](#how-to-register-an-account-again) first. Keep the corrected Android default unless you specifically need an alternative. ## Where to find it @@ -23,12 +23,25 @@ Registration data is stored with the account, so updating Libation or changing t ## How to register an account again -1. Leave **Android emulator (default)** selected unless you specifically want to try the iPhone alternative. -2. Remove the account from Libation. Existing Amazon device records keep the old registration until you sign in again. -3. Add the account and sign in, or run `login-external`. -4. Scan and try the download again. +Audible changed something in early September 2026. For `CustomerThrottled`, or a license refusal when the official Audible app can still play the title, this is our recommended workaround. We cannot be certain every step is necessary, and it is not a guaranteed fix. -If the corrected Android registration is still denied, repeat those steps with **iPhone / audible-cli**, or import an audible-cli JSON file with `import-account`. Imported audible-cli credentials already use its iPhone registration, so you do not need to change this setting first. +1. Open [Amazon's Audible device-management page (United States)](https://www.amazon.com/hz/mycd/digital-console/devicedetails?deviceFamily=AUDIBLE_APP) and **deregister entries named "Libation"**. For other regions, use your regional Amazon site and go to **Manage Your Content and Devices > Devices > Audible**. The app constructs a regional link, but links outside the US are unverified; use the manual route if yours does not work. +2. Upgrade to [the latest version of Libation](https://github.com/rmcrackan/Libation/releases/latest). +3. Go to **Settings > Accounts** and remove the affected account. **This preserves your library and downloaded files.** Save the removal, then close Libation. +4. Reopen Libation. Go to **Settings > Accounts**, verify the old account is absent, then re-add it using the same account and registration region. +5. Scan and sign in, then retry the download. If the title is deferred, mark it **Download Pending** to try sooner. + +### CLI / Docker + +Follow steps 1 and 2 above, then stop Libation and any running CLI/Docker jobs. Back up `AccountsSettings.json` and remove **only the affected account object** from its `Accounts` array, keeping valid JSON and all other entries. Save it. For Docker, edit the persistent configuration mounted into the container, not a temporary internal copy. + +Run `list-accounts` to verify the old account is absent. Then run `login-external --account --locale ` and complete sign-in, followed by `scan` and `liberate `. Use the account's registration region, which may differ from an additional marketplace it scans. LibationCli has no remove-account command; `login-external` alone skips sign-in when existing credentials are still valid. + +### If registration recovery does not help + +Audible also has temporary outages and rate limits, especially after heavy Plus use. **Wait 24 to 48 hours, sometimes a few days**, before retrying. You may still be able to listen through Audible's app or website. If the title no longer plays there either, check whether you still have access to it. + +As a secondary option, try **iPhone / audible-cli** and repeat the account removal and sign-in steps, or import credentials from [audible-cli](https://github.com/mkb79/audible-cli) with `import-account`. This experimental alternative does not support Widevine. Imported audible-cli credentials already use its iPhone registration. ## Widevine @@ -46,4 +59,4 @@ Supported choices are `CurrentAndroid` and `Mkb79IPhone`. Then remove the accoun ## If it still fails -Wait 24 to 48 hours: Audible also rate-limits heavy Plus use. See [Daily download limit](/docs/features/daily-download-limit) and [Retrying titles Audible refuses](/docs/features/retrying-refused-downloads). If the official app can play the title and a new registration still cannot download it, open a GitHub issue and attach your log. +If the recovery procedure and waiting a few days do not help, open a GitHub issue and attach your log. See also [Daily download limit](/docs/features/daily-download-limit) and [Retrying titles Audible refuses](/docs/features/retrying-refused-downloads). diff --git a/docs/advanced/troubleshoot.md b/docs/advanced/troubleshoot.md index 4ed824bc..da6d0520 100644 --- a/docs/advanced/troubleshoot.md +++ b/docs/advanced/troubleshoot.md @@ -263,8 +263,8 @@ Symptoms include a crash on startup that mentions `LibationContext.db` under a p These errors come from Audible refusing to grant a download license. Common causes: -1. **Temporary Audible outage or Plus throttling** -- wait 24 to 48 hours and try again. See the [FAQ](/docs/frequently-asked-questions). -2. **Old virtual-device registration** -- older Libation versions used an invalid Android device serial length. If the official Audible app can play the title but Libation cannot, remove and re-add the account so it gets the corrected Android registration. If that still fails, try the [experimental iPhone registration](/docs/advanced/device-registration) or import credentials from [audible-cli](https://github.com/mkb79/audible-cli). +1. **Device-registration trouble after Audible's early September 2026 changes** -- for `CustomerThrottled`, or when Audible's app plays the title, try the [full recovery procedure](/docs/advanced/device-registration#how-to-register-an-account-again): deregister Libation devices, upgrade to the [latest release](https://github.com/rmcrackan/Libation/releases/latest), remove/save the account, restart, verify removal, re-add, and scan/sign in. Your library and files are preserved. This is a recommended workaround, not a guaranteed fix. +2. **Temporary Audible outage or Plus throttling** -- if recovery does not help, wait 24 to 48 hours, sometimes a few days. Heavy Plus downloading can still trigger genuine rate limits. 3. **Title requires Widevine** -- some Plus titles no longer download as AAXC; enable **Use Widevine DRM** in Settings and re-add your account if prompted. The iPhone registration cannot use Widevine. See [issue #1580](https://github.com/rmcrackan/Libation/issues/1580) and [Device registration](/docs/advanced/device-registration#widevine). 4. **Spatial / Dolby Atmos requested (older Libation versions)** -- Audible now requires Widevine L1 for many spatial titles. Libation 13.1.3+ no longer offers spatial download. See [Spatial Audio & DRM](/docs/advanced/spatial-audio). 5. **You no longer have rights to the title** -- it was returned, it left the Plus catalog, or the account that owned it is no longer active. Check the title in the Audible app or website. diff --git a/docs/features/daily-download-limit.md b/docs/features/daily-download-limit.md index da28a303..60395285 100644 --- a/docs/features/daily-download-limit.md +++ b/docs/features/daily-download-limit.md @@ -4,7 +4,7 @@ Libation can pace itself, downloading only so much in any 24 hour period. The li Two different reasons to use it: -- **Audible throttling.** Downloading a lot of Audible Plus titles in a short time can lead Audible to deny content licenses for a day or two ("license denied"). That limit is Audible's, not Libation's, and Audible does not publish a number, but community reports put it in the dozens of titles. Capping your Plus downloads avoids provoking it. +- **Audible throttling.** Downloading a lot of Audible Plus titles in a short time can lead Audible to deny content licenses for a day or two ("license denied"). That limit is Audible's, not Libation's, and Audible does not publish a number, but community reports put it in the dozens of titles. Capping your Plus downloads can help reduce that risk. However, `CustomerThrottled` can also reflect device-registration trouble after Audible's early September 2026 changes. Try the [registration recovery procedure](/docs/advanced/device-registration#how-to-register-an-account-again), including upgrading to the [latest release](https://github.com/rmcrackan/Libation/releases/latest). If that does not help, waiting 24 to 48 hours, sometimes a few days, remains valid advice for rate limits or outages. - **Disk space.** A large library can fill a drive. A limit in MB or GB stops a long run before your disk does. ## Turning it on diff --git a/docs/frequently-asked-questions.md b/docs/frequently-asked-questions.md index 78b88e30..6cb7cb9f 100644 --- a/docs/frequently-asked-questions.md +++ b/docs/frequently-asked-questions.md @@ -69,11 +69,9 @@ Full steps: [Troubleshooting - Failed to decrypt ExistingAccessToken](/docs/adva ## Downloads fail with "Content license denied" but the Audible app still plays the title -That is Audible refusing a download license, not a Libation decrypt bug. Wait 24 to 48 hours if you just downloaded many Plus titles. +Audible's early September 2026 changes can cause license refusals, including `CustomerThrottled`, for existing device registrations. Try the [full registration recovery procedure](/docs/advanced/device-registration#how-to-register-an-account-again): deregister Amazon devices named "Libation", upgrade to the [latest Libation](https://github.com/rmcrackan/Libation/releases/latest), remove and save the affected account, close and reopen Libation, verify it is gone, then re-add it and scan/sign in. Removing the account preserves your library and downloaded files. We recommend the full sequence, although we cannot be certain every step is necessary. -If the official app can play the title and waiting does not help, **remove and re-add the account** (or run `login-external`). Older Libation versions registered Android devices with an invalid serial length, and existing accounts keep that old registration after an update. Current versions use the corrected Android registration by default. - -If registering again with Android does not help, try the experimental iPhone/audible-cli option described under [Device registration](/docs/advanced/device-registration), or import credentials from [audible-cli](https://github.com/mkb79/audible-cli). +If that does not help, waiting **24 to 48 hours, sometimes a few days**, can still resolve real rate limits or outages, especially after heavy Plus use. The recovery page also covers non-US links, CLI/Docker instructions, and secondary experimental registration options. If failures persist after several days, report an issue with logs. See [Troubleshooting](/docs/advanced/troubleshoot#download-fails-with-drm-license-response-not-ok-or-content-license-denied).