Compare commits

..
12 Commits
Author SHA1 Message Date
Robert McRackan aa829df265 log accounts changes 2026-09-08 00:15:08 -04:00
Robert McRackan e84a0a121a "Import library" 2026-09-06 21:57:46 -04:00
rmcrackan 087c107685 Merge pull request #2038 from rmcrackan/rmcrackan/mbucari-fix
Rmcrackan/mbucari fix
2026-09-06 21:36:09 -04:00
Robert McRackan af7a1e8f69 Increment version to 14.2 2026-09-06 21:27:30 -04:00
Robert McRackan cbbd8b7955 Update docs per @MBurari 's fixes 2026-09-06 21:27:10 -04:00
rmcrackan edc737df4a Merge pull request #2032 from Mbucari/master
Update Deendencies and fix warnings
2026-09-06 09:07:28 -04:00
Mbucari 8050e11a41 Revert SkiaSharp to 3.119.4, matching Avalonia 2026-09-06 01:11:20 -06:00
Mbucari bdf55009b5 Fix tests and warnings from new deps 2026-09-06 00:27:17 -06:00
Mbucari a5a28c9d62 Replace ImageSharp dependency with SkiaSharp 2026-09-05 23:52:14 -06:00
Mbucari 70909a40dc Update Dependencies
Replace obsolete Avalonia.Diagnostics package with AvaloniaUI.DiagnosticsSupport.

