Merge pull request #1331 from Mbucari/master

Audio format docs, new audio format options, series order parsing.
This commit is contained in:
rmcrackan 2025-08-15 13:08:10 -04:00 committed by GitHub
commit 690fd10e42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 569 additions and 247 deletions

View File

@ -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)

View File

@ -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.
![Audio format settings menu](images/AudioFormatSettings.png)
## 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|Yes<sup>1</sup>|Yes<sup>2,3</sup>|Yes<sup>4</sup>|
|macOS Native Support|Yes|Yes|Yes<sup>3</sup>| |
|Android Native Support<sup>5</sup>|Yes|Yes| | |
|FFmpeg (all platforms)|Yes|Yes<sup>6</sup>|Yes<sup>3</sup>||
|[VLC](https://www.videolan.org/vlc/) (Windows)|Yes| |Yes<sup>3</sup> | |
|[foobar2000](https://www.foobar2000.org/components) (Windows and Mac)|Yes|Yes<sup>7</sup> | | |
|[PotPlayer](https://potplayer.daum.net/) (Windows)|Yes|Yes|Yes<sup>3</sup>| |
|[Samsung Media Player](https://play.google.com/store/apps/details?id=com.sec.android.app.music)<sup>8</sup> (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.

View File

@ -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.

View File

@ -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.

View File

@ -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<br>the series part tags.<br>\{N\} = Series Name<br>\{#\} = Number order in series<br>\{ID\} = Audible Series ID<br><br>Default is \{N\}|`<first series>`<hr>`<first series[{N}]>`<hr>`<first series[{N}, {#}, {ID}]>`|Sherlock Holmes<hr>Sherlock Holmes<hr>Sherlock Holmes, 1, B08376S3R2|
|\{N \| # \| ID\}|Formats the series using<br>the series part tags.<br>\{N\} = Series Name<br>\{#\} = Number order in series<br>\{#:[Number_Formatter](#number-formatters)\} = Number order in series, formatted<br>\{ID\} = Audible Series ID<br><br>Default is \{N\}|`<first series>`<hr>`<first series[{N}]>`<hr>`<first series[{N}, {#}, {ID}]>`<hr>`<first series[{N}, {ID}, {#:00.0}]>`|Sherlock Holmes<hr>Sherlock Holmes<hr>Sherlock Holmes, 1-6, B08376S3R2<hr>Sherlock Holmes, B08376S3R2, 01.0-06.0|
## Series List Formatters
|Formatter|Description|Example Usage|Example Result|
|-|-|-|-|
|separator()|Speficy the text used to join<br>multiple series names.<br><br>Default is ", "|`<series[separator(; )]>`|Sherlock Holmes; Some Other Series|
|format(\{N \| # \| ID\})|Formats the series properties<br>using the name series tags.<br>See [Series Formatter Usage](#series-formatters) above.|`<series[format({N}, {#})`<br>`separator(; )]>`<hr>`<author[format({L}, {ID}) separator(; )]>`|Sherlock Holmes, 1; Some Other Series, 1<hr>herlock Holmes, B08376S3R2; Some Other Series, B000000000|
|format(\{N \| # \| ID\})|Formats the series properties<br>using the name series tags.<br>See [Series Formatter Usage](#series-formatters) above.|`<series[format({N}, {#})`<br>`separator(; )]>`<hr>`<series[format({ID}-{N}, {#:00.0})]>`|Sherlock Holmes, 1-6; Book Collection, 1<hr>B08376S3R2-Sherlock Holmes, 01.0-06.0, B000000000-Book Collection, 01.0|
|max(#)|Only use the first # of series<br><br>Default is all series|`<series[max(1)]>`|Sherlock Holmes|
## Name Formatters

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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)

View File

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="AudibleApi" Version="9.4.4.1" />
<PackageReference Include="Google.Protobuf" Version="3.31.1" />
<PackageReference Include="Google.Protobuf" Version="3.32.0" />
</ItemGroup>
<ItemGroup>

View File

@ -12,12 +12,12 @@
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="9.0.2.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -18,9 +18,6 @@ namespace FileLiberator;
public partial class DownloadOptions
{
private const string Ec3Codec = "ec+3";
private const string Ac4Codec = "ac-4";
/// <summary>
/// Initiate an audiobook download from the audible api.
/// </summary>
@ -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);

View File

@ -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;

View File

@ -71,12 +71,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.2" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.2" />
<PackageReference Include="Avalonia" Version="11.3.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.3" />
<!--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.2" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.2" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.3" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />

View File

@ -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}

View File

@ -47,52 +47,57 @@
SelectedItem="{CompiledBinding FileDownloadQuality}"/>
</Grid>
<Grid ColumnDefinitions="*,*">
<Grid ColumnDefinitions="*,Auto">
<CheckBox
IsChecked="{CompiledBinding UseWidevine, Mode=TwoWay}"
ToolTip.Tip="{CompiledBinding UseWidevineTip}"
IsCheckedChanged="UseWidevine_IsCheckedChanged"
ToolTip.Tip="{CompiledBinding UseWidevineTip}">
IsChecked="{CompiledBinding UseWidevine, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding UseWidevineText}" />
</CheckBox>
<CheckBox
Grid.Column="1"
HorizontalAlignment="Right"
ToolTip.Tip="{CompiledBinding Request_xHE_AACTip}"
IsEnabled="{CompiledBinding UseWidevine}"
IsChecked="{CompiledBinding Request_xHE_AAC, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding Request_xHE_AACText}" />
</CheckBox>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<CheckBox
ToolTip.Tip="{CompiledBinding RequestSpatialTip}"
IsEnabled="{CompiledBinding UseWidevine}"
IsChecked="{CompiledBinding RequestSpatial, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding RequestSpatialText}" />
</CheckBox>
</Grid>
<Grid ColumnDefinitions="*,Auto"
ToolTip.Tip="{CompiledBinding SpatialAudioCodecTip}">
<Grid.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<MultiBinding.Bindings>
<CompiledBinding Path="UseWidevine"/>
<CompiledBinding Path="RequestSpatial"/>
</MultiBinding.Bindings>
</MultiBinding>
</Grid.IsEnabled>
<TextBlock
VerticalAlignment="Center"
Text="{CompiledBinding SpatialAudioCodecText}" />
<controls:WheelComboBox
Margin="5,0,0,0"
<Grid
Grid.Column="1"
ItemsSource="{CompiledBinding SpatialAudioCodecs}"
SelectedItem="{CompiledBinding SpatialAudioCodec}"/>
ColumnDefinitions="Auto,Auto"
VerticalAlignment="Top"
ToolTip.Tip="{CompiledBinding SpatialAudioCodecTip}">
<Grid.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<MultiBinding.Bindings>
<CompiledBinding Path="UseWidevine"/>
<CompiledBinding Path="RequestSpatial"/>
</MultiBinding.Bindings>
</MultiBinding>
</Grid.IsEnabled>
<TextBlock
VerticalAlignment="Center"
Text="Codec:"/>
<controls:WheelComboBox
Margin="5,0,0,0"
Grid.Column="1"
VerticalAlignment="Center"
SelectionChanged="SpatialCodec_SelectionChanged"
ItemsSource="{CompiledBinding SpatialAudioCodecs}"
SelectedItem="{CompiledBinding SpatialAudioCodec}"/>
</Grid>
</Grid>
<CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding CreateCueSheetText}" />

View File

@ -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)

View File

@ -73,13 +73,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.3.2" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.2" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.3.2" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.2" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.2" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.2" />
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.3.3" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.3" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.3.3" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.3" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.3" />
</ItemGroup>
<ItemGroup>

View File

@ -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<EnumDisplay<Configuration.DownloadQuality>> 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<Configuration.DownloadQuality> 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; }

View File

@ -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

View File

@ -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); }

View File

@ -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<FileCacheV2<CacheEntry>>(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<TEntry>
{
[JsonProperty]
private readonly ConcurrentDictionary<string, List<TEntry>> Dictionary = new();
private readonly ConcurrentDictionary<string, HashSet<TEntry>> Dictionary = new();
private static object lockObject = new();
public List<string> GetIDs() => Dictionary.Keys.ToList();
public List<TEntry> GetIdEntries(string id)
{
static List<TEntry> 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<TEntry>(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<TEntry> entries)
{
Dictionary.AddOrUpdate(id, entries.ToList(), (id, entries) =>
{
entries.AddRange(entries);
return entries;
});
Dictionary.AddOrUpdate<IEnumerable<TEntry>>(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<TEntry>? entries))
if (Dictionary.TryGetValue(id, out HashSet<TEntry>? entries))
{
var removed = entries?.Remove(entry) ?? false;
if (removed && entries?.Count == 0)

View File

@ -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) ?? "";
/// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary>
[GeneratedRegex(@"{#(?:\:(.*?))?}")]
public static partial Regex FormatRegex();
}

View File

@ -12,6 +12,6 @@ internal partial class SeriesListFormat : IListFormat<SeriesListFormat>
: IListFormat<SeriesListFormat>.Join(formatString, series);
/// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary>
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{[N#]}|{ID})+.*?)\)")]
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{#(?:\:.*?)?}|{N}|{ID})+.*?)\)")]
public static partial Regex FormatRegex();
}

View File

@ -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);
/// <summary>
/// Use float formatters to format the number parts of the order.
/// </summary>
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<object> parts = new();
while (TryParseNumber(order, out var value, out var range))
{
var prefix = order[..range.Start.Value];
if(!string.IsNullOrWhiteSpace(prefix))
parts.Add(prefix);
parts.Add(value);
order = order[range.End.Value..];
}
if (!string.IsNullOrWhiteSpace(order))
parts.Add(order);
return new(parts.ToArray());
}
/// <summary>
/// Try to parse any positive number from within the string (greedy).
/// </summary>
/// <param name="numString">the string to search for a numeric value</param>
/// <param name="value">If this function succeeds, the number that was found; otherwise zero.</param>
/// <param name="range">If this function succeeds, the range of characters representing <paramref name="value"/> in <paramref name="numString"/>; otherwise default</param>
/// <returns>True if a number was found; otherwise false.</returns>
private static bool TryParseNumber([NotNullWhen(true)] string? numString, out float value, out Range range)
{
value = 0;
if (string.IsNullOrWhiteSpace(numString))
{
range = default;
return false;
}
for (int s = 0; s < numString.Length; s++)
{
//Assume any valid number will begin with a digit.
//This way, leading dots and dashes will never be considered part of a number, so
//no negative series numbers and no fractional series numbers < 1 (unless preceded with a '0').
if (!char.IsDigit(numString[s]))
continue;
for (int e = numString.Length; e > s; e--)
{
//The float parser will succeed with trailing whitespace,
//but we want to preserve it in the final display string.
if (char.IsWhiteSpace(numString[e - 1]))
continue;
var substring = numString[s..e];
if (float.TryParse(substring, out value))
{
range = new Range(s, e);
return true;
}
}
}
range = default;
return false;
}
}

View File

@ -271,7 +271,7 @@ namespace LibationFileManager.Templates
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator, FormattableFormatter },
{ TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter },
{ TemplateTags.FirstSeries, lb => lb.FirstSeries, FormattableFormatter },
{ TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Number },
{ TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Order, FormattableFormatter },
{ TemplateTags.Language, lb => lb.Language },
//Don't allow formatting of LanguageShort
{ TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort },

View File

@ -25,7 +25,7 @@ namespace LibationWinForms.Dialogs
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning));
this.useWidevineCbox.Text = desc(nameof(config.UseWidevine));
this.requestSpatialCbox.Text = desc(nameof(config.RequestSpatial));
this.spatialCodecLbl.Text = desc(nameof(config.SpatialAudioCodec));
this.request_xHE_AAC_Cbox.Text = desc(nameof(config.Request_xHE_AAC));
toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles)));
toolTip.SetToolTip(allowLibationFixupCbox, Configuration.GetHelpText(nameof(config.AllowLibationFixup)));
@ -38,7 +38,7 @@ namespace LibationWinForms.Dialogs
toolTip.SetToolTip(stripAudibleBrandingCbox, Configuration.GetHelpText(nameof(config.StripAudibleBrandAudio)));
toolTip.SetToolTip(useWidevineCbox, Configuration.GetHelpText(nameof(config.UseWidevine)));
toolTip.SetToolTip(requestSpatialCbox, Configuration.GetHelpText(nameof(config.RequestSpatial)));
toolTip.SetToolTip(spatialCodecLbl, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
toolTip.SetToolTip(request_xHE_AAC_Cbox, Configuration.GetHelpText(nameof(config.Request_xHE_AAC)));
toolTip.SetToolTip(spatialAudioCodecCb, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
fileDownloadQualityCb.Items.AddRange(
@ -80,6 +80,7 @@ namespace LibationWinForms.Dialogs
fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality;
spatialAudioCodecCb.SelectedItem = config.SpatialAudioCodec;
useWidevineCbox.Checked = config.UseWidevine;
request_xHE_AAC_Cbox.Checked = config.Request_xHE_AAC;
requestSpatialCbox.Checked = config.RequestSpatial;
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
@ -124,6 +125,7 @@ namespace LibationWinForms.Dialogs
config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked;
config.FileDownloadQuality = ((EnumDisplay<Configuration.DownloadQuality>)fileDownloadQualityCb.SelectedItem).Value;
config.UseWidevine = useWidevineCbox.Checked;
config.Request_xHE_AAC = request_xHE_AAC_Cbox.Checked;
config.RequestSpatial = requestSpatialCbox.Checked;
config.SpatialAudioCodec = ((EnumDisplay<Configuration.SpatialCodec>)spatialAudioCodecCb.SelectedItem).Value;
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
@ -175,6 +177,13 @@ namespace LibationWinForms.Dialogs
{
moveMoovAtomCbox.Enabled = convertLosslessRb.Checked;
lameOptionsGb.Enabled = !convertLosslessRb.Checked;
if (convertLossyRb.Checked && requestSpatialCbox.Checked)
{
// Only E-AC-3 can be converted to mp3
spatialAudioCodecCb.SelectedIndex = 0;
}
lameTargetRb_CheckedChanged(sender, e);
LameMatchSourceBRCbox_CheckedChanged(sender, e);
}
@ -196,7 +205,18 @@ namespace LibationWinForms.Dialogs
}
}
private void spatialAudioCodecCb_SelectedIndexChanged(object sender, EventArgs e)
{
if (spatialAudioCodecCb.SelectedIndex == 1 && convertLossyRb.Checked)
{
// Only E-AC-3 can be converted to mp3
spatialAudioCodecCb.SelectedIndex = 0;
}
}
private void requestSpatialCbox_CheckedChanged(object sender, EventArgs e)
{
spatialAudioCodecCb.Enabled = requestSpatialCbox.Checked && useWidevineCbox.Checked;
}
private void useWidevineCbox_CheckedChanged(object sender, EventArgs e)
{
@ -233,9 +253,13 @@ namespace LibationWinForms.Dialogs
return;
}
}
requestSpatialCbox.Enabled = useWidevineCbox.Checked;
spatialCodecLbl.Enabled = spatialAudioCodecCb.Enabled = useWidevineCbox.Checked && requestSpatialCbox.Checked;
}
else
{
requestSpatialCbox.Checked = request_xHE_AAC_Cbox.Checked = false;
}
requestSpatialCbox.Enabled = request_xHE_AAC_Cbox.Enabled = useWidevineCbox.Checked;
requestSpatialCbox_CheckedChanged(sender, e);
}
}
}

