diff --git a/Documentation/Advanced.md b/Documentation/Advanced.md
index 9ad2ca08..dfee661f 100644
--- a/Documentation/Advanced.md
+++ b/Documentation/Advanced.md
@@ -12,7 +12,7 @@
- [Custom File Naming](NamingTemplates.md)
- [Command Line Interface](#command-line-interface)
- [Custom Theme Colors](#custom-theme-colors) (Chardonnay Only)
-- [Dolby Atmos, Widevine, Spacial Audio, 4D](DolbyAtmos.md)
+- [Audio Formats (Dolby Atmos, Widevine, Spacial Audio)](AudioFileFormats.md)
diff --git a/Documentation/AudioFileFormats.md b/Documentation/AudioFileFormats.md
new file mode 100644
index 00000000..f5b1ae67
--- /dev/null
+++ b/Documentation/AudioFileFormats.md
@@ -0,0 +1,99 @@
+# Audio Formats Produced by Libation
+
+Libation will download audio in a number of different audio formats, depending on the settings you choose within Libation and the per-title availability of audio formats from Audible. The Libation settings which affect the format downloaded by Libation are shown in the Settings menu screenshot below.
+
+Notes:
+- Audiobook file extensions are either `.m4b` or `.mp3`. Libation uses the `.m4b` file extension for all non-MP3 files, regardless of the audio codec contained therein. Some media players don't recognize the `.m4b` file extension and may require the extension be changed to `.m4a` or `.mp4`.
+- Most (but not all) podcasts are delivered by Audible as native MP3 files. None of the following audio formats and settings discussions pertain to those podcasts because MP3s have no DRM, and those episodes are copied directly to their output folders.
+
+
+
+## Settings Summary
+### Audio quality to request from Audible
+Audiobooks can be requested from Audible as "Normal" quality or "High" quality, matching the settings in the Audible mobile apps. This setting affects the audio bitrate and, sometimes, the number of audio channels. This setting has no effect on the _audio codec_.
+
+### 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).
+
+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.
+
+### 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.
+
+### Download my books as .MP3 files (transcode if necessary).
+If selected, Libation will decode [AAC-LC](#aac-lc), [xHE-AAC](#xhe-aac), and [E-AC-3](#e-ac-3) audiobooks and re-encode them as MP3s using the MP3 encoder settings ([read about LAME MP3 encoder settings](https://lame.sourceforge.io/lame_ui_example.php)). Note that Libation cannot convert [AC-4](#ac-4) audio to MP3.
+
+# Audio Formats
+
+## Traditional Mono and Stereo Formats
+
+### AAC-LC
+#### _Full Name_
+Advanced Audio Coding - Low Complexity
+#### _Description_
+This is the base profile for AAC audio and has existed since AAC's initial release in 1997. It enjoys wide support on nearly every conceivable platform capable of playing digital audio, as ubiquitous as MP3.
+If Widevine support is not enabled, or if the book is not available in the more high-definition formats, Libation will download audiobooks in this format.
+
+### MP3
+#### _Full Name_
+MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
+#### _Description_
+An older (released in 1991) but still nearly universally supported audio codec. Its audio quality is generally worse than AAC-LC at similar bitrates. Audible delivers some podcasts in MP3 format, but no audiobooks are natively availble as MP3. Libation supports converting Audiobooks delivered in other audio formats to MP3. Note that the MP3 format supports a maximum of two audio channels, so multichannel E-AC-3 audio will be downsampled to stereo or mono (depending on the Libation's settings). [AC-4](#ac-4) cannot be converted to MP3.
+
+### xHE-AAC
+#### _Full Name_
+Extended High-Efficiency Advanced Audio Coding
+#### _Description_
+This is a proprietary codec created by the [Fraunhofer Institute for Integrated Circuits IIS](https://www.iis.fraunhofer.de/en/ff/amm/broadcast-streaming/xheaac.html). It combines features of the HE-AAC v2 and the baseline USAC (Unified Speech and Audio Coding) profiles with the parts of the MPEG-D DRC Loudness Control Profile or Dynamic Range Control Profile. Therefore, USAC and xHE-AAC are not synonymous and should not be used interchangeably. A player capable of decoding USAC will not necessarily be able to decode xHE-AAC.
+
+xHE-AAC boasts significantly higher quality audio at low bitrates. Though it has existed since at least 2016, playback support is still quite limited. FFmpeg has recently added partial decoder support for the USAC profiles, but it is insufficient to decode the xHE-AAC audio files acquired from Audible (due to FFmpeg's lack of support for MPEG Surround for Mono to Stereo Upmixing; ISO 23003-3:2012 §7.11)
+
+## 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.
+| Player | [AAC-LC](#aac-lc) | [xHE-AAC](#xhe-aac) | [E-AC-3](#e-ac-3) | [AC-4](#ac-4) |
+| :--- | :---: | :---: | :---: | :---: |
+|Windows Native Support|Yes|Yes1|Yes2,3|Yes4|
+|macOS Native Support|Yes|Yes|Yes3| |
+|Android Native Support5|Yes|Yes| | |
+|FFmpeg (all platforms)|Yes|Yes6|Yes3||
+|[VLC](https://www.videolan.org/vlc/) (Windows)|Yes| |Yes3 | |
+|[foobar2000](https://www.foobar2000.org/components) (Windows and Mac)|Yes|Yes7 | | |
+|[PotPlayer](https://potplayer.daum.net/) (Windows)|Yes|Yes|Yes3| |
+|[Samsung Media Player](https://play.google.com/store/apps/details?id=com.sec.android.app.music)8 (Samsung devices) |Yes|Yes|Yes|Yes|
+
+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.
+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.
+
diff --git a/Documentation/DolbyAtmos.md b/Documentation/DolbyAtmos.md
deleted file mode 100644
index c1bab378..00000000
--- a/Documentation/DolbyAtmos.md
+++ /dev/null
@@ -1,30 +0,0 @@
-## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
-
-### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
-...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
-
-
-
-## \*\*\* THIS PAGE IS A WORK IN PROGRESS. PLEASE HELP OUT BY CONTRIBUTING TO IT. \*\*\*
-
-
-
-# Dolby Atmos
-
-Dolby Atmos (Dolby Digital Plus (E-AC-3)) is a surround sound technology that creates an immersive audio experience by adding height channels to traditional surround sound setups. It's also used in 4D and spatial audio.
-
-## Downloading Dolby Atmos files
-
-Audible uses a different DRM technology on these files called "Widevine." To remove it, enable "Use widevine DRM" in Settings.
-
-## Listening to Dolby Atmos files
-
-### VLC Media Player
-
-If you save your file with Libation and play it in VLC and only get an image and no audio, the problem is most likely with the audio codecs. Specifically, with the xHE-AAC codec, as it is a modern codec and platforms such as Audible are implementing it in their most recent audiobooks, and VLC cannot process it. In these cases, one user recommended the Polport pre-decoder, which does run this more modern codec and will allow you to listen to your audiobook without any problems. There is an installable and portable version of Polport.
-
-### Samsung Music app
-
-To listen with the Samsung Music app, even if your phone does not have Dolby Atmos enabled, the Samsung Music app will read the Dolby Atmos data in the file. The only condition is that the audiobook must be in M4A format, as M4B is not supported.
-
-If you download it in M4B on your PC, simply change the extension to M4A. You will not lose any quality with that change, and you will be able to play it on the Samsung Music app on your cell phone.
\ No newline at end of file
diff --git a/Documentation/FrequentlyAskedQuestions.md b/Documentation/FrequentlyAskedQuestions.md
index d4268283..cf42b445 100644
--- a/Documentation/FrequentlyAskedQuestions.md
+++ b/Documentation/FrequentlyAskedQuestions.md
@@ -35,9 +35,15 @@ Self-hosting online:
* [audiobookshelf](https://www.audiobookshelf.org). On [reddit](https://www.reddit.com/r/audiobookshelf/)
* [plex](https://www.plex.tv/). Listen with [Prologue](https://prologue.audio/) (iOS)
-## I'm having trouble playing my book with 4D, spatial audio, or Dolby Atmos, how can I fix this?
+## Q: I'm having trouble playing my non-spatial audiobook, how can I fix this?
-**A:** Disable the 'Use widevine DRM' option in settings and download it again. [See this page for more information about this file format.](DolbyAtmos.md)
+**A:** If you enabled the [Use Widevine DRM](AudioFileFormats.md#use-widevine-drm) option in settings, the audiobook is most likely being downloaded in the [xHE-AAC codec](AudioFileFormats.md#xhe-aac) which isn't widely supported. You have two options:
+1. Use a media player which supports the xHE-AAC codec. [See an incomplete list of media players which support xHE-AAC](AudioFileFormats.md#supported-media-players).
+2. Disable [Use Widevine DRM](AudioFileFormats.md#use-widevine-drm) option in settings and re-download the audiobook. This will cause Libation to download audiobooks in the [AAC-LC codec](AudioFileFormats.md#aac-lc), which enjoys near-universal media player support.
+
+## Q: I'm having trouble playing my book with 4D, spatial audio, or Dolby Atmos, how can I fix this?
+
+**A:** Spatial audiobooks are delivered in two formats: [E-AC-3](AudioFileFormats.md#e-ac-3) and [AC-4](AudioFileFormats.md#ac-4). [See an incomplete list of media players which support those codecs](AudioFileFormats.md#supported-media-players).
## Q: I'm having trouble loggin into my Brazil account.
diff --git a/Documentation/NamingTemplates.md b/Documentation/NamingTemplates.md
index 184570ea..b364fa04 100644
--- a/Documentation/NamingTemplates.md
+++ b/Documentation/NamingTemplates.md
@@ -105,13 +105,13 @@ As an example, this folder template will place all Liberated podcasts into a "Po
## Series Formatters
|Formatter|Description|Example Usage|Example Result|
|-|-|-|-|
-|\{N \| # \| ID\}|Formats the series using the series part tags. \{N\} = Series Name \{#\} = Number order in series \{ID\} = Audible Series ID
Default is \{N\}|``````|Sherlock HolmesSherlock HolmesSherlock Holmes, 1, B08376S3R2|
+|\{N \| # \| ID\}|Formats the series using the series part tags. \{N\} = Series Name \{#\} = Number order in series \{#:[Number_Formatter](#number-formatters)\} = Number order in series, formatted \{ID\} = Audible Series ID
Default is \{N\}|````````|Sherlock HolmesSherlock HolmesSherlock Holmes, 1-6, B08376S3R2Sherlock Holmes, B08376S3R2, 01.0-06.0|
## Series List Formatters
|Formatter|Description|Example Usage|Example Result|
|-|-|-|-|
|separator()|Speficy the text used to join multiple series names.
Default is ", "|``|Sherlock Holmes; Some Other Series|
-|format(\{N \| # \| ID\})|Formats the series properties using the name series tags. See [Series Formatter Usage](#series-formatters) above.|``separator(; )]>```|Sherlock Holmes, 1; Some Other Series, 1herlock Holmes, B08376S3R2; Some Other Series, B000000000|
+|format(\{N \| # \| ID\})|Formats the series properties using the name series tags. See [Series Formatter Usage](#series-formatters) above.|``separator(; )]>```|Sherlock Holmes, 1-6; Book Collection, 1B08376S3R2-Sherlock Holmes, 01.0-06.0, B000000000-Book Collection, 01.0|
|max(#)|Only use the first # of series
Default is all series|``|Sherlock Holmes|
## Name Formatters
diff --git a/Documentation/images/AudioFormatSettings.png b/Documentation/images/AudioFormatSettings.png
new file mode 100644
index 00000000..786c6272
Binary files /dev/null and b/Documentation/images/AudioFormatSettings.png differ
diff --git a/README.md b/README.md
index 3378371a..150166d5 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@
- [Custom File Naming](Documentation/NamingTemplates.md)
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
- [Custom Theme Colors](Documentation/Advanced.md#custom-theme-colors) (Chardonnay Only)
+ - [Audio Formats (Dolby Atmos, Widevine, Spacial Audio)](Documentation/AudioFileFormats.md)
- [Docker](Documentation/Docker.md)
- [Frequently Asked Questions](Documentation/FrequentlyAskedQuestions.md)
diff --git a/Source/AudibleUtilities/AudibleUtilities.csproj b/Source/AudibleUtilities/AudibleUtilities.csproj
index 46a4a94f..48700e33 100644
--- a/Source/AudibleUtilities/AudibleUtilities.csproj
+++ b/Source/AudibleUtilities/AudibleUtilities.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/Source/DataLayer/DataLayer.csproj b/Source/DataLayer/DataLayer.csproj
index 1a990172..bef26edf 100644
--- a/Source/DataLayer/DataLayer.csproj
+++ b/Source/DataLayer/DataLayer.csproj
@@ -12,12 +12,12 @@
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Source/FileLiberator/DownloadOptions.Factory.cs b/Source/FileLiberator/DownloadOptions.Factory.cs
index af58d360..d560886c 100644
--- a/Source/FileLiberator/DownloadOptions.Factory.cs
+++ b/Source/FileLiberator/DownloadOptions.Factory.cs
@@ -18,9 +18,6 @@ namespace FileLiberator;
public partial class DownloadOptions
{
- private const string Ec3Codec = "ec+3";
- private const string Ac4Codec = "ac-4";
-
///
/// Initiate an audiobook download from the audible api.
///
@@ -71,8 +68,10 @@ public partial class DownloadOptions
token.ThrowIfCancellationRequested();
try
{
- //try to request a widevine content license using the user's spatial audio settings
- var codecChoice = config.SpatialAudioCodec is Configuration.SpatialCodec.AC_4 ? Ac4Codec : Ec3Codec;
+ //try to request a widevine content license using the user's audio settings
+ var aacCodecChoice = config.Request_xHE_AAC ? Codecs.xHE_AAC : Codecs.AAC_LC;
+ //Always use the ec+3 codec if converting to mp3
+ var spatialCodecChoice = config.SpatialAudioCodec is Configuration.SpatialCodec.AC_4 && !config.DecryptToLossy ? Codecs.AC_4 : Codecs.EC_3;
var contentLic
= await api.GetDownloadLicenseAsync(
@@ -81,7 +80,8 @@ public partial class DownloadOptions
ChapterTitlesType.Tree,
DrmType.Widevine,
config.RequestSpatial,
- codecChoice);
+ aacCodecChoice,
+ spatialCodecChoice);
if (contentLic.DrmType is not DrmType.Widevine)
return new LicenseInfo(contentLic);
diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs
index a276e6ad..578c8ff5 100644
--- a/Source/FileLiberator/DownloadOptions.cs
+++ b/Source/FileLiberator/DownloadOptions.cs
@@ -26,7 +26,7 @@ namespace FileLiberator
public string Language => LibraryBook.Book.Language;
public string? AudibleProductId => LibraryBookDto.AudibleProductId;
public string? SeriesName => LibraryBookDto.FirstSeries?.Name;
- public string? SeriesNumber => LibraryBookDto.FirstSeries?.Number;
+ public string? SeriesNumber => LibraryBookDto.FirstSeries?.Order?.ToString();
public NAudio.Lame.LameConfig? LameConfig { get; }
public string UserAgent => AudibleApi.Resources.Download_User_Agent;
public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged;
@@ -74,7 +74,7 @@ namespace FileLiberator
//If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3.
OutputFormat
= licInfo.DrmType is not AudibleApi.Common.DrmType.Adrm and not AudibleApi.Common.DrmType.Widevine ||
- (config.AllowLibationFixup && config.DecryptToLossy && licInfo.ContentMetadata.ContentReference.Codec != Ac4Codec)
+ (config.AllowLibationFixup && config.DecryptToLossy && licInfo.ContentMetadata.ContentReference.Codec != AudibleApi.Codecs.AC_4)
? OutputFormat.Mp3
: OutputFormat.M4b;
diff --git a/Source/HangoverAvalonia/HangoverAvalonia.csproj b/Source/HangoverAvalonia/HangoverAvalonia.csproj
index 9b140e6a..da86862c 100644
--- a/Source/HangoverAvalonia/HangoverAvalonia.csproj
+++ b/Source/HangoverAvalonia/HangoverAvalonia.csproj
@@ -71,12 +71,12 @@
-
-
+
+
-
-
-
+
+
+
diff --git a/Source/Libation.sln b/Source/Libation.sln
index 20a54d45..024fb96a 100644
--- a/Source/Libation.sln
+++ b/Source/Libation.sln
@@ -102,6 +102,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation UI", "Libation UI"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation CLI", "Libation CLI", "{47E27674-595D-4F7A-8CFB-127E768E1D1E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssertionHelper", "_Tests\AssertionHelper\AssertionHelper.csproj", "{CFE7A0E5-37FE-40BE-A70B-41B5104181C4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -220,6 +222,10 @@ Global
{E90C4651-AF11-41B4-A839-10082D0391F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CFE7A0E5-37FE-40BE-A70B-41B5104181C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CFE7A0E5-37FE-40BE-A70B-41B5104181C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CFE7A0E5-37FE-40BE-A70B-41B5104181C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CFE7A0E5-37FE-40BE-A70B-41B5104181C4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -258,6 +264,7 @@ Global
{FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{53758A35-1C7E-4702-9B96-433ABA457B37} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{47E27674-595D-4F7A-8CFB-127E768E1D1E} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
+ {CFE7A0E5-37FE-40BE-A70B-41B5104181C4} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
diff --git a/Source/LibationAvalonia/Controls/Settings/Audio.axaml b/Source/LibationAvalonia/Controls/Settings/Audio.axaml
index e99477ae..54d77e86 100644
--- a/Source/LibationAvalonia/Controls/Settings/Audio.axaml
+++ b/Source/LibationAvalonia/Controls/Settings/Audio.axaml
@@ -47,52 +47,57 @@
SelectedItem="{CompiledBinding FileDownloadQuality}"/>
-
-
-
+
+ IsChecked="{CompiledBinding UseWidevine, Mode=TwoWay}">
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ ColumnDefinitions="Auto,Auto"
+ VerticalAlignment="Top"
+ ToolTip.Tip="{CompiledBinding SpatialAudioCodecTip}">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
index a7f1d601..63758b69 100644
--- a/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
+++ b/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
@@ -5,6 +5,7 @@ using LibationAvalonia.ViewModels.Settings;
using LibationFileManager;
using LibationFileManager.Templates;
using LibationUiBase.Forms;
+using ReactiveUI;
using System.Linq;
using System.Threading.Tasks;
@@ -23,6 +24,15 @@ namespace LibationAvalonia.Controls.Settings
}
}
+ private void SpatialCodec_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_viewModel.SpatialAudioCodec.Value is Configuration.SpatialCodec.AC_4 && _viewModel.DecryptToLossy)
+ {
+ _viewModel.SpatialAudioCodec = _viewModel.SpatialAudioCodecs[0];
+ _viewModel.RaisePropertyChanged(nameof(AudioSettingsVM.SpatialAudioCodec));
+ }
+ }
+
private async void UseWidevine_IsCheckedChanged(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (sender is CheckBox cbox && cbox.IsChecked is true)
@@ -59,6 +69,10 @@ namespace LibationAvalonia.Controls.Settings
_viewModel.UseWidevine = false;
}
}
+ else
+ {
+ _viewModel.Request_xHE_AAC = _viewModel.RequestSpatial = false;
+ }
}
public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
diff --git a/Source/LibationAvalonia/LibationAvalonia.csproj b/Source/LibationAvalonia/LibationAvalonia.csproj
index 10d24d0c..1e5e0f7d 100644
--- a/Source/LibationAvalonia/LibationAvalonia.csproj
+++ b/Source/LibationAvalonia/LibationAvalonia.csproj
@@ -73,13 +73,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs b/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs
index 17fb37e9..ee1d5a90 100644
--- a/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs
+++ b/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs
@@ -54,7 +54,6 @@ namespace LibationAvalonia.ViewModels.Settings
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
StripUnabridged = config.StripUnabridged;
_chapterTitleTemplate = config.ChapterTitleTemplate;
- DecryptToLossy = config.DecryptToLossy;
MoveMoovToBeginning = config.MoveMoovToBeginning;
LameTargetBitrate = config.LameTargetBitrate;
LameDownsampleMono = config.LameDownsampleMono;
@@ -69,6 +68,8 @@ namespace LibationAvalonia.ViewModels.Settings
SelectedEncoderQuality = config.LameEncoderQuality;
UseWidevine = config.UseWidevine;
RequestSpatial = config.RequestSpatial;
+ Request_xHE_AAC = config.Request_xHE_AAC;
+ DecryptToLossy = config.DecryptToLossy;
}
public void SaveSettings(Configuration config)
@@ -100,6 +101,7 @@ namespace LibationAvalonia.ViewModels.Settings
config.SpatialAudioCodec = SpatialAudioCodec?.Value ?? config.SpatialAudioCodec;
config.UseWidevine = UseWidevine;
config.RequestSpatial = RequestSpatial;
+ config.Request_xHE_AAC = Request_xHE_AAC;
}
public AvaloniaList> DownloadQualities { get; } = new([
@@ -114,9 +116,10 @@ namespace LibationAvalonia.ViewModels.Settings
public string FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality));
public string UseWidevineText { get; } = Configuration.GetDescription(nameof(Configuration.UseWidevine));
public string UseWidevineTip { get; } = Configuration.GetHelpText(nameof(Configuration.UseWidevine));
+ public string Request_xHE_AACText { get; } = Configuration.GetDescription(nameof(Configuration.Request_xHE_AAC));
+ public string Request_xHE_AACTip { get; } = Configuration.GetHelpText(nameof(Configuration.Request_xHE_AAC));
public string RequestSpatialText { get; } = Configuration.GetDescription(nameof(Configuration.RequestSpatial));
public string RequestSpatialTip { get; } = Configuration.GetHelpText(nameof(Configuration.RequestSpatial));
- public string SpatialAudioCodecText { get; } = Configuration.GetDescription(nameof(Configuration.SpatialAudioCodec));
public string SpatialAudioCodecTip { get; } = Configuration.GetHelpText(nameof(Configuration.SpatialAudioCodec));
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));
public string CombineNestedChapterTitlesText { get; } = Configuration.GetDescription(nameof(Configuration.CombineNestedChapterTitles));
@@ -140,10 +143,9 @@ namespace LibationAvalonia.ViewModels.Settings
public string RetainAaxFileTip => Configuration.GetHelpText(nameof(RetainAaxFile));
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
-
- private bool _useWidevine;
- private bool _requestSpatial;
+ private bool _useWidevine, _requestSpatial, _request_xHE_AAC;
public bool UseWidevine { get => _useWidevine; set => this.RaiseAndSetIfChanged(ref _useWidevine, value); }
+ public bool Request_xHE_AAC { get => _request_xHE_AAC; set => this.RaiseAndSetIfChanged(ref _request_xHE_AAC, value); }
public bool RequestSpatial { get => _requestSpatial; set => this.RaiseAndSetIfChanged(ref _requestSpatial, value); }
public EnumDisplay FileDownloadQuality { get; set; }
@@ -155,7 +157,18 @@ namespace LibationAvalonia.ViewModels.Settings
public string StripAudibleBrandAudioTip => Configuration.GetHelpText(nameof(StripAudibleBrandAudio));
public bool StripUnabridged { get; set; }
public string StripUnabridgedTip => Configuration.GetHelpText(nameof(StripUnabridged));
- public bool DecryptToLossy { get => _decryptToLossy; set => this.RaiseAndSetIfChanged(ref _decryptToLossy, value); }
+ public bool DecryptToLossy {
+ get => _decryptToLossy;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _decryptToLossy, value);
+ if (DecryptToLossy && SpatialAudioCodec.Value is Configuration.SpatialCodec.AC_4)
+ {
+ SpatialAudioCodec = SpatialAudioCodecs[0];
+ this.RaisePropertyChanged(nameof(SpatialAudioCodec));
+ }
+ }
+ }
public string DecryptToLossyTip => Configuration.GetHelpText(nameof(DecryptToLossy));
public bool MoveMoovToBeginning { get; set; }
diff --git a/Source/LibationFileManager/Configuration.HelpText.cs b/Source/LibationFileManager/Configuration.HelpText.cs
index 52a59810..5f41c7dc 100644
--- a/Source/LibationFileManager/Configuration.HelpText.cs
+++ b/Source/LibationFileManager/Configuration.HelpText.cs
@@ -89,23 +89,26 @@ namespace LibationFileManager
AC-4 cannot be converted to MP3.
""" },
- {nameof(UseWidevine), """
+ {nameof(UseWidevine), """
Some audiobooks are only delivered in the highest
available quality with special, third-party content
- protection. Enabling this option will make Libation
- request audiobooks with Widevine DRM, which may
- yield higher quality audiobook files. If they are
- higher quality, however, they will also be encoded
- with a somewhat uncommon codec (xHE-AAC USAC)
- which you may have difficulty playing.
-
- This must be enable to download spatial audiobooks.
+ protection. Enabling this option will allows you to
+ request audiobooks in the xHE-AAC codec and in
+ spatial (Dolby Atmos) audio formats.
+ """ },
+ {nameof(Request_xHE_AAC), """
+ If selected, Libation will request audiobooks in the
+ xHE-AAC codec. This codec is generally better quality
+ than AAC-LC codec (which is what you'll get if this
+ option isn't enabled), but it isn't as commonly
+ supported by media players, so you may have some
+ difficulty playing these audiobooks.
""" },
{nameof(RequestSpatial), """
If selected, Libation will request audiobooks in the
Dolby Atmos 'Spatial Audio' format. Audiobooks which
don't have a spatial audio version will be download
- as usual based on your other file quality settings.
+ as usual based on your other audio format settings.
""" },
{"LocateAudiobooks","""
Scan the contents a folder to find audio files that
diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs
index b0e99649..392c5ecf 100644
--- a/Source/LibationFileManager/Configuration.PersistentSettings.cs
+++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs
@@ -285,9 +285,12 @@ namespace LibationFileManager
AC_4
}
- [Description("Use widevine DRM")]
+ [Description("Use Widevine DRM")]
public bool UseWidevine { get => GetNonString(defaultValue: false); set => SetNonString(value); }
+ [Description("Request xHE-AAC codec")]
+ public bool Request_xHE_AAC { get => GetNonString(defaultValue: false); set => SetNonString(value); }
+
[Description("Request Spatial Audio")]
public bool RequestSpatial { get => GetNonString(defaultValue: true); set => SetNonString(value); }
diff --git a/Source/LibationFileManager/FilePathCache.cs b/Source/LibationFileManager/FilePathCache.cs
index a6c99ef6..3cf6c528 100644
--- a/Source/LibationFileManager/FilePathCache.cs
+++ b/Source/LibationFileManager/FilePathCache.cs
@@ -1,10 +1,11 @@
-using System;
+using FileManager;
+using Newtonsoft.Json;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using FileManager;
-using Newtonsoft.Json;
+using System.Threading.Tasks;
#nullable enable
namespace LibationFileManager
@@ -32,6 +33,10 @@ namespace LibationFileManager
{
Cache = JsonConvert.DeserializeObject>(File.ReadAllText(jsonFileV2))
?? throw new NullReferenceException("File exists but deserialize is null. This will never happen when file is healthy.");
+
+ //Once per startup, launch a task to validate existence of files in the cache.
+ //This is fire-and-forget. Since it is never awaited, it will no exceptions will be thrown to the caller.
+ Task.Run(ValidateAllFiles);
}
catch (Exception ex)
{
@@ -42,6 +47,23 @@ namespace LibationFileManager
}
}
+ private static void ValidateAllFiles()
+ {
+ bool cacheChanged = false;
+ foreach (var id in Cache.GetIDs())
+ {
+ foreach (var entry in Cache.GetIdEntries(id))
+ {
+ if (!File.Exists(entry.Path))
+ {
+ cacheChanged |= Remove(entry);
+ }
+ }
+ }
+ if (cacheChanged)
+ save();
+ }
+
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
public static List<(FileType fileType, LongPath path)> GetFiles(string id)
@@ -111,10 +133,20 @@ namespace LibationFileManager
return false;
}
- public static void Insert(string id, string path)
+ public static void Insert(string id, params string[] paths)
{
- var type = FileTypes.GetFileTypeFromPath(path);
- Insert(new CacheEntry(id, type, path));
+ var newEntries
+ = paths
+ .Select(path => new CacheEntry(id, FileTypes.GetFileTypeFromPath(path), path))
+ .ToList();
+
+ lock (locker)
+ Cache.AddRange(id, newEntries);
+
+ if (Inserted is not null)
+ newEntries.ForEach(e => Inserted?.Invoke(null, e));
+
+ save();
}
public static void Insert(CacheEntry entry)
@@ -150,9 +182,11 @@ namespace LibationFileManager
private class FileCacheV2
{
[JsonProperty]
- private readonly ConcurrentDictionary> Dictionary = new();
+ private readonly ConcurrentDictionary> Dictionary = new();
private static object lockObject = new();
+ public List GetIDs() => Dictionary.Keys.ToList();
+
public List GetIdEntries(string id)
{
static List empty() => new();
@@ -162,23 +196,34 @@ namespace LibationFileManager
public void Add(string id, TEntry entry)
{
- Dictionary.AddOrUpdate(id, [entry], (id, entries) => { entries.Add(entry); return entries; });
+ Dictionary.AddOrUpdate(id,
+ (_, e) => [e], //Add new Dictionary Value
+ (id, existingEntries, newEntry) => //Update existing Dictionary Value
+ {
+ existingEntries.Add(entry);
+ return existingEntries;
+ },
+ entry);
}
public void AddRange(string id, IEnumerable entries)
{
- Dictionary.AddOrUpdate(id, entries.ToList(), (id, entries) =>
- {
- entries.AddRange(entries);
- return entries;
- });
+ Dictionary.AddOrUpdate>(id,
+ (_, e) => e.ToHashSet(), //Add new Dictionary Value
+ (id, existingEntries, newEntries) => //Update existing Dictionary Value
+ {
+ foreach (var entry in newEntries)
+ existingEntries.Add(entry);
+ return existingEntries;
+ },
+ entries);
}
public bool Remove(string id, TEntry entry)
{
lock (lockObject)
{
- if (Dictionary.TryGetValue(id, out List? entries))
+ if (Dictionary.TryGetValue(id, out HashSet? entries))
{
var removed = entries?.Remove(entry) ?? false;
if (removed && entries?.Count == 0)
diff --git a/Source/LibationFileManager/Templates/SeriesDto.cs b/Source/LibationFileManager/Templates/SeriesDto.cs
index cfd3d3fd..a2d7a867 100644
--- a/Source/LibationFileManager/Templates/SeriesDto.cs
+++ b/Source/LibationFileManager/Templates/SeriesDto.cs
@@ -1,27 +1,34 @@
using System;
+using System.Text.RegularExpressions;
#nullable enable
namespace LibationFileManager.Templates;
-public record SeriesDto : IFormattable
+public partial record SeriesDto : IFormattable
{
public string Name { get; }
- public string? Number { get; }
+ public SeriesOrder Order { get; }
public string AudibleSeriesId { get; }
public SeriesDto(string name, string? number, string audibleSeriesId)
{
Name = name;
- Number = number;
+ Order = SeriesOrder.Parse(number);
AudibleSeriesId = audibleSeriesId;
}
public override string ToString() => Name.Trim();
public string ToString(string? format, IFormatProvider? _)
=> string.IsNullOrWhiteSpace(format) ? ToString()
- : format
- .Replace("{N}", Name)
- .Replace("{#}", Number?.ToString())
- .Replace("{ID}", AudibleSeriesId)
- .Trim();
+ : FormatRegex().Replace(format, MatchEvaluator)
+ .Replace("{N}", Name)
+ .Replace("{ID}", AudibleSeriesId)
+ .Trim();
+
+ private string MatchEvaluator(Match match)
+ => Order?.ToString(match.Groups[1].Value, null) ?? "";
+
+ /// Format must have at least one of the string {N}, {#}, {ID}
+ [GeneratedRegex(@"{#(?:\:(.*?))?}")]
+ public static partial Regex FormatRegex();
}
diff --git a/Source/LibationFileManager/Templates/SeriesListFormat.cs b/Source/LibationFileManager/Templates/SeriesListFormat.cs
index 1127eaa5..9db3e441 100644
--- a/Source/LibationFileManager/Templates/SeriesListFormat.cs
+++ b/Source/LibationFileManager/Templates/SeriesListFormat.cs
@@ -12,6 +12,6 @@ internal partial class SeriesListFormat : IListFormat
: IListFormat.Join(formatString, series);
/// Format must have at least one of the string {N}, {#}, {ID}
- [GeneratedRegex(@"[Ff]ormat\((.*?(?:{[N#]}|{ID})+.*?)\)")]
+ [GeneratedRegex(@"[Ff]ormat\((.*?(?:{#(?:\:.*?)?}|{N}|{ID})+.*?)\)")]
public static partial Regex FormatRegex();
}
diff --git a/Source/LibationFileManager/Templates/SeriesOrder.cs b/Source/LibationFileManager/Templates/SeriesOrder.cs
new file mode 100644
index 00000000..ddf31a63
--- /dev/null
+++ b/Source/LibationFileManager/Templates/SeriesOrder.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+#nullable enable
+namespace LibationFileManager.Templates;
+
+public class SeriesOrder : IFormattable
+{
+ public object[] OrderParts { get; }
+ private SeriesOrder(object[] orderParts)
+ {
+ OrderParts = orderParts;
+ }
+
+ public override string ToString() => ToString(null, null);
+
+ ///
+ /// Use float formatters to format the number parts of the order.
+ ///
+ public string ToString(string? format, IFormatProvider? formatProvider)
+ => string.Concat(OrderParts.Select(p => p is float f ? f.ToString(format) : p.ToString())).Trim();
+
+ public static SeriesOrder Parse(string? order)
+ {
+ List