Compare commits

...
35 Commits
Author SHA1 Message Date
Robert b2c5ae3c2e incr ver 2026-06-08 10:27:17 -04:00
rmcrackan fb7e0857c6 Merge pull request #1856 from rmcrackan/rmcrackan/releaseindex
update backwards compat releaseindex in prep for future changes
2026-06-08 10:15:26 -04:00
rmcrackan 60965902ca update backwards compat releaseindex in prep for future changes 2026-06-08 10:15:00 -04:00
rmcrackan ef62ee15e2 Merge branch 'master' of https://github.com/rmcrackan/Libation 2026-06-07 12:56:21 -04:00
rmcrackan c285522a0c Add contributor from PR #1854 2026-06-07 12:56:13 -04:00
rmcrackan 0fb3eb152f Merge pull request #1854 from matalvernaz/fix/1729-sqlite-migration-lock-deadlock
Skip EF migration lock when no migrations are pending (fixes SQLite scan-hang #1729)
2026-06-07 12:55:20 -04:00
Matthew Alvernaz 63325b9cad Skip EF migration lock when no migrations are pending (#1729)
DbContexts.GetContext() runs ApplyMigrations on every context creation,
calling Database.Migrate(). Migrate() acquires the EF migration lock
before checking whether anything is pending. For SQLite that lock is a
persisted row in __EFMigrationsLock with no timeout (SQLite has no
connection-scoped lock that frees on disconnect). A process killed after
acquiring the lock but before releasing it - even during an otherwise
no-op Migrate() - orphans the row, and every later Migrate() then spins
forever in SqliteHistoryRepository.AcquireDatabaseLock(). The row lives
in the db file, so restarts never clear it, matching the "scan hangs,
reboot doesn't help" reports in #1729.

Guard Migrate()/MigrateAsync() behind GetPendingMigrations(), which only
reads __EFMigrationsHistory and never acquires the lock. The steady-state
path (schema already current) no longer touches the lock at all; first
run and real migrations are unchanged.
2026-06-06 22:23:12 -07:00
rmcrackan 37a60ca654 Merge pull request #1853 from rmcrackan/rmcrackan-patch-1
increm ver
2026-06-05 10:42:50 -04:00
rmcrackan 6623300883 Update AppScaffolding.csproj 2026-06-05 10:42:21 -04:00
rmcrackan 329df0882a Merge pull request #1852 from rmcrackan/rmcrackan/1851-threading
#1851 - threading bug
2026-06-04 10:23:11 -04:00
rmcrackan da000694d7 upgrade dependency 2026-06-04 10:17:23 -04:00
rmcrackan f1a7a837f3 CI bug. try upgrading 2026-06-04 10:13:22 -04:00
rmcrackan 869917e0ce #1851 - threading bug 2026-06-04 09:59:12 -04:00
rmcrackan d532e40fe9 Merge pull request #1850 from rmcrackan/rmcrackan/doc-cleanup
Clean up docs
2026-06-03 11:05:49 -04:00
rmcrackan 9ef5e95801 Clean up docs 2026-06-03 11:04:59 -04:00
rmcrackan c1c3c33ea8 Merge pull request #1849 from rmcrackan/rmcrackan/docs-better-atmos
better atmos docs
2026-06-02 16:45:36 -04:00
rmcrackan b31e7cb819 better atmos docs 2026-06-02 16:44:54 -04:00
rmcrackan 314a46d59d Merge pull request #1848 from rmcrackan/rmcrackan/docs-spacial-audio
new page for spacial audio info
2026-06-02 16:39:21 -04:00
rmcrackan 721f09a62b new page for spacial audio info 2026-06-02 16:38:12 -04:00
rmcrackan 545b3057d4 Merge pull request #1847 from rmcrackan/rmcrackan/1845-better-non-json
#1845 - improve error message for common VPN blocking errors
2026-06-02 12:58:30 -04:00
rmcrackan 2c2ae2c8b0 #1845 - improve error message for common VPN blocking errors. improve troubleshooting docs 2026-06-02 12:53:20 -04:00
rmcrackan f456c68176 Merge pull request #1846 from rmcrackan/rmcrackan/1844-nre
#1844 - add lock to avoid race/NRE
2026-06-02 10:59:25 -04:00
rmcrackan e6c17053f7 #1844 - add lock to avoid race/NRE 2026-06-02 10:54:14 -04:00
rmcrackan 0c22b821bf incr ver 2026-05-25 11:16:38 -04:00
rmcrackan 8902ddc565 Merge pull request #1838 from rmcrackan/rmcrackan/1836-disk-space
#1836 - Fix misleading "Not enough disk space" warnings
2026-05-25 11:15:08 -04:00
rmcrackan 9ed98cbfff unit test fix: this is always going be a weird test. it's trying to balance machine speed vs long running process -- and test 'unreasonable' response time 2026-05-25 10:58:57 -04:00
rmcrackan 3cf5d0b3fa unit test bug fix: culture info 2026-05-25 10:46:34 -04:00
rmcrackan 6bd1ea7ca4 fix unit tests for windows-only logic 2026-05-25 10:41:20 -04:00
rmcrackan f8c5f0da68 #1836 - Fix misleading "Not enough disk space" warnings when Books and In progress are on different drives 2026-05-25 10:25:46 -04:00
rmcrackan a3f272a48e #1835 - add log file guidance to FAQ 2026-05-22 15:00:34 -04:00
Robert 9ee59738eb incr ver 2026-05-21 10:36:21 -04:00
rmcrackan f57839c104 Merge pull request #1834 from rmcrackan/rmcrackan/1833-pdf-bug-fix
Download works but queue goes red with Download PDF
2026-05-21 10:33:52 -04:00
rmcrackan d63fabd73d Download works but queue goes red with Download PDF 2026-05-21 10:33:13 -04:00
rmcrackan 54a97cc3c4 Merge pull request #1830 from rmcrackan/fix/docs-vite-security-override
Pin vite 6.4.2 for docs site (Dependabot GHSA-4w7w-66w2-5vf9)
2026-05-19 09:44:28 -04:00
rmcrackanandCursor 1e3daa40dc Pin vite 6.4.2 for docs site to resolve Dependabot alert.
VitePress 1.6.4 requires vite ^5.4.14, which has no backport for
GHSA-4w7w-66w2-5vf9. An npm override keeps VitePress 1.x while
using a patched vite. Deploy workflow now watches package files.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 09:40:44 -04:00
40 changed files with 909 additions and 283 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+2
View File
@@ -10,6 +10,8 @@ on:
- docs/**
- .vitepress/**
- index.md
- package.json
- package-lock.json
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
+1 -1
View File
@@ -1,5 +1,5 @@
{
"WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-classic-x64\\.zip",
"WindowsClassic": "(?:Classic-Libation|Libation-Classic)\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-classic-x64\\.zip",
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-x64\\.zip",
"WindowsAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-arm64\\.zip",
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.deb",
+2 -1
View File
@@ -84,7 +84,8 @@ export default defineConfig({
items: [
{ text: "Advanced Topics", link: "/docs/advanced/advanced" },
{ text: "Command Line Interface", link: "/docs/advanced/command-line-interface" },
{ text: "Troubleshooting", link: "/docs/advanced/troubleshoot" }
{ text: "Troubleshooting", link: "/docs/advanced/troubleshoot" },
{ text: "Spatial Audio & DRM", link: "/docs/advanced/spatial-audio" },
],
},
{
+1
View File
@@ -32,6 +32,7 @@ Comprehensive documentation is available in the `docs` directory and on our [Doc
- [Getting Started](https://getlibation.com/docs/getting-started)
- [FAQ](https://getlibation.com/docs/frequently-asked-questions)
- [Spatial Audio & DRM](https://getlibation.com/docs/advanced/spatial-audio)
## Development & Contributing
+1 -1
View File
@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Version>13.4.2.1</Version>
<Version>13.4.6.1</Version>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="10.1.5.1" />
<PackageReference Include="AudibleApi" Version="10.2.0.1" />
<PackageReference Include="Google.Protobuf" Version="3.34.1" />
</ItemGroup>
@@ -0,0 +1,69 @@
using AudibleApi;
using System;
using System.Collections.Generic;
namespace AudibleUtilities;
/// <summary>
/// Libation-facing helpers when Audible returns HTML instead of JSON (library scan, catalog, etc.).
/// </summary>
public static class NonJsonResponseExceptionExtensions
{
public const string LibraryScanFailedCaption = "Library scan failed";
private static readonly string[] ThingsToTryBullets =
[
"Scan again after a few minutes.",
"Sign in to Audible in a browser on the same network.",
"Disable VPN or proxy and scan again.",
"Remove and re-add the account in Libation.",
"If it still fails, check the log for Full body: near the error and attach it to a bug report.",
];
public static IEnumerable<string> GetExplainerLines(this NonJsonResponseException ex)
{
ArgumentNullException.ThrowIfNull(ex);
yield return getIntro(ex);
yield return string.Empty;
yield return "Things to try:";
foreach (var bullet in ThingsToTryBullets)
yield return "• " + bullet;
}
public static string GetExplainerBody(this NonJsonResponseException ex)
=> string.Join("\r\n", ex.GetExplainerLines());
public static bool TryFindInTree(Exception ex, out NonJsonResponseException? match)
{
ArgumentNullException.ThrowIfNull(ex);
NonJsonResponseException? found = null;
walk(ex);
match = found;
return found is not null;
void walk(Exception? e)
{
if (e is null || found is not null)
return;
if (e is NonJsonResponseException nonJson)
found = nonJson;
if (found is not null)
return;
if (e is AggregateException agg)
{
foreach (var inner in agg.InnerExceptions)
walk(inner);
}
walk(e.InnerException);
}
}
private static string getIntro(NonJsonResponseException ex)
=> ex.HtmlTitle is null
? "Audible returned an HTML page instead of the expected library data."
: $"Audible returned an HTML page ({ex.HtmlTitle}) instead of the expected library data.";
}
+13 -2
View File
@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -49,7 +50,15 @@ public class LibationContextFactory
{
try
{
context.Database.Migrate();
// Migrate() acquires the EF migration lock before checking whether anything is pending.
// For SQLite that lock is a row in __EFMigrationsLock with no timeout (SQLite has no
// connection-scoped lock that frees on disconnect). A process killed after acquiring the
// lock but before releasing it - even during an otherwise no-op Migrate() - orphans the row,
// and every later Migrate() then spins forever in SqliteHistoryRepository.AcquireDatabaseLock().
// The row is persisted in the db file, so restarting does not clear it. Skipping Migrate()
// when nothing is pending keeps the steady-state path off the lock entirely. See #1729.
if (context.Database.GetPendingMigrations().Any())
context.Database.Migrate();
}
// SQLITE_READONLY == 8 (https://www.sqlite.org/rescode.html)
catch (SqliteException ex) when (ex.SqliteErrorCode == 8 && sqliteDatabaseFilePath is not null)
@@ -63,7 +72,9 @@ public class LibationContextFactory
{
try
{
await context.Database.MigrateAsync(cancellationToken);
// See ApplyMigrations for why pending migrations are checked before acquiring the lock (#1729).
if ((await context.Database.GetPendingMigrationsAsync(cancellationToken)).Any())
await context.Database.MigrateAsync(cancellationToken);
}
// SQLITE_READONLY == 8 (https://www.sqlite.org/rescode.html)
catch (SqliteException ex) when (ex.SqliteErrorCode == 8 && sqliteDatabaseFilePath is not null)
@@ -73,7 +73,7 @@
<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.1" />
<PackageReference Include="ReactiveUI.Avalonia" Version="12.0.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.2" />
</ItemGroup>
<ItemGroup>
@@ -77,7 +77,7 @@
<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.1" />
<PackageReference Include="ReactiveUI.Avalonia" Version="12.0.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.2" />
</ItemGroup>
@@ -1,4 +1,5 @@
using ApplicationServices;
using Avalonia.Threading;
using DataLayer;
using LibationFileManager;
using ReactiveUI;
@@ -77,6 +78,9 @@ partial class MainVM
if (Configuration.Instance.AutoDownloadEpisodes
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
await BackupAllBooksAsync(stats.LibraryBooks);
{
// RunWorkerCompleted has no SynchronizationContext; queue items require the UI thread.
await Dispatcher.UIThread.InvokeAsync(async () => await BackupAllBooksAsync(stats.LibraryBooks));
}
}
}
@@ -231,6 +231,14 @@ public partial class MainVM
WebView2LoginErrorMessage.Caption,
webViewEx);
}
else if (NonJsonResponseExceptionExtensions.TryFindInTree(ex, out var htmlEx) && htmlEx is not null)
{
await MessageBox.ShowAdminAlert(
MainWindow,
htmlEx.GetExplainerBody(),
NonJsonResponseExceptionExtensions.LibraryScanFailedCaption,
htmlEx);
}
else
{
await MessageBox.ShowAdminAlert(
@@ -443,6 +443,14 @@ public class ProductsDisplayViewModel : ViewModelBase
WebView2LoginErrorMessage.Caption,
webViewEx);
}
else if (NonJsonResponseExceptionExtensions.TryFindInTree(ex, out var htmlEx) && htmlEx is not null)
{
await MessageBox.ShowAdminAlert(
null,
htmlEx.GetExplainerBody(),
NonJsonResponseExceptionExtensions.LibraryScanFailedCaption,
htmlEx);
}
else
{
await MessageBox.ShowAdminAlert(
@@ -1,6 +1,7 @@
using CommandLine;
using Dinah.Core;
using FileManager;
using AudibleUtilities;
using LibationFileManager;
using System;
using System.Collections.Generic;
@@ -44,6 +45,15 @@ public abstract class OptionsBase
catch (Exception ex)
{
Environment.ExitCode = (int)ExitCode.RunTimeError;
if (NonJsonResponseExceptionExtensions.TryFindInTree(ex, out var htmlEx) && htmlEx is not null)
{
foreach (var line in htmlEx.GetExplainerLines())
Console.Error.WriteLine(line);
Serilog.Log.Logger.Error(htmlEx, "Audible returned HTML instead of JSON");
return;
}
PrintVerbUsage(
"ERROR",
"=====",
@@ -166,7 +166,8 @@ internal class AaxcFileStorage : AudibleFileStorage
//Cache the results for a short time to avoid excessive file system hits.
private DateTime lastDlInProgressEnumeration;
private static readonly TimeSpan dlInProgressCacheTime = TimeSpan.FromSeconds(10);
private IEnumerable<LongPath>? dlInProgressFilesCache;
private LongPath[]? dlInProgressFilesCache;
private Lock dlInProgressLocker { get; } = new();
protected override List<LongPath> GetFilePathsCustom(string productId)
{
@@ -175,22 +176,26 @@ internal class AaxcFileStorage : AudibleFileStorage
var regex = GetBookSearchRegex(productId);
if (DateTime.UtcNow - lastDlInProgressEnumeration > dlInProgressCacheTime)
LongPath[] files;
lock (dlInProgressLocker)
{
dlInProgressFilesCache = null;
if (DateTime.UtcNow - lastDlInProgressEnumeration > dlInProgressCacheTime)
dlInProgressFilesCache = null;
if (dlInProgressFilesCache is null)
{
dlInProgressFilesCache
= FileUtility
.SaferEnumerateFiles(dlFolder, "*.*", SearchOption.AllDirectories)
.ToArray();
lastDlInProgressEnumeration = DateTime.UtcNow;
}
files = dlInProgressFilesCache;
}
if (dlInProgressFilesCache is null)
{
dlInProgressFilesCache
= FileUtility
.SaferEnumerateFiles(dlFolder, "*.*", SearchOption.AllDirectories)
.ToArray();
lastDlInProgressEnumeration = DateTime.UtcNow;
}
return dlInProgressFilesCache.Where(s => regex.IsMatch(s)).ToList();
return files.Where(s => regex.IsMatch(s)).ToList();
}
public bool Exists(string productId) => GetFilePath(productId) is not null;
+108 -36
View File
@@ -15,10 +15,21 @@ public static class DiskSpaceHelper
/// <summary>Conservative per-title estimate (download + decrypt temp + final file) for bulk preflight.</summary>
public const long EstimatedBytesPerAudiobookBackup = 400_000_000L;
/// <summary>Below this free space on a relevant drive, bulk backup is blocked (no Continue).</summary>
/// <summary>Below this free space on a Books drive, bulk backup is blocked (no Continue).</summary>
public const long CriticalFreeBytes = 100_000_000L;
/// <summary>Extra headroom required on the In progress drive beyond one active title.</summary>
public const long InProgressPreflightMarginBytes = 50_000_000L;
private const int HResultDiskFull = unchecked((int)0x80070070);
private const string WinLongPathPrefix = @"\\?\";
[Flags]
public enum BackupDriveUsage
{
InProgress = 1,
Books = 2,
}
public static bool IsDiskFullException(Exception? ex)
{
@@ -52,39 +63,93 @@ public static class DiskSpaceHelper
}
/// <summary>
/// Returns free bytes for the volume containing <paramref name="path"/>, or null if unknown.
/// Null means preflight cannot warn/block on that root (writable shares with no capacity API, offline drive, bad path).
/// On Windows, <see cref="Path.GetPathRoot"/> yields drive letters (C:\) or UNC roots (\\server\share\).
/// Strips the Win32 extended-length prefix so <see cref="DriveInfo"/> and path APIs see a normal root.
/// No-op on non-Windows platforms (Libation only uses the prefix there).
/// </summary>
public static long? TryGetAvailableFreeBytes(string? path)
public static string NormalizePathForDriveQuery(string path)
{
if (!OperatingSystem.IsWindows() || !path.StartsWith(WinLongPathPrefix, StringComparison.Ordinal))
return path;
var stripped = path[WinLongPathPrefix.Length..];
if (stripped.StartsWith(@"UNC\", StringComparison.OrdinalIgnoreCase))
return @"\\" + stripped[4..];
return stripped;
}
/// <summary>
/// Returns the volume root used for free-space queries (e.g. C:\ or \\server\share\), or null if unknown.
/// </summary>
public static string? GetPathRootForDiskSpaceCheck(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return null;
try
{
var fullPath = Path.GetFullPath(path);
var normalized = NormalizePathForDriveQuery(path);
var fullPath = Path.GetFullPath(normalized);
var root = Path.GetPathRoot(fullPath);
if (string.IsNullOrWhiteSpace(root))
return null;
var drive = new DriveInfo(root);
// IsReady is false for disconnected network drives; AvailableFreeSpace may be wrong on some NAS reporting.
return drive.IsReady ? drive.AvailableFreeSpace : null;
return string.IsNullOrWhiteSpace(root) ? null : root;
}
catch
{
// DriveInfo can throw for invalid roots; treat as unknown rather than failing backup setup.
return null;
}
}
/// <summary>
/// Returns free bytes for the volume containing <paramref name="path"/>, or null if unknown.
/// Null means preflight cannot warn/block on that root (writable shares with no capacity API, offline drive, bad path).
/// </summary>
public static long? TryGetAvailableFreeBytes(string? path)
{
var root = GetPathRootForDiskSpaceCheck(path);
if (root is null)
return null;
try
{
var drive = new DriveInfo(root);
return drive.IsReady ? drive.AvailableFreeSpace : null;
}
catch
{
return null;
}
}
public static long GetRequiredBytesForDriveUsage(BackupDriveUsage usage, int bookCount)
{
var hasBooks = usage.HasFlag(BackupDriveUsage.Books);
var hasInProgress = usage.HasFlag(BackupDriveUsage.InProgress);
if (hasBooks)
return Math.Max(0, bookCount) * EstimatedBytesPerAudiobookBackup;
if (hasInProgress)
return EstimatedBytesPerAudiobookBackup;
return 0;
}
public static long GetCriticalFreeBytesForDriveUsage(BackupDriveUsage usage)
{
if (usage.HasFlag(BackupDriveUsage.Books))
return CriticalFreeBytes;
if (usage.HasFlag(BackupDriveUsage.InProgress))
return EstimatedBytesPerAudiobookBackup + InProgressPreflightMarginBytes;
return CriticalFreeBytes;
}
public static IReadOnlyList<BackupDriveSpace> GetBackupDriveSpaces(Configuration config, int bookCount)
{
var requiredBytes = Math.Max(0, bookCount) * EstimatedBytesPerAudiobookBackup;
var pathsByRoot = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
var pathsByRoot = new Dictionary<string, (List<string> paths, BackupDriveUsage usage)>(StringComparer.OrdinalIgnoreCase);
void addPath(string? path)
void addPath(string? path, BackupDriveUsage usageFlag)
{
if (string.IsNullOrWhiteSpace(path))
return;
@@ -92,7 +157,7 @@ public static class DiskSpaceHelper
string fullPath;
try
{
fullPath = Path.GetFullPath(path);
fullPath = Path.GetFullPath(NormalizePathForDriveQuery(path));
}
catch
{
@@ -103,31 +168,37 @@ public static class DiskSpaceHelper
if (string.IsNullOrWhiteSpace(root))
return;
// Same physical share via Z: vs \\server\share appears as two roots; each gets the full estimate (conservative).
if (!pathsByRoot.TryGetValue(root, out var list))
{
list = [];
pathsByRoot[root] = list;
}
if (!pathsByRoot.TryGetValue(root, out var entry))
entry = ([], usageFlag);
else
entry.usage |= usageFlag;
if (!list.Contains(fullPath, StringComparer.OrdinalIgnoreCase))
list.Add(fullPath);
if (!entry.paths.Contains(fullPath, StringComparer.OrdinalIgnoreCase))
entry.paths.Add(fullPath);
pathsByRoot[root] = entry;
}
addPath(config.Books?.Path);
addPath(config.InProgress);
addPath(config.Books?.Path, BackupDriveUsage.Books);
addPath(config.InProgress, BackupDriveUsage.InProgress);
return pathsByRoot
.Select(kvp => new BackupDriveSpace(
kvp.Key,
kvp.Value,
TryGetAvailableFreeBytes(kvp.Key),
requiredBytes))
.Select(kvp =>
{
var usage = kvp.Value.usage;
var required = GetRequiredBytesForDriveUsage(usage, bookCount);
return new BackupDriveSpace(
kvp.Key,
kvp.Value.paths,
TryGetAvailableFreeBytes(kvp.Key),
required,
usage);
})
.ToList();
}
/// <summary>
/// True when every root is unknown or has enough reported space. All-unknown => no preflight dialog.
/// True when every root is unknown or has enough reported space for its role. All-unknown => no preflight dialog.
/// </summary>
public static bool HasSufficientSpaceForBulkBackup(IReadOnlyList<BackupDriveSpace> drives)
=> drives.All(d => d.AvailableBytes is null || d.AvailableBytes >= d.RequiredBytes);
@@ -136,13 +207,14 @@ public static class DiskSpaceHelper
/// Only applies when free space was read successfully; unknown (null) never hard-blocks.
/// </summary>
public static bool AnyDriveCriticallyLow(IReadOnlyList<BackupDriveSpace> drives)
=> drives.Any(d => d.AvailableBytes is not null && d.AvailableBytes < CriticalFreeBytes);
=> drives.Any(d => d.AvailableBytes is not null && d.AvailableBytes < GetCriticalFreeBytesForDriveUsage(d.Usage));
public readonly record struct BackupDriveSpace(
/// <summary>Path root from <see cref="Path.GetPathRoot"/> (e.g. C:\ or \\nas\library\).</summary>
/// <summary>Volume root used for free-space display (e.g. C:\ or \\nas\library\).</summary>
string DriveRoot,
IReadOnlyList<string> Paths,
/// <summary>Null when <see cref="TryGetAvailableFreeBytes"/> could not query this root.</summary>
long? AvailableBytes,
long RequiredBytes);
long RequiredBytes,
BackupDriveUsage Usage);
}
@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="10.1.5.1" />
<PackageReference Include="AudibleApi" Version="10.2.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.7" />
<PackageReference Include="NameParserSharp" Version="1.5.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
@@ -14,12 +14,14 @@ public record CultureInfoDto : IFormattable
public static CultureInfoDto OfCurrentUi()
{
return new CultureInfoDto(CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentUICulture, CultureInfo.CurrentUICulture.Name, "{N}");
var culture = CultureInfo.CurrentUICulture;
return new CultureInfoDto(culture, culture.Name, "{N}");
}
public static CultureInfoDto OfCurrentOs()
{
return new CultureInfoDto(CultureInfo.DefaultThreadCurrentCulture ?? CultureInfo.CurrentCulture, CultureInfo.CurrentCulture.Name, "{N}");
var culture = CultureInfo.CurrentCulture;
return new CultureInfoDto(culture, culture.Name, "{N}");
}
public CultureInfoDto(string hint) : this(hint, "{O}")
+25 -5
View File
@@ -1,6 +1,5 @@
using LibationFileManager;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LibationUiBase;
@@ -16,7 +15,7 @@ public static class DiskFullUserMessage
=> """
Libation stopped the backup queue because the disk ran out of free space.
Download and decrypt use temporary files under your "In progress" folder, then write finished audiobooks to your Books location. Both need enough free space on their drives.
Download and decrypt use temporary files under your "In progress" folder, then write finished audiobooks to your Books location. Those folders can be on different drives, and each needs enough free space for its role.
Free disk space (or move Books / In progress to a larger drive in Settings), delete partial files under your In progress folder if needed, then retry your backups in smaller batches.
""";
@@ -26,6 +25,8 @@ public static class DiskFullUserMessage
var sb = new StringBuilder();
sb.AppendLine($"You are about to back up {bookCount} books, but a drive Libation uses does not have enough free space to continue safely.");
sb.AppendLine();
sb.AppendLine("Books is where finished audiobooks are stored. In progress holds temporary files while one title downloads and decrypts (about one book at a time, often on a different drive than Books).");
sb.AppendLine();
AppendDriveLines(sb, drives);
sb.AppendLine();
sb.Append("Free space or change the Books and In progress locations in Settings, then try again.");
@@ -35,7 +36,9 @@ public static class DiskFullUserMessage
public static string BuildPreflightWarningBody(IReadOnlyList<DiskSpaceHelper.BackupDriveSpace> drives, int bookCount)
{
var sb = new StringBuilder();
sb.AppendLine($"You are about to back up {bookCount} books. Libation estimates you may need on the order of {FormatBytes(bookCount * DiskSpaceHelper.EstimatedBytesPerAudiobookBackup)} total, plus extra room for temporary files during each download.");
sb.AppendLine($"You are about to back up {bookCount} books. Libation estimates you may need on the order of {FormatBytes(bookCount * DiskSpaceHelper.EstimatedBytesPerAudiobookBackup)} on your Books drive for the finished files, plus about {FormatBytes(DiskSpaceHelper.EstimatedBytesPerAudiobookBackup)} free on your In progress drive at a time for temporary download/decrypt files.");
sb.AppendLine();
sb.AppendLine("Books and In progress can be on different drives. A warning below for your In progress drive does not necessarily mean your Books location is full.");
sb.AppendLine();
AppendDriveLines(sb, drives);
sb.AppendLine();
@@ -47,15 +50,32 @@ public static class DiskFullUserMessage
{
foreach (var drive in drives)
{
// "unknown" when DriveInfo could not report space (typical for some network paths); user may still have chosen Continue.
var free = drive.AvailableBytes is null ? "unknown" : FormatBytes(drive.AvailableBytes.Value);
var needed = FormatBytes(drive.RequiredBytes);
sb.AppendLine($"{drive.DriveRoot} Free: {free} Estimated needed: {needed}");
var role = DescribeDriveUsage(drive.Usage);
sb.AppendLine($"{drive.DriveRoot} ({role}) Free: {free} Estimated needed: {needed}");
foreach (var path in drive.Paths)
sb.AppendLine($" {path}");
}
}
private static string DescribeDriveUsage(DiskSpaceHelper.BackupDriveUsage usage)
{
var hasBooks = usage.HasFlag(DiskSpaceHelper.BackupDriveUsage.Books);
var hasInProgress = usage.HasFlag(DiskSpaceHelper.BackupDriveUsage.InProgress);
if (hasBooks && hasInProgress)
return "Books + In progress";
if (hasBooks)
return "Books";
if (hasInProgress)
return "In progress";
return "Libation paths";
}
private static string FormatBytes(long bytes)
{
const long gb = 1024L * 1024 * 1024;
@@ -1,6 +1,5 @@
using LibationFileManager;
using LibationUiBase.Forms;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LibationUiBase;
@@ -13,18 +12,29 @@ public static class DiskSpaceBackupPreflight
{
private const int BulkBackupBookThreshold = 2;
public static async Task<bool> ConfirmBulkBackupAsync(int bookCount, Configuration config)
private static bool bulkPreflightConfirmedForQueueRun;
/// <summary>Clears the per-queue-run preflight flag when the backup queue finishes.</summary>
public static void ResetBulkPreflightForQueueRun() => bulkPreflightConfirmedForQueueRun = false;
public static async Task<bool> ConfirmBulkBackupAsync(
int bookCount,
Configuration config,
bool backupQueueAlreadyRunning = false)
{
if (bookCount < BulkBackupBookThreshold)
if (bookCount < BulkBackupBookThreshold
|| backupQueueAlreadyRunning
|| bulkPreflightConfirmedForQueueRun)
return true;
var drives = DiskSpaceHelper.GetBackupDriveSpaces(config, bookCount);
// All roots unknown, or all have enough reported space: queue without prompting.
if (DiskSpaceHelper.HasSufficientSpaceForBulkBackup(drives))
{
bulkPreflightConfirmedForQueueRun = true;
return true;
}
// Known space below CriticalFreeBytes: do not offer Continue (avoids starting a huge queue on a full local disk).
if (DiskSpaceHelper.AnyDriveCriticallyLow(drives))
{
await MessageBoxBase.Show(
@@ -35,7 +45,6 @@ public static class DiskSpaceBackupPreflight
return false;
}
// At least one root reported space below estimate but above critical (or mixed known/unknown with a shortfall).
var result = await MessageBoxBase.Show(
DiskFullUserMessage.BuildPreflightWarningBody(drives, bookCount),
DiskFullUserMessage.DialogCaption,
@@ -43,6 +52,9 @@ public static class DiskSpaceBackupPreflight
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (result == DialogResult.Yes)
bulkPreflightConfirmedForQueueRun = true;
return result == DialogResult.Yes;
}
}
@@ -39,6 +39,7 @@ public class LibationContributor
GitHubUser("Shuvashish76"),
GitHubUser("RokeJulianLockhart"),
GitHubUser("maaximal"),
GitHubUser("matalvernaz"),
GitHubUser("muchtall"),
GitHubUser("ScubyG"),
GitHubUser("patienttruth"),
@@ -173,6 +173,11 @@ public class ProcessBookViewModel : ReactiveObject
ProcessBookResult result = ProcessBookResult.None;
try
{
// Optional steps (e.g. PDF when the book has no supplement) use Validate() to mean "does not apply".
// Skip them instead of treating ProcessSingleAsync's "Validation failed" as a queue failure.
if (!processable.Validate(LibraryBook))
return ProcessBookResult.Success;
LinkProcessable(processable);
var statusHandler = await processable.ProcessSingleAsync(LibraryBook, validate: true);
@@ -188,7 +188,7 @@ public class ProcessQueueViewModel : ReactiveObject
if (toLiberate.Length > 0)
{
// May no-op when free space is unknown (common on UNC); see DiskSpaceBackupPreflight.
if (!await DiskSpaceBackupPreflight.ConfirmBulkBackupAsync(toLiberate.Length, config))
if (!await DiskSpaceBackupPreflight.ConfirmBulkBackupAsync(toLiberate.Length, config, backupQueueAlreadyRunning: Running))
return false;
Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length);
@@ -255,7 +255,16 @@ public class ProcessQueueViewModel : ReactiveObject
&& entry.Status is ProcessBookStatus.Completed
&& Queue.RemoveCompleted(entry);
/// <summary>
/// ProcessBookViewModel requires a captured UI SynchronizationContext. Callers may resume on a
/// thread-pool thread after await (e.g. auto-download after BackgroundWorker).
/// </summary>
private void RunOnQueueUiThread(Action action) => Invoke(action);
private void AddDownloadPdf(IList<LibraryBook> entries, Configuration config)
=> RunOnQueueUiThread(() => addDownloadPdfCore(entries, config));
private void addDownloadPdfCore(IList<LibraryBook> entries, Configuration config)
{
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
@@ -266,6 +275,9 @@ public class ProcessQueueViewModel : ReactiveObject
}
private void AddDownloadDecrypt(IList<LibraryBook> entries, Configuration config)
=> RunOnQueueUiThread(() => addDownloadDecryptCore(entries, config));
private void addDownloadDecryptCore(IList<LibraryBook> entries, Configuration config)
{
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
@@ -276,6 +288,9 @@ public class ProcessQueueViewModel : ReactiveObject
}
private void AddConvertMp3(IList<LibraryBook> entries, Configuration config)
=> RunOnQueueUiThread(() => addConvertMp3Core(entries, config));
private void addConvertMp3Core(IList<LibraryBook> entries, Configuration config)
{
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
@@ -372,6 +387,10 @@ public class ProcessQueueViewModel : ReactiveObject
{
Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
}
finally
{
DiskSpaceBackupPreflight.ResetBulkPreflightForQueueRun();
}
string timeToStr(TimeSpan time)
=> time.TotalHours < 1 ? $"{time:mm\\:ss}"
@@ -96,6 +96,14 @@ public partial class Form1
WebView2LoginErrorMessage.Caption,
webViewEx);
}
else if (NonJsonResponseExceptionExtensions.TryFindInTree(ex, out var htmlEx) && htmlEx is not null)
{
MessageBoxLib.ShowAdminAlert(
this,
htmlEx.GetExplainerBody(),
NonJsonResponseExceptionExtensions.LibraryScanFailedCaption,
htmlEx);
}
else
{
MessageBoxLib.ShowAdminAlert(
+13 -7
View File
@@ -1,5 +1,6 @@
using ApplicationServices;
using AudibleUtilities;
using Dinah.Core.Threading;
using Dinah.Core.WindowsDesktop.Drawing;
using FileManager;
using LibationFileManager;
@@ -26,14 +27,19 @@ public partial class Form1
// wire-up event to automatically download after scan.
// winforms only. this should NOT be allowed in cli
updateCountsBw.RunWorkerCompleted += async (object? sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
{
if (!Configuration.Instance.AutoDownloadEpisodes || e.Result is not LibraryCommands.LibraryStats libraryStats)
return;
updateCountsBw.RunWorkerCompleted += (_, e) => tryAutoDownloadAfterCounts(e);
}
if ((libraryStats.PendingBooks + libraryStats.pdfsNotDownloaded) > 0)
await BackupAllBooksAsync(libraryStats.LibraryBooks);
};
private void tryAutoDownloadAfterCounts(System.ComponentModel.RunWorkerCompletedEventArgs e)
{
if (!Configuration.Instance.AutoDownloadEpisodes || e.Result is not LibraryCommands.LibraryStats libraryStats)
return;
if (libraryStats.PendingBooks + libraryStats.pdfsNotDownloaded <= 0)
return;
// RunWorkerCompleted has no SynchronizationContext; queue items require the UI thread.
this.UIThreadAsync(() => _ = BackupAllBooksAsync(libraryStats.LibraryBooks));
}
private static object? LoadResourceImage(string resourceName)
@@ -63,7 +63,8 @@ public class ConditionalTagCollectionTests
[DataRow("(?'name)abc", "test_value", DisplayName = "InvalidRegexPattern_InvalidGroup")]
[DataRow("(a+)+b", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", DisplayName = "CatastrophicBacktracking_NestedQuantifiers")]
[DataRow("(a|aa|aaa|aaaa)*?b", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", DisplayName = "CatastrophicBacktracking_AlternationOverlap")]
[DataRow("(a+a+)+b", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", DisplayName = "CatastrophicBacktracking_RepeatedConcatenation")]
// (a+a+)+b needs a longer input than other evil-regex cases to exceed the 100ms match timeout on .NET
[DataRow("(a+a+)+b", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", DisplayName = "CatastrophicBacktracking_RepeatedConcatenation")]
[DataRow("^(a+)+$", "aaaaaaaaaaaaaaaaaaaaaab", DisplayName = "CatastrophicBacktracking_AnchoredRepeated")]
[DataRow("(a*)*b", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", DisplayName = "CatastrophicBacktracking_StarStar")]
public void ConditionalTag_InvalidRegexPattern_ThrowsInvalidOperationException(string pattern, string testValue)
@@ -31,4 +31,99 @@ public class DiskSpaceHelperTests
Assert.IsTrue(DiskSpaceHelper.ErrorMessageIndicatesDiskFull("No space left on device"));
Assert.IsFalse(DiskSpaceHelper.ErrorMessageIndicatesDiskFull("Unable to read beyond the end of the stream."));
}
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void Windows_NormalizePathForDriveQuery_strips_extended_prefix()
{
Assert.AreEqual(@"C:\Audiobooks\Books", DiskSpaceHelper.NormalizePathForDriveQuery(@"\\?\C:\Audiobooks\Books"));
Assert.AreEqual(@"\\server\share\Books", DiskSpaceHelper.NormalizePathForDriveQuery(@"\\?\UNC\server\share\Books"));
}
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void Windows_GetPathRootForDiskSpaceCheck_strips_extended_prefix()
{
Assert.AreEqual(@"C:\", DiskSpaceHelper.GetPathRootForDiskSpaceCheck(@"\\?\C:\Audiobooks\Books"));
}
[TestMethod]
public void GetPathRootForDiskSpaceCheck_unix_absolute_path()
{
if (OperatingSystem.IsWindows())
Assert.Inconclusive("Skipped because OS is Windows.");
Assert.AreEqual("/", DiskSpaceHelper.GetPathRootForDiskSpaceCheck("/home/user/Libation/Books"));
}
[TestMethod]
public void GetRequiredBytesForDriveUsage_uses_batch_for_books_only()
{
const int bookCount = 500;
var expected = bookCount * DiskSpaceHelper.EstimatedBytesPerAudiobookBackup;
Assert.AreEqual(expected, DiskSpaceHelper.GetRequiredBytesForDriveUsage(DiskSpaceHelper.BackupDriveUsage.Books, bookCount));
Assert.AreEqual(
expected,
DiskSpaceHelper.GetRequiredBytesForDriveUsage(DiskSpaceHelper.BackupDriveUsage.Books | DiskSpaceHelper.BackupDriveUsage.InProgress, bookCount));
}
[TestMethod]
public void GetRequiredBytesForDriveUsage_uses_single_title_for_in_progress_only()
{
Assert.AreEqual(
DiskSpaceHelper.EstimatedBytesPerAudiobookBackup,
DiskSpaceHelper.GetRequiredBytesForDriveUsage(DiskSpaceHelper.BackupDriveUsage.InProgress, 500));
}
[TestMethod]
public void GetCriticalFreeBytesForDriveUsage_in_progress_requires_one_title_plus_margin()
{
Assert.AreEqual(
DiskSpaceHelper.EstimatedBytesPerAudiobookBackup + DiskSpaceHelper.InProgressPreflightMarginBytes,
DiskSpaceHelper.GetCriticalFreeBytesForDriveUsage(DiskSpaceHelper.BackupDriveUsage.InProgress));
Assert.AreEqual(
DiskSpaceHelper.CriticalFreeBytes,
DiskSpaceHelper.GetCriticalFreeBytesForDriveUsage(DiskSpaceHelper.BackupDriveUsage.Books));
}
[TestMethod]
public void HasSufficientSpaceForBulkBackup_in_progress_not_judged_by_batch_estimate()
{
var drives = new[]
{
new DiskSpaceHelper.BackupDriveSpace(
@"D:\",
[@"D:\Books"],
500L * 1024 * 1024 * 1024,
500L * DiskSpaceHelper.EstimatedBytesPerAudiobookBackup,
DiskSpaceHelper.BackupDriveUsage.Books),
new DiskSpaceHelper.BackupDriveSpace(
@"C:\",
[@"C:\Temp\Libation-user"],
600L * 1024 * 1024,
DiskSpaceHelper.EstimatedBytesPerAudiobookBackup,
DiskSpaceHelper.BackupDriveUsage.InProgress),
};
Assert.IsTrue(DiskSpaceHelper.HasSufficientSpaceForBulkBackup(drives));
}
[TestMethod]
public void AnyDriveCriticallyLow_in_progress_uses_single_title_threshold()
{
var drives = new[]
{
new DiskSpaceHelper.BackupDriveSpace(
@"C:\",
[@"C:\Temp\Libation-user"],
200L * 1024 * 1024,
DiskSpaceHelper.EstimatedBytesPerAudiobookBackup,
DiskSpaceHelper.BackupDriveUsage.InProgress),
};
Assert.IsTrue(DiskSpaceHelper.AnyDriveCriticallyLow(drives));
Assert.IsFalse(DiskSpaceHelper.HasSufficientSpaceForBulkBackup(drives));
}
}
@@ -954,8 +954,12 @@ namespace TemplatesTests
var old = Thread.CurrentThread.CurrentCulture;
var oldUi = Thread.CurrentThread.CurrentUICulture;
var oldDefault = CultureInfo.DefaultThreadCurrentCulture;
var oldDefaultUi = CultureInfo.DefaultThreadCurrentUICulture;
try
{
CultureInfo.DefaultThreadCurrentCulture = null;
CultureInfo.DefaultThreadCurrentUICulture = null;
Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-SE");
Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-ES");
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
@@ -966,6 +970,8 @@ namespace TemplatesTests
{
Thread.CurrentThread.CurrentCulture = old;
Thread.CurrentThread.CurrentUICulture = oldUi;
CultureInfo.DefaultThreadCurrentCulture = oldDefault;
CultureInfo.DefaultThreadCurrentUICulture = oldDefaultUi;
}
result.Should().Be(expected);
+116
View File
@@ -0,0 +1,116 @@
# Spatial Audio, Dolby Atmos, and Widevine DRM
> [!IMPORTANT]
> tl;dr: Libation **cannot** download Dolby Atmos or spatial audio from Audible and it will **never** be able to do so in the future.
## Short answer
Cryptography is always a game of cat and mouse, so it's *possible* someone will release a hack tomorrow. However this is extremely unlikely. The cryptography discussed below is hardware-based DRM which has been used for years by huge companies including Netflix, Amazon Prime, and Disney+. A small army of hackers has been going at this since 2014 -- [no general reproducible hack has been shown](https://www.forasoft.com/learn/video-streaming/articles-streaming/widevine-l1-l2-l3). I'd love to be proven wrong but I'm not holding my breath.
**What is still possible with Libation**
- Download and decrypt most titles in **stereo** (AAC-LC, or xHE-AAC with Widevine enabled). (The Libation settings **Use Widevine DRM** or **Request xHE-AAC Codec** only affect **stereo** formats (AAC-LC or xHE-AAC). They do not restore the Atmos mix.)
- Play spatial files you **already downloaded before January 2026**, if your media player supports E-AC-3 or AC-4.
**What is not possible with Libation**
- Download new Dolby Atmos / spatial (E-AC-3 or AC-4) content from Audible.
- Get the Atmos mix by toggling Widevine or xHE-AAC settings.
For true spatial playback of current catalog titles, use the **Audible app** on a supported device. See [Dolby Atmos and spatial playback](#dolby-atmos-and-spatial-playback) at the bottom of this page, and [What you can do today](#what-you-can-do-today) for stereo download steps.
## What changed
### Libation 13.1.3 removed spatial download
In [Libation 13.1.3](https://github.com/rmcrackan/Libation/releases/tag/v13.1.3), the **Request Spatial Audio** and **Spatial Audio Codec** settings were removed from the UI. Internally, spatial requests are disabled. See [GitHub issue #1553](https://github.com/rmcrackan/Libation/issues/1553).
### Audible began requiring Widevine L1 for spatial titles
For a time, Audible delivered spatial (Dolby Atmos) audiobooks using **Widevine L3** -- software-based DRM. Libation could work with L3 thanks to community research, notably [MSWMan's Widevine CDM work](https://github.com/rmcrackan/Libation/issues/1553).
In early January 2026, Audible began requiring **Widevine L1** for many spatial titles. L1 ties decryption to hardware in a trusted execution environment -- the same class of protection used by Netflix, Disney+, and Prime Video. Libation runs on desktop with a software (L3) CDM and cannot satisfy L1 license requests.
When Libation (or any L3 client) requests spatial audio today, Audible typically responds with errors such as:
- `DRM license response not "OK"`
- `"The device is not allowed to consume content using this codec"` / `"InsufficientSecurityLevel"`
Streaming license requests for spatial content fail the same way. The official Audible Android app can still download spatial titles because it uses the phone's hardware-backed keys.
### What still works
| Format | Widevine level | Libation today |
| :--- | :--- | :--- |
| AAC-LC (AAXC / ADRM) | N/A | Yes, default path |
| xHE-AAC stereo (Widevine) | L3 | Yes, with **Use Widevine DRM** + **Request xHE-AAC** |
| Dolby Atmos / spatial (E-AC-3, AC-4) | L1 (for many titles) | No |
xHE-AAC via Widevine L3 continues to work for many books. That gives you higher-quality **stereo**, not Atmos.
> [!NOTE]
> Audible may expand stricter Widevine requirements over time. Some Audible Plus titles now require Widevine where AAXC no longer works ([#1580](https://github.com/rmcrackan/Libation/issues/1580)). If ordinary (non-spatial) books start failing with license errors, report them on GitHub with logs so the project can track how far Audible is taking these changes.
## What you can do today
### 1. Back up books you care about
Download and decrypt titles while Libation still supports the format Audible delivers for that book. DRM policies can change per title without notice.
### 2. Get the best stereo quality Libation can offer
1. Open **Settings** and enable **Use Widevine DRM**.
2. Enable **Request xHE-AAC Codec**.
3. Re-add your account if Libation prompts you (Widevine requires an Android-style device registration).
4. Re-download the title.
See [Audio File Formats](../features/audio-file-formats.md) for codec details and [Supported Media Players](../features/audio-file-formats.md#supported-media-players) if you have trouble playing xHE-AAC.
### 3. If downloads fail even with spatial disabled
Check your log file ([FAQ: Where Can I Find the Log File?](/docs/frequently-asked-questions#where-can-i-find-the-log-file)) and search [open issues](https://github.com/rmcrackan/Libation/issues). Some failures are Audible-side (Plus throttling, temporary outages) rather than spatial-related. See also [Troubleshooting](./troubleshoot.md).
## Will spatial download come back?
The Libation maintainers are watching Audible's DRM changes and will adapt if a viable path appears. There is no widely shared, reliable public method to decrypt Widevine L1 content on arbitrary PCs, and the project is not optimistic about spatial downloads returning soon.
Discussion continues on [GitHub issue #1553](https://github.com/rmcrackan/Libation/issues/1553). Maintainer context is also in [this Reddit comment](https://old.reddit.com/r/audiobooks/comments/1qu1qvy/libation_users_how_to_you_listen_to_your/o37dyw9/) from the Libation author.
## Dolby Atmos and spatial playback
> [!IMPORTANT]
> The **Request Spatial Audio** setting was **removed in Libation 13.1.3** (January 2026). Libation cannot download new spatial/Atmos content from Audible today. The sections below cover **playback** of spatial files you may have downloaded before that change.
### Does "Use Widevine DRM" download spatial audio?
**No.** **Use Widevine DRM** unlocks **Request xHE-AAC Codec** and higher-bitrate stereo formats. It does **not** re-enable spatial/Atmos download.
For titles where Audible still serves a high-quality stereo Widevine stream, Widevine + xHE-AAC is the best quality Libation can obtain. That is not the same as the Dolby Atmos mix in the Audible app.
### Listen in Dolby Atmos / spatial today
- **Dolby Atmos / spatial download** is not available in current Libation versions.
- Use the **Audible app** on a phone, tablet, or speaker that supports spatial playback for the full Atmos mix.
- **True spatial playback** from files you own: only if you backed up spatial titles before January 2026 (see below).
### Playing spatial files you already have
If you liberated spatial titles before January 2026, your files may be **E-AC-3** or **AC-4**. Libation no longer downloads new spatial content, but existing files remain playable if your player supports those codecs.
Spatial audiobooks from Audible use:
- [E-AC-3 (Dolby Digital Plus Atmos)](../features/audio-file-formats.md#e-ac-3)
- [AC-4 (Dolby AC-4 Immersive Stereo)](../features/audio-file-formats.md#ac-4)
See [Supported Media Players](../features/audio-file-formats.md#supported-media-players) for an incomplete compatibility list.
**Playing a file is not the same as hearing Atmos.** Many players decode E-AC-3 as plain 5.1 surround without rendering spatial objects. Dolby Atmos rendering often requires extra software (for example [Dolby Access](https://apps.microsoft.com/detail/9n0866fs04w8) on Windows) or hardware support. See [Spatial Audio and Dolby Atmos](../features/audio-file-formats.md#spatial-audio-and-dolby-atmos) for more detail.
If you expected Atmos but your **new** download is stereo, that is expected with current Libation versions.
## Related documentation
- [Audio File Formats](../features/audio-file-formats.md) -- codecs, settings, and media players
- [FAQ: Non-spatial playback trouble](/docs/frequently-asked-questions#im-having-trouble-playing-my-non-spatial-audiobook-how-can-i-fix-this)
- [Troubleshooting](./troubleshoot.md)
+19
View File
@@ -14,6 +14,15 @@ There are two possible causes of this error.
1. [Run hangover](#how-to-run-the-hangover-app) and execute the following command in the "Database" tab: `PRAGMA journal_mode=DELETE`
2. run this command in your terminal: `sqlite3 "path/to/libation/files/LibationContext.db" "PRAGMA journal_mode=DELETE;"`
## Library scan fails ("Unexpected character" or "HTML instead of JSON")
Audible returned an HTML page instead of JSON. Common causes: transient outage, expired login, VPN/proxy, or rate limiting. What to try:
1. Scan again after a few minutes.
2. Sign in to Audible in a browser on the same network.
3. Disable VPN/proxy and scan again.
4. Remove and re-add the account in Libation.
## How to run the Hangover App
When troubleshooting, you may be asked to run 'Hangover'. Hangover is a debugging app to help diagnose and solve some problems with Libation.
@@ -91,3 +100,13 @@ Symptoms include a crash on startup that mentions `LibationContext.db` under a p
**Fix:** edit `appsettings.json` in the active revision (for example under `~/snap/libation/current/...`) so the `LibationFiles` value uses the **same** `.../snap/libation/<number>/...` as that file, or use `LIBATION_FILES_DIR`. Step-by-step context: [Install on Linux - Snap](/docs/installation/linux#snap) and [issue #1776](https://github.com/rmcrackan/Libation/issues/1776).
3. **Non-Snap build** - If you still suspect Snap confinement after the above, try a `.deb` / `.rpm` / AppImage build from [Releases](https://github.com/rmcrackan/Libation/releases) to compare behavior.
## Download fails with "DRM license response not OK" or "Content license denied"
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. **Title requires Widevine** -- some Plus titles no longer download as AAXC; enable **Use Widevine DRM** in Settings and re-add your account if prompted. See [issue #1580](https://github.com/rmcrackan/Libation/issues/1580).
3. **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).
Attach your log file when opening a GitHub issue.
+28 -33
View File
@@ -18,22 +18,14 @@ Audiobooks can be requested from Audible as "Normal" quality or "High" quality,
### Use Widevine DRM
When this setting is disabled, all audiobooks will be downloaded using Audible's in-house DRM (AAX(C)) in the [AAC-LC](#aac-lc) format.
When this setting is enabled, Libation will request audio files protected by Google's Widevine Digital Rights Managements scheme, and two additional settings will be unlocked: [Request xHE-AAC Codec](#request-xhe-aac-codec) and [Request Spatial Audio](#request-spatial-audio) (explained further below).
When this setting is enabled, Libation will request audio files protected by Google's Widevine DRM scheme. This unlocks [Request xHE-AAC Codec](#request-xhe-aac-codec) for higher-quality stereo downloads on titles where Audible delivers them via Widevine L3.
If you don't enable either of those additional options, then enabling 'Use Widevine DRM' will have no pratcical effect in nearly all circumstances. Audiobooks will be downloaded in the same [AAC-LC](#aac-lc) format with the same bitrate and the same number of audio channels. On rare occasions, enabling 'Use Widevine DRM' without the other two options will result in audio files with a different bitrate.
If you don't enable **Request xHE-AAC Codec**, then enabling **Use Widevine DRM** will have no practical effect in nearly all circumstances. Audiobooks will be downloaded in the same [AAC-LC](#aac-lc) format with the same bitrate and the same number of audio channels. On rare occasions, enabling **Use Widevine DRM** without xHE-AAC will result in audio files with a different bitrate.
### Request xHE-AAC Codec
Enable this setting to request audiobooks in the [xHE-AAC](#xhe-aac) format. This codec is generally better quality than the [AAC-LC](#aac-lc) codec at the same bitrate, but it isn't as commonly supported by media players, so you may have some difficulty playing these audiobooks. The highest bitrate version of some audiobooks is only available as [xHE-AAC](#xhe-aac).
### Request Spatial Audio
Enable this setting to request audiobooks in a "spatial" ([Dolby Atmos](#dolby-atmos)) audio format. If an audiobook is not available in a spatial format, it will instead be downloaded in the [xHE-AAC codec](#xhe-aac).
### Spatial Audio Codec
Choose whether spatial audiobooks are downloaded in the [E-AC-3](#e-ac-3) or [AC-4](#ac-4) format.
### Download My Books in the Original Audio Format (Lossless)
If selected, Audiobooks will be downloaded and saved in the format delivered by audible (which depends on the settings explained above). Libation will not change the audio.
@@ -77,28 +69,6 @@ Note that the xHE-AAC files authored by Audible have some USAC conformance error
- Disagreement between stts and UsacFrame usacIndependencyFlag value.
- Stts indicating a frame is an immediate play-out frame, but USAC AudioPreRoll is absent.
### Dolby Atmos
Atmos is a surround sound technology that expands on existing surround sound systems by adding height channels as well as free-moving sound objects. Audible delivers Dolby Atmos in two formats: E-AC-3 and AC-4.
Your device's ability to play audio from these formats does not necessarily mean that the audio you are hearing is Atmos (spatial). For instance, downloading the AC-4 codec for Windows ([links in the [Supported media Players](#supported-media-players) section) will enable you to play AC-4 audiobooks, but you'll still need to download [Dolby Access](https://apps.microsoft.com/detail/9n0866fs04w8?hl=en-US&gl=US) and pay $15 to enable _Dolby Atmos For Headphones_. Please refer to [this comment](https://github.com/rmcrackan/Libation/pull/1331#discussion_r2268660524) for additional context.
#### E-AC-3
**Full Name:** Dolby Digital Plus (a.k.a Enhanced AC-3, DDP, DD+, and EC-3)
**Description:**
A proprietary digital audio compression scheme developed by Dolby Digital for the transport and storage of multichannel audio. This format can be extended to add support for Atmos, making the codec _Dolby Digital Plus Atmos_. _Dolby Digital Plus Atmos_ is backwards compatible with Dolby Digital Plus, so any media player capable of playing Dolby Digital Plus can play _Dolby Digital Plus Atmos_. Audible spatial audiobooks downloaded in the E-AC-3 format are _Dolby Digital Plus Atmos_. If they are played by a media player that supports Atmos, they will play as Atmos audio. If they are played by a media player that does not support Atmos, they will be played as traditional 5.1 surround audio.
#### AC-4
**Full Name:** Dolby AC-4
**Description:**
A proprietary audio compression technology developed by Dolby Digital for the transport and storage of audio channels and/or audio objects. Audible spatial audiobooks downloaded in the AC-4 format are 2-channel AC-4 Immersive Stereo (AC4-IMS) audio, intended for playback in headphones or earbuds (though apparently [not supported on Apple devices](https://github.com/rmcrackan/Libation/issues/996#issuecomment-3169574514)).
## Supported Media Players
Below is an incomplete matrix of codec support across various media players and platforms.
@@ -115,9 +85,34 @@ Below is an incomplete matrix of codec support across various media players and
1. Windows 11 22H2 and later
2. On Windows [prior to Windows 11, version 24H2](https://support.microsoft.com/en-us/windows/codecs-in-media-player-d5c2cdcd-83a2-4805-abb0-c6888138e456). You can still get the codec by running the following command from a Windows PowerShell console: `winget install --id 9nvjqjbdkn97`
3. As mentioned in the [Dolby Atmos](#dolby-atmos) section, just because a media player can play a file does not mean it's rendering Atmos. _Dolby Digital Plus Atmos_ is backwards compatible with _Dolby Digital Plus_, so media players which only support _Dolby Digital Plus_ will play E-AC-3 audio files as regular 5.1 surround without rendering the Atmos spatial qualities. Additional software or hardware support may be required for Dolby Atmos playback.
3. As mentioned in [Spatial Audio and Dolby Atmos](#spatial-audio-and-dolby-atmos), just because a media player can play a file does not mean it's rendering Atmos. _Dolby Digital Plus Atmos_ is backwards compatible with _Dolby Digital Plus_, so media players which only support _Dolby Digital Plus_ will play E-AC-3 audio files as regular 5.1 surround without rendering the Atmos spatial qualities. Additional software or hardware support may be required for Dolby Atmos playback.
4. You can download the AC-4 codec for Windows from 3rd party sites like [Major Geeks](https://www.majorgeeks.com/files/details/dolby_ac_3ac_4_installer.html) and [Free-Codecs](https://www.free-codecs.com/dolby-ac-4-decoder_download.htm). Once you install the codec bundle from one of those sources, the Windows store app will keep it updated. Read more about the process [in this comment](https://github.com/rmcrackan/Libation/pull/1331#discussion_r2268660524).
5. All Android devices will support AAC-LC and xHE-AAC. Some manufactures (such as Samsung) will include Dolby codecs for playing E-AC-3 and AC-4 audio.
6. requires FFmpeg to be [built with fdk-aac](https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_aac). You will almost certainly not find pre-build binaries in the wild due to licensing restrictions.
7. Requires the [fdk-aac plugin](https://www.foobar2000.org/components/view/foo_pd_aac) (Windows only)
8. Requires audio file extensions to be `.m4a` or `.mp4`. Libation sets the file extensions to `.m4b`, so you must manually change it to `.m4a` by renaming the audio file.
## Spatial Audio and Dolby Atmos
> [!IMPORTANT]
> **Download status (2026):** The **Request Spatial Audio** and **Spatial Audio Codec** settings were removed in Libation 13.1.3. Audible now requires Widevine L1 (hardware-backed DRM) for many spatial titles, which Libation cannot use. Libation does not currently download new spatial/Atmos content. The sections below describe E-AC-3 and AC-4 for **playback** of files downloaded before the change. See [Spatial Audio, Dolby Atmos, and Widevine DRM](../advanced/spatial-audio.md) for full details and [issue #1553](https://github.com/rmcrackan/Libation/issues/1553).
Atmos is a surround sound technology that expands on existing surround sound systems by adding height channels as well as free-moving sound objects. Audible delivers Dolby Atmos in two formats: E-AC-3 and AC-4.
Your device's ability to play audio from these formats does not necessarily mean that the audio you are hearing is Atmos (spatial). For instance, downloading the AC-4 codec for Windows ([links in the [Supported media Players](#supported-media-players) section) will enable you to play AC-4 audiobooks, but you'll still need to download [Dolby Access](https://apps.microsoft.com/detail/9n0866fs04w8?hl=en-US&gl=US) and pay $15 to enable _Dolby Atmos For Headphones_. Please refer to [this comment](https://github.com/rmcrackan/Libation/pull/1331#discussion_r2268660524) for additional context.
### E-AC-3
**Full Name:** Dolby Digital Plus (a.k.a Enhanced AC-3, DDP, DD+, and EC-3)
**Description:**
A proprietary digital audio compression scheme developed by Dolby Digital for the transport and storage of multichannel audio. This format can be extended to add support for Atmos, making the codec _Dolby Digital Plus Atmos_. _Dolby Digital Plus Atmos_ is backwards compatible with Dolby Digital Plus, so any media player capable of playing Dolby Digital Plus can play _Dolby Digital Plus Atmos_. Audible spatial audiobooks downloaded in the E-AC-3 format are _Dolby Digital Plus Atmos_. If they are played by a media player that supports Atmos, they will play as Atmos audio. If they are played by a media player that does not support Atmos, they will be played as traditional 5.1 surround audio.
### AC-4
**Full Name:** Dolby AC-4
**Description:**
A proprietary audio compression technology developed by Dolby Digital for the transport and storage of audio channels and/or audio objects. Audible spatial audiobooks downloaded in the AC-4 format are 2-channel AC-4 Immersive Stereo (AC4-IMS) audio, intended for playback in headphones or earbuds (though apparently [not supported on Apple devices](https://github.com/rmcrackan/Libation/issues/996#issuecomment-3169574514)).
+3 -3
View File
@@ -167,7 +167,7 @@ Text formatting can change length and case of the text. Use \<#\>, \<#\>\<case\>
| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | `<series[unique()]>`<hr>`<series[unique({N:L})`<br>`separator(; )]>` | Sherlock Holmes; Some Other Series<hr>sherlock holmes; some other series |
| sort(N \| # \| ID) | Sorts the series by name, number or ID.<br><br>These terms define the primary, secondary, tertiary, … sorting order.<br>You may combine multiple terms in sequence to specify multilevel sorting.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<series[sort(N)`<br>`separator(; )]>` | Book Collection, 1; Sherlock Holmes, 1-6 |
| max(#) | Only use the first # of series | `<series[max(1)]>` | Sherlock Holmes |
| slice(#..#) | Only use entries of the series list starting from # and ending at # (inclusive)<br><br>See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `<series[slice(..-2)]>` | Sherlock Holmes |
| slice(#..#) | Only use entries of the series list starting from # and ending at # (inclusive)<br><br>See [Text List Formatter Usage](#text-list-formatters) above for details on all the variants of `slice()` | `<series[slice(..-2)]>` | Sherlock Holmes |
| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of series using the specified [format](#number-formatters). | `<series[count()]>`<hr>`<series[count(00)]>` | 2<hr>02 |
| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each series is first formatted using the specified [Series Format](#series-formatters) (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.<br><br>**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`<br>- `FORMAT`: Optional series format to apply to each entry (e.g., `{N}`, `{N:L}`, `{#}`); defaults to `{N}`<br>- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)<br>- `VALUE`: The value to compare against | `<series[filter({N} = 'Holmes')]>`<hr>`<series[filter(~ "Sherlock")]>` | Sherlock Holmes<hr>Sherlock Holmes |
@@ -179,7 +179,7 @@ Text formatting can change length and case of the text. Use \<#\>, \<#\>\<case\>
| Formatter | Description | Example Usage | Example Result |
|----------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------- |
| \{T \| F \| M \| L \| S \| ID\} **†** | Formats the human name using<br>the name part tags.<br>\{T:[Text_Formatter](#text-formatters)\} = Title (e.g. "Dr.")<br>\{F:[Text_Formatter](#Text-Formatters)\} = First name<br>\{M:[Text_Formatter](#text-formatters)\} = Middle name<br>\{L:[Text_Formatter](#text-formatters)\} = Last Name<br>\{S:[Text_Formatter](#text-formatters)\} = Suffix (e.g. "PhD")<br>\{ID:[Text_Formatter](#text-formatters)\} = Audible Contributor ID<br><br>Formatter parts are optional and introduced by the colon. If specified the string will be used to format the part using the correspoing formatter.<br><br>Default is \{T\} \{F\} \{M\} \{L\} \{S\} | `<first narrator[{L}, {F:1}.]>`<hr>`<first author[{L:u}, {F} _{ID}_]>` | Fry, S.<hr>DOYLE, Arthur \_B000AQ43GQ\_ |
| \{T \| F \| M \| L \| S \| ID\} **†** | Formats the human name using<br>the name part tags.<br>\{T:[Text_Formatter](#text-formatters)\} = Title (e.g. "Dr.")<br>\{F:[Text_Formatter](#text-formatters)\} = First name<br>\{M:[Text_Formatter](#text-formatters)\} = Middle name<br>\{L:[Text_Formatter](#text-formatters)\} = Last Name<br>\{S:[Text_Formatter](#text-formatters)\} = Suffix (e.g. "PhD")<br>\{ID:[Text_Formatter](#text-formatters)\} = Audible Contributor ID<br><br>Formatter parts are optional and introduced by the colon. If specified the string will be used to format the part using the correspoing formatter.<br><br>Default is \{T\} \{F\} \{M\} \{L\} \{S\} | `<first narrator[{L}, {F:1}.]>`<hr>`<first author[{L:u}, {F} _{ID}_]>` | Fry, S.<hr>DOYLE, Arthur \_B000AQ43GQ\_ |
**†** For further information on format templates, please refer to the [Format templates](#format-templates) section.
@@ -192,7 +192,7 @@ Text formatting can change length and case of the text. Use \<#\>, \<#\>\<case\>
| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | `<author[unique()]>`<hr>`<author[unique({L:L})`<br>`separator(; )]>` | Arthur Conan Doyle, Stephen Fry<hr>doyle; fry |
| sort(T \| F \| M \| L \| S \| ID) | Sorts the names by title,<br> first, middle, or last name,<br>suffix or Audible Contributor ID<br><br>These terms define the primary, secondary, tertiary, … sorting order.<br>You may combine multiple terms in sequence to specify multilevel sorting.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<author[sort(M)]>`<hr>`<author[sort(Fl)]>`<hr>`<author[sort(L FM ID)]>` | Stephen Fry, Arthur Conan Doyle<hr>Stephen King, Stephen Fry<hr>John P. Smith \_B000TTTBBB\_, John P. Smith \_B000TTTCCC\_, John S. Smith \_B000HHHVVV\_ |
| max(#) | Only use the first # of names<br><br>Default is all names | `<author[max(1)]>` | Arthur Conan Doyle |
| slice(#..#) | Only use entries of the names list starting from # and ending at # (inclusive)<br><br>See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `<author[slice(..-2)]>` | Arthur Conan Doyle |
| slice(#..#) | Only use entries of the names list starting from # and ending at # (inclusive)<br><br>See [Text List Formatter Usage](#text-list-formatters) above for details on all the variants of `slice()` | `<author[slice(..-2)]>` | Arthur Conan Doyle |
| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of names using the specified [format](#number-formatters). | `<author[count()]>`<hr>`<author[count(00)]>` | 2<hr>02 |
| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each person is first formatted using the specified name format (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.<br><br>**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`<br>- `FORMAT`: Optional name format to apply to each entry (e.g., `{L}`, `{M}`, `{L}, {F}`); defaults to `{T} {F} {M} {L} {S}`<br>- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)<br>- `VALUE`: The value to compare against | `<author[filter({L} = 'Doyle')]>`<hr>`<author[filter({F} ~ '^Stephen')]>`<hr>`<narrator[filter(~ "Fry")]>` | Conan Doyle<hr>Stephen Fry<hr>Stephen Fry |
+40 -7
View File
@@ -2,7 +2,23 @@
## Where Can I Get Help For My Specific Problem?
You can open an issue here for bug reports, feature requests, or specialized help.
You can open an issue here for bug reports, feature requests, or specialized help. For bug reports, attach your log file (see [Where Can I Find the Log File?](#where-can-i-find-the-log-file) below). Reports with limited information or no log may get limited or delayed help.
## Where Can I Find the Log File?
Libation keeps its log files in your Libation files folder. When reporting a bug, attach the Libation log file from that folder. If the folder contains **LibationCrash.log**, attach that too.
**Default log folder locations**
| Platform | Folder |
| - | - |
| Windows | `%userprofile%\Libation` |
| macOS | `~/Library/Application Support/Libation` |
| Linux | `~/.local/share/Libation` |
**macOS:** If that folder does not exist because Libation never starts, try launching from Terminal and note whether it works: `/Applications/Libation.app/Contents/MacOS/Libation` (see [Troubleshooting for macOS](/docs/advanced/troubleshoot#app-wont-start)).
Alternatively, open the log folder from within Libation: open **Settings**, and on the first tab click **Open log folder**.
## What's the Difference Between 'Classic' and 'Chardonnay'?
@@ -35,10 +51,6 @@ If you enabled the [Request xHE-AAC Codec](./features/audio-file-formats.md#requ
1. Use a media player which supports the xHE-AAC codec. [See an incomplete list of media players which support xHE-AAC](./features/audio-file-formats.md#supported-media-players).
2. Disable the [Request xHE-AAC Codec](./features/audio-file-formats.md#request-xhe-aac-codec) option in settings and re-download the audiobook. This will cause Libation to download audiobooks in the [AAC-LC codec](./features/audio-file-formats.md#aac-lc), which enjoys near-universal media player support.
## I'm Having Trouble Playing My Book with 4D, Spatial Audio, or Dolby Atmos, How Can I Fix This?
Spatial audiobooks are delivered in two formats: [E-AC-3](./features/audio-file-formats.md#e-ac-3) and [AC-4](./features/audio-file-formats.md#ac-4). [See an incomplete list of media players which support those codecs](./features/audio-file-formats.md#supported-media-players).
## I'm Having Trouble Logging Into My Brazil Account
For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. The external browser login option is not possible for Brazil. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
@@ -53,5 +65,26 @@ Open `appsettings.json` next to your running install (often under `~/snap/libati
Like many countries, amazon gives South Africa it's own amazon site. [Unlike many other regions](https://www.audible.com/ep/country-selector) there is not South Africa specific audible site. Use `US` for your region -- ie: audible.com.
> [!NOTE]
> (Not exactly a _frequently_ asked question but it's come up more than once)
(Not exactly a *frequently* asked question but it's come up more than once)
## Spatial audio and Dolby Atmos
Libation **cannot download** Dolby Atmos or spatial audio from Audible. Full explanation: [Spatial Audio, Dolby Atmos, and Widevine DRM](./advanced/spatial-audio.md).
### Will Libation ever support spatial audio again?
[No.](./advanced/spatial-audio.md)
### Why Was "Request Spatial Audio" Removed? Can I Still Download Dolby Atmos?
Libation removed the **Request Spatial Audio** option in version 13.1.3 (January 2026). Most Dolby Atmos / spatial titles can no longer be downloaded. For many titles, **Use Widevine DRM** + **Request xHE-AAC** still gets the highest **stereo** quality available -- not the Atmos mix.
### Does "Use Widevine DRM" Still Download Spatial Audio?
No. **Use Widevine DRM** unlocks **Request xHE-AAC Codec** for higher-bitrate stereo. It does not re-enable spatial/Atmos download. See [Does "Use Widevine DRM" download spatial audio?](./advanced/spatial-audio.md#does-use-widevine-drm-download-spatial-audio) on the spatial audio page.
### I'm Having Trouble Playing My Book with 4D, Spatial Audio, or Dolby Atmos, How Can I Fix This?
Libation no longer requests spatial/Atmos from Audible (see [Spatial Audio, Dolby Atmos, and Widevine DRM](./advanced/spatial-audio.md)). Books you liberated before 13.1.3 may be E-AC-3 or AC-4; newer downloads are stereo unless you kept older files.
For spatial files you already have, see [Playing spatial files you already have](./advanced/spatial-audio.md#playing-spatial-files-you-already-have) and [Supported Media Players](./features/audio-file-formats.md#supported-media-players).
+5
View File
@@ -139,3 +139,8 @@ Export your library to Excel, CSV, or JSON
## I Still Need Help
[You can open an issue here](https://github.com/rmcrackan/Libation/issues) for bug reports, feature requests, or specialized help.
## Dolby Atmos / spatial audio
> [!NOTE]
> Libation no longer downloads spatial (Atmos) audiobooks as of version 13.1.3. For highest **stereo** quality, see [Spatial Audio & DRM](./advanced/spatial-audio.md).
+1
View File
@@ -29,6 +29,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
- **[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
## Development
+1 -1
View File
@@ -75,7 +75,7 @@ sudo docker run -d \
## User
This docker image runs as user `1001`. In order for the image to function properly, user `1001` must be able to read and write the volumes that are mounted in. If they are not, you will see errors, including [sqlite error](#1060), [Microsoft.Data.Sqlite.SqliteException](#1110), [unable to open database file](#1113), [Microsoft.EntityFrameworkCore.DbUpdateException](#1049)
This docker image runs as user `1001`. In order for the image to function properly, user `1001` must be able to read and write the volumes that are mounted in. If they are not, you will see errors, including [sqlite error](https://github.com/rmcrackan/Libation/issues/1060), [Microsoft.Data.Sqlite.SqliteException](https://github.com/rmcrackan/Libation/issues/1110), [unable to open database file](https://github.com/rmcrackan/Libation/issues/1113), [Microsoft.EntityFrameworkCore.DbUpdateException](https://github.com/rmcrackan/Libation/issues/1049)
If you're not sure what your user number is, check the output of the `id` command. Docker should normally run with the number of the user who configured and ran it.
+1
View File
@@ -81,6 +81,7 @@ Learn about Libation's powerful features:
### Advanced
- **[Advanced Topics](/docs/advanced/advanced)** - Deep dives and configuration details
- **[Spatial Audio & DRM](/docs/advanced/spatial-audio)** - Why Dolby Atmos download is not available and what still works
### Development
+243 -157
View File
@@ -371,394 +371,419 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"x64"
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@iconify-json/simple-icons": {
@@ -1708,42 +1733,44 @@
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/estree-walker": {
@@ -1753,6 +1780,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/focus-trap": {
"version": "7.6.6",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz",
@@ -2039,6 +2083,18 @@
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
@@ -2260,6 +2316,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -2375,21 +2447,23 @@
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -2398,19 +2472,25 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
@@ -2431,6 +2511,12 @@
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
+3
View File
@@ -11,5 +11,8 @@
},
"devDependencies": {
"vitepress": "^1.6.4"
},
"overrides": {
"vite": "6.4.2"
}
}