Update Avalonia to 12.1, applying necessary fixes.
2026-09-05 23:23:00 -06:00
Mbucari a922cfdf40 Add option to overwrite existing files in DownloadPdf 2026-09-05 16:37:57 -06:00
rmcrackan bc806bada0 Merge pull request #2029 from rmcrackan/rmcrackan/2021-device-registration
Rmcrackan/2021 device registration
2026-09-04 21:05:32 -04:00
48 changed files with 213 additions and 126 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Version>14.1.0</Version>
<Version>14.2.0</Version>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@@ -97,6 +97,7 @@ public class AccountsSettings : IUpdatable
public void Add(Account account)
{
_add(account);
Serilog.Log.Logger.Information("Added Audible account {Account}", account.MaskedLogEntry);
update_no_validate();
}
@@ -165,6 +166,8 @@ public class AccountsSettings : IUpdatable
account.Updated -= update;
var result = _accounts_backing.Remove(account);
if (result)
Serilog.Log.Logger.Information("Removed Audible account {Account}", account.MaskedLogEntry);
update_no_validate();
return result;
}
@@ -7,8 +7,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="14.0.0" />
<PackageReference Include="Google.Protobuf" Version="3.34.1" />
<PackageReference Include="Google.Protobuf" Version="3.36.1">
<PrivateAssets>all</PrivateAssets>
<PublicAssets>runtime</PublicAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
+1 -1
View File
@@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="11.0.0.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="11.0.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.11">
<PrivateAssets>all</PrivateAssets>
+1 -1
View File
@@ -218,7 +218,7 @@ public class DownloadPdf : Processable, IProcessable<DownloadPdf>, ILicensedDown
= Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId))
?? AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook, Configuration);
return AudibleFileStorage.Audio.GetCustomDirFilename(libraryBook, destinationDir, extension);
return AudibleFileStorage.Audio.GetCustomDirFilename(libraryBook, destinationDir, extension, returnFirstExisting: Configuration.OverwriteExisting);
}
private static string? getdownloadUrl(LibraryBook libraryBook)
+1 -1
View File
@@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="11.0.0.1" />
<PackageReference Include="Polly" Version="8.6.6" />
<PackageReference Include="Polly" Version="8.7.0" />
</ItemGroup>
<ItemGroup>
@@ -70,11 +70,10 @@
<TrimmableAssembly Include="Avalonia.Themes.Default" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="12.0.2" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.14" />
<PackageReference Include="ReactiveUI.Avalonia" Version="12.0.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.2" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.2" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageReference Include="ReactiveUI.Avalonia" Version="12.1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
@@ -1,56 +1,54 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Interactivity;
using DataLayer;
namespace LibationAvalonia.Controls;
public class DataGridMyRatingColumn : DataGridBoundColumn
public class DataGridMyRatingColumn : DataGridTemplateColumn
{
[AssignBinding] public BindingBase? BackgroundBinding { get; set; }
[AssignBinding] public BindingBase? OpacityBinding { get; set; }
[AssignBinding] public BindingBase? RatingBinding { get; set; }
private static Rating DefaultRating => new Rating(0, 0, 0);
public DataGridMyRatingColumn()
{
BindingTarget = MyRatingCellEditor.RatingProperty;
this.IsReadOnly = false;
//Must set the CellEditingTemplate to enable cell editing in a DataGridTemplateColumn.
CellEditingTemplate = new FuncDataTemplate(typeof(Rating), (value, _) =>
{
var myRatingElement = CreateControl();
myRatingElement.Name = "CellMyRatingEditor";
myRatingElement.IsEditingMode = true;
return myRatingElement;
});
}
protected override Control GenerateElement(DataGridCell cell, object dataItem)
{
var myRatingElement = new MyRatingCellEditor
{
Name = "CellMyRatingDisplay",
IsEditingMode = false
};
var myRatingElement = CreateControl();
myRatingElement.Name = "CellMyRatingDisplay";
myRatingElement.IsEditingMode = false;
cell.Tag = this;
if (!IsReadOnly)
ToolTip.SetTip(myRatingElement, "Click to change ratings");
if (Binding != null)
myRatingElement.Bind(BindingTarget, Binding);
if (BackgroundBinding != null)
myRatingElement.Bind(MyRatingCellEditor.BackgroundProperty, BackgroundBinding);
if (OpacityBinding != null)
myRatingElement.Bind(MyRatingCellEditor.OpacityProperty, OpacityBinding);
return myRatingElement;
}
protected override Control GenerateEditingElementDirect(DataGridCell cell, object dataItem)
private MyRatingCellEditor CreateControl()
{
var myRatingElement = new MyRatingCellEditor
{
Name = "CellMyRatingEditor",
IsEditingMode = true
};
var myRatingElement = new MyRatingCellEditor();
if (RatingBinding != null)
myRatingElement.Bind(MyRatingCellEditor.RatingProperty, RatingBinding);
if (BackgroundBinding != null)
myRatingElement.Bind(MyRatingCellEditor.BackgroundProperty, BackgroundBinding);
if (OpacityBinding != null)
myRatingElement.Bind(MyRatingCellEditor.OpacityProperty, OpacityBinding);
return myRatingElement;
}
@@ -23,13 +23,19 @@ public partial class MyRatingCellEditor : UserControl
{
InitializeComponent();
var subscriber = this.ObservableForProperty(p => p.Rating).Subscribe(o => DisplayStarRating(o.Value ?? new Rating(0, 0, 0)));
Unloaded += (_, _) => subscriber.Dispose();
if (Design.IsDesignMode)
Rating = new Rating(5, 4, 3);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RatingProperty)
{
DisplayStarRating(change.GetNewValue<Rating>() ?? new Rating(0f, 0f, 0f));
}
}
private void DisplayStarRating(Rating rating)
{
var blankValue = IsEditingMode ? HOLLOW_STAR : string.Empty;
@@ -50,7 +50,7 @@ public partial class ImageDisplayDialog : DialogWindow, INotifyPropertyChanged
try
{
_bitmapHolder.CoverImage?.Save(selectedFile);
_bitmapHolder.CoverImage?.Save(selectedFile, JpegBitmapEncoderOptions.Default);
}
catch (Exception ex)
{
@@ -10,7 +10,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
namespace LibationAvalonia.Dialogs;
@@ -72,13 +72,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="12.0.2" />
<PackageReference Include="Avalonia.Controls.WebView" Version="12.0.0" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.14" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.0.0" />
<PackageReference Include="Avalonia.Desktop" Version="12.0.2" />
<PackageReference Include="ReactiveUI.Avalonia" Version="12.0.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.2" />
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="12.1.2" />
<PackageReference Include="Avalonia.Controls.WebView" Version="12.1.0" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.2" />
<PackageReference Include="ReactiveUI.Avalonia" Version="12.1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.2" />
</ItemGroup>
<ItemGroup>
@@ -3,8 +3,8 @@ using LibationFileManager;
using LibationUiBase.Forms;
using ReactiveUI;
using System;
using System.Reactive;
using System.Threading.Tasks;
using System.Windows.Input;
namespace LibationAvalonia.ViewModels;
@@ -12,7 +12,7 @@ partial class MainVM
{
public string FindBetterQualityBooksTip => Configuration.GetHelpText("FindBetterQualityBooks");
public bool MenuBarVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = !Configuration.IsMacOs;
public ReactiveCommand<Unit, Unit> LaunchHangover { get; private set; } = null!;
public ICommand LaunchHangover { get; private set; } = null!;
private void Configure_Settings()
{
@@ -20,7 +20,7 @@ partial class MainVM
if (App.Current is Avalonia.Application app &&
NativeMenu.GetMenu(app)?.Items[0] is NativeMenuItem aboutMenu)
aboutMenu.Command = ReactiveCommand.Create(ShowAboutAsync);
aboutMenu.Command = ReactiveCommand.CreateFromTask(ShowAboutAsync);
}
public Task ShowAboutAsync() => new LibationAvalonia.Dialogs.AboutDialog().ShowDialog(MainWindow);
@@ -242,7 +242,7 @@
SortMemberPath="ProductRating" CanUserSort="True"
OpacityBinding="{Binding Liberate.Opacity}"
ClipboardContentBinding="{Binding ProductRating}"
Binding="{Binding ProductRating}">
RatingBinding="{Binding ProductRating}">
<controls:DataGridMyRatingColumn.Width>
<Binding x:DataType="vm:ProductsDisplayViewModel" Path="ProductRatingWidth" Mode="TwoWay" />
</controls:DataGridMyRatingColumn.Width>
@@ -269,7 +269,7 @@
SortMemberPath="MyRating" CanUserSort="True"
OpacityBinding="{Binding Liberate.Opacity}"
ClipboardContentBinding="{Binding MyRating}"
Binding="{Binding MyRating, Mode=TwoWay}">
RatingBinding="{Binding MyRating, Mode=TwoWay}">
<controls:DataGridMyRatingColumn.Width>
<Binding x:DataType="vm:ProductsDisplayViewModel" Path="MyRatingWidth" Mode="TwoWay" />
</controls:DataGridMyRatingColumn.Width>
@@ -57,7 +57,7 @@ public partial class Configuration
(KnownDirectories.MyDocs, () => MyDocs),
// this is important to not let very early calls try to accidentally load LibationFiles too early.
// also, keep this at bottom of this list
(KnownDirectories.LibationFiles, () => Instance.LibationFiles.Location)
(KnownDirectories.LibationFiles, () => Instance!.LibationFiles.Location)
};
public static string? GetKnownDirectoryPath(KnownDirectories directory)
{
@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="14.0.0" />
<PackageReference Include="AudibleApi" Version="14.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.11" />
<PackageReference Include="NameParserSharp" Version="1.5.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
@@ -22,6 +22,8 @@ public static class ContentLicenseDeniedUserMessage
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();
@@ -48,6 +50,8 @@ public static class ContentLicenseDeniedUserMessage
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();
/// <summary>
@@ -7,19 +7,22 @@ namespace LibationUiBase;
public static class DeviceRegistrationSettingsUi
{
public static EnumDisplay<DeviceRegistrationKind>[] Options { get; } =
[
new(DeviceRegistrationKind.CurrentAndroid, "Android emulator (default)"),
new(DeviceRegistrationKind.RetailAndroid, "Android Pixel (experimental)"),
new(DeviceRegistrationKind.Mkb79IPhone, "iPhone / audible-cli (experimental; no Widevine)"),
];
DeviceRegistrationProfile.AllProfiles.Select(p => new EnumDisplay<DeviceRegistrationKind>(p.Kind, p.Description)).ToArray();
public static string SettingLabel { get; } = "Device registration (experimental)";
public static string ReLoginNote { get; }
= "Changing this does not convert existing accounts. Remove and re-add the account (or run login-external) to register again.";
= "Changing this does not convert existing accounts. Remove the account, save or close the Accounts dialog, then re-add the account (or run login-external) to register again.";
/// <summary>
/// Steps that actually persist a fresh device registration. Removing alone is not enough if the
/// Accounts dialog is still open with the removal uncommitted.
/// </summary>
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 and re-add the account. You can also import credentials from audible-cli.";
= "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<DeviceRegistrationKind> Display(DeviceRegistrationKind kind)
=> Options.FirstOrDefault(o => o.Value.Equals(kind)) ?? Options[0];
@@ -108,6 +108,7 @@ public static class StatusImageGenerator
var lamp = new SKPath();
lamp.AddRect(SKRect.Create(LiberateIconGeometry.LampLeft, lampTop, LiberateIconGeometry.LampWidth, LiberateIconGeometry.LampHeight));
//Sitting flush with the top edge keeps the badge out of the stoplight's height, so a Plus
//title's stoplight is drawn at exactly the same size as a purchased one's.
var badgeRadius = LiberateIconGeometry.PlusBadgeDiameter / 2;
@@ -42,7 +42,7 @@
<ItemGroup>
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="11.0.0.1" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3912.50" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.4191.47" />
</ItemGroup>
<ItemGroup>
@@ -1,15 +1,15 @@
using SixLabors.ImageSharp;
using System.IO;
using System.IO;
using SkiaSharp;
namespace WindowsConfigApp;
internal static partial class FolderIcon
{
static readonly IcoEncoder IcoEncoder = new();
public static byte[] ToIcon(this Image img)
public static byte[] ToIcon(this SKBitmap img)
{
using var ms = new MemoryStream();
img.Save(ms, IcoEncoder);
IcoEncoder.Encode(img, ms);
return ms.ToArray();
}
+10 -16
View File
@@ -1,18 +1,12 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SkiaSharp;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace WindowsConfigApp;
public class IcoEncoder : IImageEncoder
public class IcoEncoder
{
public bool SkipMetadata { get; init; } = true;
public ReadOnlyCollection<int> ExportSizes { get; }
public IcoEncoder() : this(512, 256, 128, 96, 64, 48, 32, 24) { }
public IcoEncoder(params int[] icoSizes)
@@ -21,20 +15,23 @@ public class IcoEncoder : IImageEncoder
ExportSizes = new(icoSizes);
}
public void Encode<TPixel>(Image<TPixel> image, Stream stream) where TPixel : unmanaged, IPixel<TPixel>
public void Encode(SKBitmap image, Stream stream)
{
// https://stackoverflow.com/a/21389253
//Knowing the image size ahead of time removes the
//requirement of the output stream to support seeking.
byte[][] iconPngs = new byte[ExportSizes.Count][];
var samplingOptions = new SKSamplingOptions(SKCubicResampler.CatmullRom);
for (int i = 0; i < ExportSizes.Count; i++)
{
int size = ExportSizes[i];
using var resized = image.Clone(x => x.Resize(size, size, KnownResamplers.Lanczos2));
using var pngMs = new MemoryStream();
resized.SaveAsPng(pngMs);
iconPngs[i] = pngMs.ToArray();
var imageInfo = new SKImageInfo(size, size);
using var resized = image.Resize(imageInfo, samplingOptions);
using var skImage = SKImage.FromBitmap(resized);
using var data = skImage.Encode(SKEncodedImageFormat.Png, 100);
iconPngs[i] = data.ToArray();
}
//Disposing of the BinaryWriter disposes the soutput stream. Let the caller clean up.
@@ -65,7 +62,4 @@ public class IcoEncoder : IImageEncoder
for (int i = 0; i < ExportSizes.Count; i++)
bw.Write(iconPngs[i]);
}
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel<TPixel>
=> throw new NotImplementedException();
}
@@ -1,6 +1,5 @@
using Dinah.Core;
using LibationFileManager;
using SixLabors.ImageSharp;
using System;
using System.Diagnostics;
using System.IO;
@@ -14,15 +13,15 @@ internal class WinInterop : IInteropFunctions
public WinInterop(params object[] values) { }
public void SetFolderIcon(string image, string directory)
{
using var img = Image.Load(image);
var icon = img.ToIcon();
using var bmp = SkiaSharp.SKBitmap.Decode(image);
var icon = bmp.ToIcon();
new DirectoryInfo(directory)?.SetIcon(icon, "Music");
}
public void SetFolderIcon(byte[] imageJpegBytes, string directory)
{
using var img = Image.Load(new MemoryStream(imageJpegBytes, writable: false));
var icon = img.ToIcon();
using var bmp = SkiaSharp.SKBitmap.Decode(imageJpegBytes);
var icon = bmp.ToIcon();
new DirectoryInfo(directory)?.SetIcon(icon, "Music");
}
@@ -25,10 +25,6 @@
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\LibationUiBase\LibationUiBase.csproj" />
</ItemGroup>
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" Version="4.3.3" />
<PackageReference Include="MSTest" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest.TestFramework" Version="4.3.3" />
<PackageReference Include="MSTest.TestFramework" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
@@ -7,6 +7,9 @@ using AudibleUtilities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.IO;
@@ -1161,5 +1164,64 @@ public class SerializedShape : AccountsTestBase
JObject.Parse(loaded.ToJson())["Accounts"]![0]!["MaskedLogEntry"].Should().BeNull();
}
}
[TestClass]
public class AccountAddRemoveLogging
{
[TestMethod]
public void Add_and_Delete_write_masked_account_to_the_log()
{
var sink = new CollectingSink();
var original = Serilog.Log.Logger;
Serilog.Log.Logger = new LoggerConfiguration().WriteTo.Sink(sink).CreateLogger();
try
{
var settings = new AccountsSettings();
var account = settings.Upsert("user@example.com", "us");
settings.Delete(account).Should().BeTrue();
var messages = sink.Events.Select(e => e.RenderMessage()).ToList();
Assert.AreEqual(1, messages.Count(m => m.Contains("Added Audible account", StringComparison.Ordinal)));
Assert.AreEqual(1, messages.Count(m => m.Contains("Removed Audible account", StringComparison.Ordinal)));
Assert.IsTrue(messages.All(m => m.Contains(account.MaskedLogEntry, StringComparison.Ordinal)));
Assert.IsFalse(messages.Any(m => m.Contains("user@example.com", StringComparison.Ordinal)));
}
finally
{
Serilog.Log.Logger = original;
}
}
[TestMethod]
public void Loading_accounts_from_json_does_not_log_an_add()
{
var sink = new CollectingSink();
var original = Serilog.Log.Logger;
Serilog.Log.Logger = new LoggerConfiguration().WriteTo.Sink(sink).CreateLogger();
try
{
var settings = new AccountsSettings();
settings.Add(new Account("user@example.com") { IdentityTokens = new Identity(Localization.Get("us")) });
var json = settings.ToJson();
sink.Events.Clear();
_ = AccountsSettings.FromJson(json);
Assert.AreEqual(0, sink.Events.Count);
}
finally
{
Serilog.Log.Logger = original;
}
}
private class CollectingSink : ILogEventSink
{
public List<LogEvent> Events { get; } = [];
public void Emit(LogEvent logEvent) => Events.Add(logEvent);
}
}
#pragma warning restore CS8981
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" Version="4.3.3" />
<PackageReference Include="MSTest" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" Version="4.3.3" />
<PackageReference Include="MSTest" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" Version="4.3.3" />
<PackageReference Include="MSTest" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" Version="4.3.3" />
<PackageReference Include="MSTest" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
@@ -22,7 +22,7 @@ public class LoginExternalOptionsTests
Assert.IsTrue(options.TryResolveRegistrationProfile(out var profile, out var error));
Assert.AreEqual("", error);
Assert.AreEqual(DeviceRegistrationKind.RetailAndroid, profile.Kind);
Assert.AreEqual(DeviceRegistrationKind.CurrentAndroid, profile.Kind);
}
[TestMethod]
@@ -5,6 +5,7 @@ using LibationFileManager;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
namespace DeviceRegistrationKindConfigurationTests;
@@ -32,8 +33,8 @@ public class DeviceRegistrationKindConfigurationTests
public void Round_trips_each_kind()
{
var config = Configuration.CreateMockInstance();
foreach (var kind in Enum.GetValues<DeviceRegistrationKind>())
//Exclude RetailAndroid from this test because it is not a valid option for the setting
foreach (var kind in Enum.GetValues<DeviceRegistrationKind>().Where(p => p is not DeviceRegistrationKind.RetailAndroid))
{
config.DeviceRegistrationKind = kind;
Assert.AreEqual(kind, config.DeviceRegistrationKind);
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" Version="4.3.3" />
<PackageReference Include="MSTest" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" Version="4.3.3" />
<PackageReference Include="MSTest" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
@@ -23,6 +23,7 @@ public class ContentLicenseDeniedUserMessageTests
StringAssert.Contains(body, "not a Libation bug");
StringAssert.Contains(body, "experimental device registration");
StringAssert.Contains(body, "audible-cli");
AssertSuggestsRemoveSaveReAdd(body);
}
[TestMethod]
@@ -32,6 +33,7 @@ public class ContentLicenseDeniedUserMessageTests
StringAssert.Contains(body, "temporary interruption of service");
Assert.IsFalse(body.Contains("account is being throttled", StringComparison.Ordinal));
AssertSuggestsRemoveSaveReAdd(body);
}
[TestMethod]
@@ -41,5 +43,14 @@ public class ContentLicenseDeniedUserMessageTests
StringAssert.Contains(body, "Audible Plus catalog");
Assert.IsFalse(body.Contains("account is being throttled", StringComparison.Ordinal));
AssertSuggestsRemoveSaveReAdd(body);
}
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, "re-add the account");
}
}
@@ -9,8 +9,9 @@ public class DeviceRegistrationSettingsUiTests
[TestMethod]
public void Options_cover_every_DeviceRegistrationKind()
{
var kinds = DeviceRegistrationSettingsUi.Options.Select(o => o.Value).ToHashSet();
CollectionAssert.AreEquivalent(Enum.GetValues<DeviceRegistrationKind>(), kinds.ToArray());
var kinds = DeviceRegistrationSettingsUi.Options.Select(o => o.Value).ToArray();
// RetailAndroid is not a valid option for the setting, so it is excluded from the assertion.
CollectionAssert.AreEquivalent(Enum.GetValues<DeviceRegistrationKind>().Where(p => p is not DeviceRegistrationKind.RetailAndroid).ToArray(), kinds);
}
[TestMethod]
@@ -24,6 +25,11 @@ public class DeviceRegistrationSettingsUiTests
{
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");
StringAssert.Contains(DeviceRegistrationSettingsUi.RemoveSaveReAddAccountSteps, "save or close the Accounts dialog");
StringAssert.Contains(DeviceRegistrationSettingsUi.RemoveSaveReAddAccountSteps, "re-add the account");
}
}
@@ -9,7 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" Version="4.3.3" />
<PackageReference Include="MSTest" Version="4.4.0" />
<PackageReference Include="Google.Protobuf" Version="3.36.1" />
<!-- The Liberate icon tests rasterize with SkiaSharp, whose Linux native isn't
brought in by the SkiaSharp package itself the way Windows' and macOS' are. -->
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" Condition="$([MSBuild]::IsOSPlatform('Linux'))" />
+1 -1
View File
@@ -14,7 +14,7 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi
- Check for new Libation versions at startup. Enabled by default: each time Libation starts it asks GitHub whether a newer release exists, and offers it to you if there is one. Turn it off if something else keeps Libation up to date, such as a package manager or an AppImage updater. Turning it off only stops the automatic check - Settings > About still has a "Check for Upgrade" button that works either way.
- Device registration (experimental). Which virtual device Libation registers with Amazon when you sign in. Leave the Android emulator default unless downloads fail for titles that still play in the official Audible app. Changing it does not convert existing accounts; you must remove and re-add the account. See [Device registration](./device-registration.md).
- Device registration (experimental). Which virtual device Libation registers with Amazon when you sign in. The corrected Android registration is the default and supports Widevine; the iPhone/audible-cli alternative does not. Changing it or updating Libation does not convert existing accounts, so affected accounts must be removed and re-added. See [Device registration](./device-registration.md).
- Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
+1 -1
View File
@@ -115,7 +115,7 @@ libationcli login-external -a you@example.com -l us --response-url "https://www.
If the account row already has valid saved tokens, the CLI reports that no browser login is needed and exits without opening the flow.
Optional `--device-registration` picks which virtual device to register as on a **new** sign-in: `CurrentAndroid` (default from Settings), `RetailAndroid`, or `Mkb79IPhone`. It does nothing to an account that is already authenticated; remove the account first. See [Device registration](/docs/advanced/device-registration).
Optional `--device-registration` picks which virtual device to register as on a **new** sign-in: `CurrentAndroid` (the corrected default) or `Mkb79IPhone` (experimental; no Widevine). It does nothing to an account that is already authenticated; remove the account first. See [Device registration](/docs/advanced/device-registration).
```console
libationcli login-external --account you@example.com --locale us --device-registration Mkb79IPhone
+12 -13
View File
@@ -2,48 +2,47 @@
When you sign in, Libation registers a virtual device with Amazon. Audible then ties download licenses to that device. The default is an Android emulator, which is required for [Widevine](/docs/features/audio-file-formats#use-widevine-drm).
Audible has been refusing licenses (`License Denied` / `CustomerThrottled`) for some emulator registrations even when the same title still plays in the official Audible app. If that happens, you can try an experimental registration, or import credentials from [mkb79's audible-cli](https://github.com/mkb79/audible-cli).
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.
Changing this setting does **not** convert accounts you already signed in. Remove and re-add the account (or run `login-external`) after you pick a different profile.
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).
## Where to find it
- **Chardonnay:** Settings -> Important -> **Device registration (experimental)**
- **Chardonnay:** Settings -> Import library -> **Device registration (experimental)**
- **Classic:** Settings -> Import library -> **Device registration (experimental)**
- **CLI / Docker:** `DeviceRegistrationKind` in `Settings.json`, or `--device-registration` on `login-external`. See [Command Line Interface](/docs/advanced/command-line-interface#log-in-with-an-external-browser-login-external).
## The three profiles
## The two profiles
| Setting value | Label in Settings | Widevine | What it registers |
|---------------|-------------------|----------|-------------------|
| `CurrentAndroid` | Android emulator (default) | Yes | The emulator Libation has used for years |
| `RetailAndroid` | Android Pixel (experimental) | Yes | Same Android Audible app as the default, with a retail Pixel fingerprint |
| `CurrentAndroid` | Android emulator (default) | Yes | The corrected Android Audible app registration |
| `Mkb79IPhone` | iPhone / audible-cli (experimental; no Widevine) | No | The virtual iPhone used by audible-cli |
Leave the default unless downloads fail for titles that still work in the official app.
`RetailAndroid` appeared briefly in Libation 14.1 but is no longer a separate option. Existing `RetailAndroid` values are treated as `CurrentAndroid`.
## How to apply a new profile
## How to register an account again
1. Pick the profile in Settings (or set `DeviceRegistrationKind` / `--device-registration`).
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.
Importing an audible-cli JSON file with `import-account` is the other workaround: those credentials already come from audible-cli's iPhone registration, so you do not need to change this setting first.
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.
## Widevine
**Use Widevine DRM** only works when the account was registered as an Android Audible app (`CurrentAndroid` or `RetailAndroid`). The iPhone profile cannot use Widevine. If you need Widevine later, remove the account and sign in again with an Android profile.
**Use Widevine DRM** only works when the account was registered with `CurrentAndroid`. The iPhone profile cannot use Widevine. If you need Widevine later, remove the account and sign in again with the Android profile.
## Settings.json (Docker and CLI)
```json
{
"DeviceRegistrationKind": "RetailAndroid"
"DeviceRegistrationKind": "Mkb79IPhone"
}
```
Accepted values: `CurrentAndroid`, `RetailAndroid`, `Mkb79IPhone`. Then remove the account and sign in again. `login-external --device-registration Mkb79IPhone` overrides Settings for that one sign-in.
Supported choices are `CurrentAndroid` and `Mkb79IPhone`. Then remove the account and sign in again. `login-external --device-registration Mkb79IPhone` overrides Settings for that one sign-in. A legacy `RetailAndroid` value behaves as `CurrentAndroid`.
## If it still fails
+1 -1
View File
@@ -264,7 +264,7 @@ 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. **Virtual-device registration** -- the official Audible app can play the title, but Libation cannot. Try an [experimental device registration](/docs/advanced/device-registration) (then remove and re-add the account), or import credentials from [audible-cli](https://github.com/mkb79/audible-cli).
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).
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.
+1 -1
View File
@@ -74,4 +74,4 @@ A container that liberates on a schedule combines well with a limit: each run do
## When a license is denied anyway
If Audible refuses a license despite the limit, Libation waits before asking about that title again instead of re-requesting it on every run. See [Retrying titles Audible refuses](/docs/features/retrying-refused-downloads). If the official Audible app can still play the title, try an [experimental device registration](/docs/advanced/device-registration) or import credentials from audible-cli.
If Audible refuses a license despite the limit, Libation waits before asking about that title again instead of re-requesting it on every run. See [Retrying titles Audible refuses](/docs/features/retrying-refused-downloads). If the official Audible app can still play the title, remove and re-add the account to get the corrected Android registration. See [Device registration](/docs/advanced/device-registration) for the experimental iPhone alternative and audible-cli import.
+4 -3
View File
@@ -15,9 +15,10 @@ Without this, a title Audible had just refused was requested again on the very n
refused licenses every run, forever: pointless traffic to Audible, which itself risks throttling, and a
console and log full of the same warning for the same titles.
If the official Audible app can play a title that Libation cannot download, that can be the virtual device
Libation registered at sign-in rather than the wait described here. See [Device
registration](/docs/advanced/device-registration).
If the official Audible app can play a title that Libation cannot download, the account may still have the
invalid Android registration created by an older Libation version. Remove and re-add the account to get the
corrected registration. See [Device registration](/docs/advanced/device-registration) for details and the
experimental iPhone alternative.
## How long Libation waits
+3 -1
View File
@@ -71,7 +71,9 @@ Full steps: [Troubleshooting - Failed to decrypt ExistingAccessToken](/docs/adva
That is Audible refusing a download license, not a Libation decrypt bug. Wait 24 to 48 hours if you just downloaded many Plus titles.
If the official app can play the title and waiting does not help, try an [experimental device registration](/docs/advanced/device-registration): pick a profile in Settings, then **remove and re-add the account** (or `login-external`). You can also import credentials from [audible-cli](https://github.com/mkb79/audible-cli). Changing the setting does not convert accounts you already signed in.
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).
See [Troubleshooting](/docs/advanced/troubleshoot#download-fails-with-drm-license-response-not-ok-or-content-license-denied).
+1 -1
View File
@@ -32,7 +32,7 @@ Learn about Libation's powerful features:
- **[Advanced Topics](/docs/advanced/advanced)** - Deep dives and configuration details
- **[Command Line Interface](/docs/advanced/command-line-interface)** - CLI usage and commands
- **[Device registration](/docs/advanced/device-registration)** - Experimental login profiles when Audible denies download licenses
- **[Device registration](/docs/advanced/device-registration)** - Re-register affected accounts and choose the experimental iPhone alternative
- **[Troubleshooting](/docs/advanced/troubleshoot)** - Common errors and solutions
- **[Spatial Audio & DRM](/docs/advanced/spatial-audio)** - Why Dolby Atmos download is not available and what still works
+1 -1
View File
@@ -52,7 +52,7 @@ If you run Libation on a server or in Docker and do not want to copy `AccountsSe
- `login-external` — Browser-based sign-in: the CLI prints an Audible login URL; you open it in a normal browser, sign in, then paste the final URL from the address bar back into the terminal. Example:
`LibationCli login-external --account you@example.com --locale us`
If standard input is not a TTY (for example in some automation), pass the final URL with `--response-url "https://..."` instead of pasting interactively.
Optional `--device-registration CurrentAndroid|RetailAndroid|Mkb79IPhone` applies only to a new sign-in; remove the account first. See [Device registration](/docs/advanced/device-registration).
Optional `--device-registration CurrentAndroid|Mkb79IPhone` applies only to a new sign-in; remove the account first. Android is the corrected default and supports Widevine; iPhone is experimental and does not. See [Device registration](/docs/advanced/device-registration).
- `list-accounts` — List configured accounts and whether each has valid stored credentials (and scan-on/off). Example:
`LibationCli list-accounts` or `LibationCli list-accounts --bare` for tab-separated output.
+1 -1
View File
@@ -82,7 +82,7 @@ Learn about Libation's powerful features:
- **[Advanced Topics](/docs/advanced/advanced)** - Deep dives and configuration details
- **[Command Line Interface](/docs/advanced/command-line-interface)** - CLI usage and commands
- **[Device registration](/docs/advanced/device-registration)** - Experimental login profiles when Audible denies download licenses
- **[Device registration](/docs/advanced/device-registration)** - Re-register affected accounts and choose the experimental iPhone alternative
- **[Troubleshooting](/docs/advanced/troubleshoot)** - Common errors and solutions
- **[Spatial Audio & DRM](/docs/advanced/spatial-audio)** - Why Dolby Atmos download is not available and what still works