View File

@ -84,10 +84,10 @@
folderTemplateTb = new System.Windows.Forms.TextBox();
folderTemplateLbl = new System.Windows.Forms.Label();
tab4AudioFileOptions = new System.Windows.Forms.TabPage();
request_xHE_AAC_Cbox = new System.Windows.Forms.CheckBox();
requestSpatialCbox = new System.Windows.Forms.CheckBox();
useWidevineCbox = new System.Windows.Forms.CheckBox();
spatialAudioCodecCb = new System.Windows.Forms.ComboBox();
spatialCodecLbl = new System.Windows.Forms.Label();
moveMoovAtomCbox = new System.Windows.Forms.CheckBox();
fileDownloadQualityCb = new System.Windows.Forms.ComboBox();
fileDownloadQualityLbl = new System.Windows.Forms.Label();
@ -288,7 +288,7 @@
stripAudibleBrandingCbox.Location = new System.Drawing.Point(13, 70);
stripAudibleBrandingCbox.Name = "stripAudibleBrandingCbox";
stripAudibleBrandingCbox.Size = new System.Drawing.Size(143, 34);
stripAudibleBrandingCbox.TabIndex = 14;
stripAudibleBrandingCbox.TabIndex = 16;
stripAudibleBrandingCbox.Text = "[StripAudibleBranding\r\ndesc]";
stripAudibleBrandingCbox.UseVisualStyleBackColor = true;
//
@ -298,7 +298,7 @@
splitFilesByChapterCbox.Location = new System.Drawing.Point(13, 22);
splitFilesByChapterCbox.Name = "splitFilesByChapterCbox";
splitFilesByChapterCbox.Size = new System.Drawing.Size(162, 19);
splitFilesByChapterCbox.TabIndex = 12;
splitFilesByChapterCbox.TabIndex = 14;
splitFilesByChapterCbox.Text = "[SplitFilesByChapter desc]";
splitFilesByChapterCbox.UseVisualStyleBackColor = true;
splitFilesByChapterCbox.CheckedChanged += splitFilesByChapterCbox_CheckedChanged;
@ -311,7 +311,7 @@
allowLibationFixupCbox.Location = new System.Drawing.Point(19, 230);
allowLibationFixupCbox.Name = "allowLibationFixupCbox";
allowLibationFixupCbox.Size = new System.Drawing.Size(162, 19);
allowLibationFixupCbox.TabIndex = 11;
allowLibationFixupCbox.TabIndex = 13;
allowLibationFixupCbox.Text = "[AllowLibationFixup desc]";
allowLibationFixupCbox.UseVisualStyleBackColor = true;
allowLibationFixupCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged;
@ -323,6 +323,7 @@
convertLossyRb.Name = "convertLossyRb";
convertLossyRb.Size = new System.Drawing.Size(329, 19);
convertLossyRb.TabIndex = 27;
convertLossyRb.TabStop = true;
convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)";
convertLossyRb.UseVisualStyleBackColor = true;
convertLossyRb.CheckedChanged += convertFormatRb_CheckedChanged;
@ -774,10 +775,10 @@
// tab4AudioFileOptions
//
tab4AudioFileOptions.AutoScroll = true;
tab4AudioFileOptions.Controls.Add(request_xHE_AAC_Cbox);
tab4AudioFileOptions.Controls.Add(requestSpatialCbox);
tab4AudioFileOptions.Controls.Add(useWidevineCbox);
tab4AudioFileOptions.Controls.Add(spatialAudioCodecCb);
tab4AudioFileOptions.Controls.Add(spatialCodecLbl);
tab4AudioFileOptions.Controls.Add(moveMoovAtomCbox);
tab4AudioFileOptions.Controls.Add(fileDownloadQualityCb);
tab4AudioFileOptions.Controls.Add(fileDownloadQualityLbl);
@ -802,19 +803,31 @@
tab4AudioFileOptions.Text = "Audio File Options";
tab4AudioFileOptions.UseVisualStyleBackColor = true;
//
// request_xHE_AAC_Cbox
//
request_xHE_AAC_Cbox.CheckAlign = System.Drawing.ContentAlignment.MiddleRight;
request_xHE_AAC_Cbox.Checked = true;
request_xHE_AAC_Cbox.CheckState = System.Windows.Forms.CheckState.Checked;
request_xHE_AAC_Cbox.Location = new System.Drawing.Point(239, 35);
request_xHE_AAC_Cbox.Name = "request_xHE_AAC_Cbox";
request_xHE_AAC_Cbox.Size = new System.Drawing.Size(183, 19);
request_xHE_AAC_Cbox.TabIndex = 3;
request_xHE_AAC_Cbox.Text = "[Request_xHE_AAC desc]";
request_xHE_AAC_Cbox.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
request_xHE_AAC_Cbox.UseVisualStyleBackColor = true;
//
// requestSpatialCbox
//
requestSpatialCbox.AutoSize = true;
requestSpatialCbox.CheckAlign = System.Drawing.ContentAlignment.MiddleRight;
requestSpatialCbox.Checked = true;
requestSpatialCbox.CheckState = System.Windows.Forms.CheckState.Checked;
requestSpatialCbox.Location = new System.Drawing.Point(284, 35);
requestSpatialCbox.Location = new System.Drawing.Point(19, 60);
requestSpatialCbox.Name = "requestSpatialCbox";
requestSpatialCbox.Size = new System.Drawing.Size(138, 19);
requestSpatialCbox.TabIndex = 29;
requestSpatialCbox.TabIndex = 4;
requestSpatialCbox.Text = "[RequestSpatial desc]";
requestSpatialCbox.UseVisualStyleBackColor = true;
requestSpatialCbox.CheckedChanged += useWidevineCbox_CheckedChanged;
requestSpatialCbox.CheckedChanged += requestSpatialCbox_CheckedChanged;
//
// useWidevineCbox
//
@ -824,7 +837,7 @@
useWidevineCbox.Location = new System.Drawing.Point(19, 35);
useWidevineCbox.Name = "useWidevineCbox";
useWidevineCbox.Size = new System.Drawing.Size(129, 19);
useWidevineCbox.TabIndex = 28;
useWidevineCbox.TabIndex = 2;
useWidevineCbox.Text = "[UseWidevine desc]";
useWidevineCbox.UseVisualStyleBackColor = true;
useWidevineCbox.CheckedChanged += useWidevineCbox_CheckedChanged;
@ -837,16 +850,8 @@
spatialAudioCodecCb.Margin = new System.Windows.Forms.Padding(3, 3, 5, 3);
spatialAudioCodecCb.Name = "spatialAudioCodecCb";
spatialAudioCodecCb.Size = new System.Drawing.Size(173, 23);
spatialAudioCodecCb.TabIndex = 2;
//
// spatialCodecLbl
//
spatialCodecLbl.AutoSize = true;
spatialCodecLbl.Location = new System.Drawing.Point(19, 62);
spatialCodecLbl.Name = "spatialCodecLbl";
spatialCodecLbl.Size = new System.Drawing.Size(143, 15);
spatialCodecLbl.TabIndex = 24;
spatialCodecLbl.Text = "[SpatialAudioCodec desc]";
spatialAudioCodecCb.TabIndex = 5;
spatialAudioCodecCb.SelectedIndexChanged += spatialAudioCodecCb_SelectedIndexChanged;
//
// moveMoovAtomCbox
//
@ -875,7 +880,7 @@
fileDownloadQualityLbl.Margin = new System.Windows.Forms.Padding(0, 0, 2, 0);
fileDownloadQualityLbl.Name = "fileDownloadQualityLbl";
fileDownloadQualityLbl.Size = new System.Drawing.Size(152, 15);
fileDownloadQualityLbl.TabIndex = 22;
fileDownloadQualityLbl.TabIndex = 1;
fileDownloadQualityLbl.Text = "[FileDownloadQuality desc]";
//
// combineNestedChapterTitlesCbox
@ -884,7 +889,7 @@
combineNestedChapterTitlesCbox.Location = new System.Drawing.Point(19, 206);
combineNestedChapterTitlesCbox.Name = "combineNestedChapterTitlesCbox";
combineNestedChapterTitlesCbox.Size = new System.Drawing.Size(217, 19);
combineNestedChapterTitlesCbox.TabIndex = 10;
combineNestedChapterTitlesCbox.TabIndex = 12;
combineNestedChapterTitlesCbox.Text = "[CombineNestedChapterTitles desc]";
combineNestedChapterTitlesCbox.UseVisualStyleBackColor = true;
//
@ -895,7 +900,7 @@
clipsBookmarksFormatCb.Location = new System.Drawing.Point(285, 132);
clipsBookmarksFormatCb.Name = "clipsBookmarksFormatCb";
clipsBookmarksFormatCb.Size = new System.Drawing.Size(67, 23);
clipsBookmarksFormatCb.TabIndex = 6;
clipsBookmarksFormatCb.TabIndex = 9;
//
// downloadClipsBookmarksCbox
//
@ -903,7 +908,7 @@
downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 134);
downloadClipsBookmarksCbox.Name = "downloadClipsBookmarksCbox";
downloadClipsBookmarksCbox.Size = new System.Drawing.Size(248, 19);
downloadClipsBookmarksCbox.TabIndex = 5;
downloadClipsBookmarksCbox.TabIndex = 8;
downloadClipsBookmarksCbox.Text = "Download Clips, Notes, and Bookmarks as";
downloadClipsBookmarksCbox.UseVisualStyleBackColor = true;
downloadClipsBookmarksCbox.CheckedChanged += downloadClipsBookmarksCbox_CheckedChanged;
@ -916,7 +921,7 @@
audiobookFixupsGb.Location = new System.Drawing.Point(6, 254);
audiobookFixupsGb.Name = "audiobookFixupsGb";
audiobookFixupsGb.Size = new System.Drawing.Size(416, 114);
audiobookFixupsGb.TabIndex = 19;
audiobookFixupsGb.TabIndex = 14;
audiobookFixupsGb.TabStop = false;
audiobookFixupsGb.Text = "Audiobook Fix-ups";
//
@ -926,7 +931,7 @@
stripUnabridgedCbox.Location = new System.Drawing.Point(13, 46);
stripUnabridgedCbox.Name = "stripUnabridgedCbox";
stripUnabridgedCbox.Size = new System.Drawing.Size(147, 19);
stripUnabridgedCbox.TabIndex = 13;
stripUnabridgedCbox.TabIndex = 15;
stripUnabridgedCbox.Text = "[StripUnabridged desc]";
stripUnabridgedCbox.UseVisualStyleBackColor = true;
//
@ -948,7 +953,7 @@
chapterTitleTemplateBtn.Location = new System.Drawing.Point(769, 22);
chapterTitleTemplateBtn.Name = "chapterTitleTemplateBtn";
chapterTitleTemplateBtn.Size = new System.Drawing.Size(75, 23);
chapterTitleTemplateBtn.TabIndex = 15;
chapterTitleTemplateBtn.TabIndex = 17;
chapterTitleTemplateBtn.Text = "Edit...";
chapterTitleTemplateBtn.UseVisualStyleBackColor = true;
chapterTitleTemplateBtn.Click += chapterTitleTemplateBtn_Click;
@ -960,7 +965,7 @@
chapterTitleTemplateTb.Name = "chapterTitleTemplateTb";
chapterTitleTemplateTb.ReadOnly = true;
chapterTitleTemplateTb.Size = new System.Drawing.Size(757, 23);
chapterTitleTemplateTb.TabIndex = 16;
chapterTitleTemplateTb.TabIndex = 18;
//
// lameOptionsGb
//
@ -977,7 +982,7 @@
lameOptionsGb.Location = new System.Drawing.Point(438, 78);
lameOptionsGb.Name = "lameOptionsGb";
lameOptionsGb.Size = new System.Drawing.Size(412, 304);
lameOptionsGb.TabIndex = 14;
lameOptionsGb.TabIndex = 28;
lameOptionsGb.TabStop = false;
lameOptionsGb.Text = "Mp3 Encoding Options";
//
@ -997,7 +1002,7 @@
label21.Location = new System.Drawing.Point(227, 75);
label21.Name = "label21";
label21.Size = new System.Drawing.Size(94, 15);
label21.TabIndex = 3;
label21.TabIndex = 0;
label21.Text = "Encoder Quality:";
//
// encoderQualityCb
@ -1045,7 +1050,7 @@
lameBitrateGb.Location = new System.Drawing.Point(6, 100);
lameBitrateGb.Name = "lameBitrateGb";
lameBitrateGb.Size = new System.Drawing.Size(400, 92);
lameBitrateGb.TabIndex = 0;
lameBitrateGb.TabIndex = 33;
lameBitrateGb.TabStop = false;
lameBitrateGb.Text = "Bitrate";
//
@ -1170,7 +1175,7 @@
lameQualityGb.Location = new System.Drawing.Point(6, 196);
lameQualityGb.Name = "lameQualityGb";
lameQualityGb.Size = new System.Drawing.Size(400, 85);
lameQualityGb.TabIndex = 0;
lameQualityGb.TabIndex = 36;
lameQualityGb.TabStop = false;
lameQualityGb.Text = "Quality";
//
@ -1260,7 +1265,7 @@
label13.Location = new System.Drawing.Point(355, 66);
label13.Name = "label13";
label13.Size = new System.Drawing.Size(39, 15);
label13.TabIndex = 1;
label13.TabIndex = 0;
label13.Text = "Lower";
//
// label10
@ -1269,7 +1274,7 @@
label10.Location = new System.Drawing.Point(6, 66);
label10.Name = "label10";
label10.Size = new System.Drawing.Size(43, 15);
label10.TabIndex = 1;
label10.TabIndex = 0;
label10.Text = "Higher";
//
// label14
@ -1311,7 +1316,7 @@
groupBox2.Location = new System.Drawing.Point(6, 22);
groupBox2.Name = "groupBox2";
groupBox2.Size = new System.Drawing.Size(182, 45);
groupBox2.TabIndex = 0;
groupBox2.TabIndex = 28;
groupBox2.TabStop = false;
groupBox2.Text = "Target";
//
@ -1348,7 +1353,7 @@
label1.Location = new System.Drawing.Point(6, 286);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(172, 15);
label1.TabIndex = 1;
label1.TabIndex = 0;
label1.Text = "Using L.A.M.E. encoding engine";
//
// mergeOpeningEndCreditsCbox
@ -1357,7 +1362,7 @@
mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 182);
mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox";
mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(198, 19);
mergeOpeningEndCreditsCbox.TabIndex = 9;
mergeOpeningEndCreditsCbox.TabIndex = 11;
mergeOpeningEndCreditsCbox.Text = "[MergeOpeningEndCredits desc]";
mergeOpeningEndCreditsCbox.UseVisualStyleBackColor = true;
//
@ -1367,7 +1372,7 @@
retainAaxFileCbox.Location = new System.Drawing.Point(19, 158);
retainAaxFileCbox.Name = "retainAaxFileCbox";
retainAaxFileCbox.Size = new System.Drawing.Size(131, 19);
retainAaxFileCbox.TabIndex = 8;
retainAaxFileCbox.TabIndex = 10;
retainAaxFileCbox.Text = "[RetainAaxFile desc]";
retainAaxFileCbox.UseVisualStyleBackColor = true;
retainAaxFileCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged;
@ -1380,7 +1385,7 @@
downloadCoverArtCbox.Location = new System.Drawing.Point(19, 110);
downloadCoverArtCbox.Name = "downloadCoverArtCbox";
downloadCoverArtCbox.Size = new System.Drawing.Size(162, 19);
downloadCoverArtCbox.TabIndex = 4;
downloadCoverArtCbox.TabIndex = 7;
downloadCoverArtCbox.Text = "[DownloadCoverArt desc]";
downloadCoverArtCbox.UseVisualStyleBackColor = true;
downloadCoverArtCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged;
@ -1393,7 +1398,7 @@
createCueSheetCbox.Location = new System.Drawing.Point(19, 86);
createCueSheetCbox.Name = "createCueSheetCbox";
createCueSheetCbox.Size = new System.Drawing.Size(145, 19);
createCueSheetCbox.TabIndex = 3;
createCueSheetCbox.TabIndex = 6;
createCueSheetCbox.Text = "[CreateCueSheet desc]";
createCueSheetCbox.UseVisualStyleBackColor = true;
createCueSheetCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged;
@ -1560,8 +1565,8 @@
private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.Button applyDisplaySettingsBtn;
private System.Windows.Forms.ComboBox spatialAudioCodecCb;
private System.Windows.Forms.Label spatialCodecLbl;
private System.Windows.Forms.CheckBox useWidevineCbox;
private System.Windows.Forms.CheckBox requestSpatialCbox;
private System.Windows.Forms.CheckBox request_xHE_AAC_Cbox;
}
}

View File

@ -0,0 +1,37 @@
using System.Diagnostics;
namespace AssertionHelper;
public static class AssertionExtensions
{
[StackTraceHidden]
public static T? Should<T>(this T? value) => value;
[StackTraceHidden]
public static void Be<T>(this T? value, T? expectedValue) where T : IEquatable<T>
=> Assert.AreEqual(value, expectedValue);
[StackTraceHidden]
public static void BeNull<T>(this T? value) where T : class
=> Assert.IsNull(value);
[StackTraceHidden]
public static void BeSameAs<T>(this T? value, T? otherValue)
=> Assert.AreSame(value, otherValue);
[StackTraceHidden]
public static void BeFalse(this bool value)
=> Assert.IsFalse(value);
[StackTraceHidden]
public static void BeTrue(this bool value)
=> Assert.IsTrue(value);
[StackTraceHidden]
public static void HaveCount<T>(this IEnumerable<T?> value, int expected)
=> Assert.HasCount(expected, value);
[StackTraceHidden]
public static void BeEquivalentTo<T>(this IEnumerable<T?>? value, IEnumerable<T?>? expectedValue)
=> CollectionAssert.AreEquivalent(value, expectedValue, EqualityComparer<T?>.Default);
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest.TestAdapter" Version="3.10.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.10.2" />
</ItemGroup>
</Project>

View File

@ -1,21 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AssertionHelper;
using AudibleApi;
using AudibleApi.Authorization;
using AudibleUtilities;
using Dinah.Core;
using FluentAssertions;
using FluentAssertions.Common;
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace AccountsTests
{
@ -528,7 +518,7 @@ namespace AccountsTests
var a2 = new Account("a") { AccountName = "two", IdentityTokens = idIn };
// violation: validate()
Assert.ThrowsException<InvalidOperationException>(() => accountsSettings.Add(a2));
Assert.ThrowsExactly<InvalidOperationException>(() => accountsSettings.Add(a2));
}
[TestMethod]
@ -545,7 +535,7 @@ namespace AccountsTests
accountsSettings.Add(a2);
// violation: GetAccount.SingleOrDefault
Assert.ThrowsException<InvalidOperationException>(() => a2.IdentityTokens = idIn);
Assert.ThrowsExactly<InvalidOperationException>(() => a2.IdentityTokens = idIn);
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@ -6,10 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="MSTest.TestAdapter" Version="3.10.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.10.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -18,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\..\AudibleUtilities\AudibleUtilities.csproj" />
<ProjectReference Include="..\AssertionHelper\AssertionHelper.csproj" />
</ItemGroup>
</Project>

View File

@ -1,14 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using AssertionHelper;
using AudibleApi.Common;
using Dinah.Core;
using FileManager;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FileLiberator.Tests
{
[TestClass]

View File

@ -6,10 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="MSTest.TestAdapter" Version="3.10.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.10.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -18,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\..\FileLiberator\FileLiberator.csproj" />
<ProjectReference Include="..\AssertionHelper\AssertionHelper.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@ -7,10 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="MSTest.TestAdapter" Version="3.10.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.10.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -19,6 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\..\FileManager\FileManager.csproj" />
<ProjectReference Include="..\AssertionHelper\AssertionHelper.csproj" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,6 @@
using System.Linq;
using AssertionHelper;
using FileManager.NamingTemplate;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NamingTemplateTests

View File

@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core;
using AssertionHelper;
using FileManager;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FileUtilityTests
@ -17,7 +13,7 @@ namespace FileUtilityTests
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod]
public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default));
public void null_path_throws() => Assert.ThrowsExactly<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default));
[TestMethod]
// non-empty replacement
@ -137,25 +133,25 @@ namespace FileUtilityTests
public class GetSequenceFormatted
{
[TestMethod]
public void negative_partsPosition() => Assert.ThrowsException<ArgumentException>(()
public void negative_partsPosition() => Assert.ThrowsExactly<ArgumentException>(()
=> FileUtility.GetSequenceFormatted(-1, 2)
);
[TestMethod]
public void zero_partsPosition() => Assert.ThrowsException<ArgumentException>(()
public void zero_partsPosition() => Assert.ThrowsExactly<ArgumentException>(()
=> FileUtility.GetSequenceFormatted(0, 2)
);
[TestMethod]
public void negative_partsTotal() => Assert.ThrowsException<ArgumentException>(()
public void negative_partsTotal() => Assert.ThrowsExactly<ArgumentException>(()
=> FileUtility.GetSequenceFormatted(2, -1)
);
[TestMethod]
public void zero_partsTotal() => Assert.ThrowsException<ArgumentException>(()
public void zero_partsTotal() => Assert.ThrowsExactly<ArgumentException>(()
=> FileUtility.GetSequenceFormatted(2, 0)
);
[TestMethod]
public void partsPosition_greater_than_partsTotal() => Assert.ThrowsException<ArgumentException>(()
public void partsPosition_greater_than_partsTotal() => Assert.ThrowsExactly<ArgumentException>(()
=> FileUtility.GetSequenceFormatted(2, 1)
);

View File

@ -7,10 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="MSTest.TestAdapter" Version="3.10.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.10.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -19,6 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\..\LibationFileManager\LibationFileManager.csproj" />
<ProjectReference Include="..\AssertionHelper\AssertionHelper.csproj" />
</ItemGroup>
</Project>

View File

@ -2,10 +2,9 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core;
using AssertionHelper;
using FileManager;
using FileManager.NamingTemplate;
using FluentAssertions;
using LibationFileManager.Templates;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -388,6 +387,7 @@ namespace TemplatesTests
[DataRow("<first series>", "Series A")]
[DataRow("<first series[]>", "Series A")]
[DataRow("<first series[{N}, {#}, {ID}]>", "Series A, 1, B1")]
[DataRow("<first series[{N}, {#:00.0}]>", "Series A, 01.0")]
public void SeriesFormat_formatters(string template, string expected)
{
var bookDto = GetLibraryBook();
@ -406,6 +406,30 @@ namespace TemplatesTests
.Should().Be(expected);
}
[TestMethod]
[DataRow("<first series[{#}]>", "1-6", "1-6")]
[DataRow("<series[format({#:F2})]>", "1-6", "1.00-6.00")]
[DataRow("<first series[{#:F2}]>", "1-6", "1.00-6.00")]
[DataRow("<series#[F2]>", "1-6", "1.00-6.00")]
[DataRow("<series#[F2]>", "front 1-6 back", "front 1.00-6.00 back")]
[DataRow("<series#[F2]>", "front 1 - 6 back", "front 1.00 - 6.00 back")]
[DataRow("<series#[F2]>", "f.1", "f.1.00")]
[DataRow("<series#[F2]>", "f1g", "f1.00g")]
[DataRow("<series#[F2]>", " f1g ", "f1.00g")]
[DataRow("<series#[]>", "1", "1")]
[DataRow("<series#>", "1", "1")]
public void SeriesOrder_formatters(string template, string seriesOrder, string expected)
{
var bookDto = GetLibraryBook();
bookDto.Series = [new("Series A", seriesOrder, "B1")];
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
fileTemplate
.GetFilename(bookDto, "", "", Replacements)
.PathWithoutPrefix
.Should().Be(expected);
}
[TestMethod]
[DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)]
[DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]
@ -496,7 +520,7 @@ namespace Templates_Other
var sb = new System.Text.StringBuilder();
sb.Append('0', 300);
var longText = sb.ToString();
Assert.ThrowsException<PathTooLongException>(() => NEW_GetValidFilename_FileNamingTemplate(baseDir, template, "my: book " + longText, "txt"));
Assert.ThrowsExactly<PathTooLongException>(() => NEW_GetValidFilename_FileNamingTemplate(baseDir, template, "my: book " + longText, "txt"));
}
private class TemplateTag : ITemplateTag

View File

@ -7,10 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="MSTest.TestAdapter" Version="3.10.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.10.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -19,6 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\..\LibationSearchEngine\LibationSearchEngine.csproj" />
<ProjectReference Include="..\AssertionHelper\AssertionHelper.csproj" />
</ItemGroup>
</Project>

View File

@ -1,20 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Dinah.Core;
using FluentAssertions;
using FluentAssertions.Common;
using AssertionHelper;
using LibationSearchEngine;
using Lucene.Net.Analysis.Standard;
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace SearchEngineTests
{