Compare commits

...

39 Commits

Author SHA1 Message Date
Mbucari
74c76a7414
Enhance bug report template formatting and instructions
Updated the bug report template to improve clarity and formatting, including default log file locations for various platforms.
2025-09-04 12:46:22 -06:00
Mbucari
17a0c21453
Document xHE-AAC conformance errors for Audible
Added notes on xHE-AAC conformance errors related to Audible files.
2025-09-04 12:24:46 -06:00
rmcrackan
fc9c9dfe48
Merge pull request #1360 from rmcrackan/dependabot/github_actions/actions/setup-dotnet-5
Bump actions/setup-dotnet from 4 to 5
2025-09-03 21:43:10 -04:00
dependabot[bot]
d5f0e39981
Bump actions/setup-dotnet from 4 to 5
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 00:05:35 +00:00
rmcrackan
0f6493f4af
Merge pull request #1353 from rmcrackan/rmcrackan-patch-2
Update NamingTemplates.md
2025-08-27 09:30:09 -04:00
rmcrackan
454b490a06
Update NamingTemplates.md 2025-08-27 09:28:45 -04:00
rmcrackan
ffea2648aa
Merge pull request #1352 from rmcrackan/rmcrackan-patch-1
Update NamingTemplates.md
2025-08-27 09:22:20 -04:00
rmcrackan
1ac967500c
Update NamingTemplates.md
Better documentation for inverted tags
2025-08-27 08:57:20 -04:00
rmcrackan
ed5afe5d0f update dependencies 2025-08-20 12:07:53 -04:00
rmcrackan
ab075d0bef
Merge pull request #1343 from MajorTanya/patch-1
Place examples in their own line
2025-08-20 11:49:46 -04:00
MajorTanya
7fb1adb41b
Place examples in their own line
Markdown collapses single line breaks, so this change makes it so the examples will have their own lines.
2025-08-20 17:26:48 +02:00
rmcrackan
9735a8391c incr ver 2025-08-20 08:39:10 -04:00
rmcrackan
dbdfdbc536
Merge pull request #1342 from Mbucari/master
Added new <has PROPERTY-><-has> conditional tag
2025-08-20 08:37:11 -04:00
Michael Bucari-Tovo
3b86fc405f Add <has-> template tag 2025-08-19 18:41:31 -06:00
MBucari
4ea7f04921 Preserve space between series order numbers 2025-08-17 13:40:37 -06:00
MBucari
5b59b442ab Add last downloaded sample rate, bitrate and codec name to search engine. 2025-08-17 13:07:24 -06:00
rmcrackan
b5d9c0a27a Incr ver 2025-08-17 10:06:12 -04:00
rmcrackan
f5cbf89e13
Merge pull request #1337 from Mbucari/master
Fix linux crpto and series order parsing
2025-08-17 09:57:43 -04:00
rmcrackan
00dc9e020d
Update bug_report.md 2025-08-17 09:55:26 -04:00
MBucari
bfa0e4d338 Parse floats with invariant culture 2025-08-16 16:39:36 -06:00
Mbucari
5ceda408da Use managed RSASSA-PSS with SHA-1
OpenSSL (the underlying RSA implementation on Linux) has deprecated SHA-1 signing. Used a managed implementation so that it does not error.
2025-08-16 16:28:33 -06:00
Mbucari
716b1923a4
Update FrequentlyAskedQuestions.md 2025-08-15 12:25:03 -06:00
rmcrackan
1148d8125d incr ver 2025-08-15 13:10:05 -04:00
rmcrackan
690fd10e42
Merge pull request #1331 from Mbucari/master
Audio format docs, new audio format options, series order parsing.
2025-08-15 13:08:10 -04:00
Michael Bucari-Tovo
736fbbf82f Improve FilePathCache performance 2025-08-15 10:50:37 -06:00
Michael Bucari-Tovo
eda100b7ac Remove FluentAssertions 2025-08-15 10:29:23 -06:00
Michael Bucari-Tovo
ceb007500d Update assertions to use ThrowsExactly 2025-08-14 15:58:01 -06:00
Michael Bucari-Tovo
05fad01624 Update dependencies 2025-08-14 15:57:35 -06:00
Michael Bucari-Tovo
e1d789ccdc Improve series order parsing and formatting 2025-08-14 15:37:53 -06:00
Michael Bucari-Tovo
d0f00f3f1e Add xHE-AAC option 2025-08-14 13:20:01 -06:00
Michael Bucari-Tovo
6ab82dba7b Add audio format info to wiki 2025-08-14 13:16:27 -06:00
rmcrackan
0045202334 update dependencies 2025-08-13 07:52:13 -04:00
rmcrackan
4c80813651
Merge pull request #1330 from rmcrackan/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-08-11 17:38:40 -04:00
dependabot[bot]
6b637b35ab
Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 20:35:21 +00:00
rmcrackan
9b55ffa715
Merge pull request #1329 from rmcrackan/rmcrackan/dolby-atmos
Begin documentation for Dolby Atmos
2025-08-11 09:22:03 -04:00
rmcrackan
65da7890f1 Begin documentation for Dolby Atmos 2025-08-11 09:21:03 -04:00
rmcrackan
72f92ec6c0 update dependencies 2025-08-10 11:21:34 -04:00
rmcrackan
4efc084375
Merge pull request #1324 from rmcrackan/dependabot/github_actions/actions/download-artifact-5
Bump actions/download-artifact from 4 to 5
2025-08-06 10:24:33 -04:00
dependabot[bot]
f955daa5ed
Bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-06 14:09:36 +00:00
61 changed files with 993 additions and 274 deletions

View File

@ -6,10 +6,14 @@ labels: bug
assignees: '' assignees: ''
--- ---
**Describe the bug** PLEASE FILL OUT THE FOLLOWING. Bug reports with limited information or lacking an attached log file may get limited or delayed help.
___
## Describe the bug
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
**To Reproduce** ## To Reproduce
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
@ -17,14 +21,23 @@ Steps to reproduce the behavior:
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See error
**Expected behavior** ## Expected behavior
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
**Screenshots** ## Screenshots
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Platform** ## Platform
[e.g. Windows 10, Windows 11, Mac, Linux (State distribution)] [e.g. Windows 10, Windows 11, Mac, Linux (State distribution)]
**Log Files** ## Log Files
Attach your Libation log file here. Logs are typically in your `[user]\Libation` folder. (For example, on windows: `C:\my_username\Libation`) Also within Libation, on the first tab in Settings you can click the button 'Open log folder'. If your user folder contains the file "LibationCrash.log", attach that also. Attach your Libation log file here. If your user folder contains the file "LibationCrash.log", attach that also.
**Default Log File Locations**
|Platform|Folder|
|-|-|
|Windows|`%userprofile%\Libation`|
|macOS|`~/Library/Application Support/Libation`|
|Linux|`~/.local/share/Libation`|
Alternative, you may open the log file folder from within Libation. Open Libation's settings, and on the first tab in Settings you can click the button 'Open log folder'.

View File

@ -41,9 +41,9 @@ jobs:
name: "${{ inputs.OS }}-${{ inputs.architecture }}" name: "${{ inputs.OS }}-${{ inputs.architecture }}"
runs-on: ${{ inputs.runs_on }} runs-on: ${{ inputs.runs_on }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: ${{ env.DOTNET_VERSION }} dotnet-version: ${{ env.DOTNET_VERSION }}
env: env:

View File

@ -42,9 +42,9 @@ jobs:
release_name: classic release_name: classic
prefix: Classic- prefix: Classic-
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: ${{ env.DOTNET_VERSION }} dotnet-version: ${{ env.DOTNET_VERSION }}
env: env:

View File

@ -26,7 +26,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3

View File

@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
path: artifacts path: artifacts
pattern: "*(Classic-)Libation.*" pattern: "*(Classic-)Libation.*"

View File

@ -17,6 +17,6 @@ jobs:
container: container:
image: ghcr.io/flathub/flatpak-builder-lint:latest image: ghcr.io/flathub/flatpak-builder-lint:latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Check the MetaInfo file - name: Check the MetaInfo file
run: flatpak-builder-lint appstream Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml run: flatpak-builder-lint appstream Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml

View File

@ -15,7 +15,7 @@ jobs:
validate-desktop-file: validate-desktop-file:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- run: sudo apt --yes install desktop-file-utils - run: sudo apt --yes install desktop-file-utils
- name: Check the desktop file - name: Check the desktop file
run: desktop-file-validate Source/LoadByOS/LinuxConfigApp/Libation.desktop run: desktop-file-validate Source/LoadByOS/LinuxConfigApp/Libation.desktop

View File

@ -12,6 +12,7 @@
- [Custom File Naming](NamingTemplates.md) - [Custom File Naming](NamingTemplates.md)
- [Command Line Interface](#command-line-interface) - [Command Line Interface](#command-line-interface)
- [Custom Theme Colors](#custom-theme-colors) (Chardonnay Only) - [Custom Theme Colors](#custom-theme-colors) (Chardonnay Only)
- [Audio Formats (Dolby Atmos, Widevine, Spacial Audio)](AudioFileFormats.md)

View File

@ -0,0 +1,104 @@
# 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)
Note that the xHE-AAC files authored by Audible have some USAC conformance errors including:
- Number of samples per frame not matching the UsacConfig coreCoderFrameLength value.
- Disagreement between stts and UsacFrame usacIndependencyFlag value.
- Stts indicating a frame is an immediate play-out frame, but USAC AudioPreRoll is absent.
## Dolby Atmos
Atmos is a surround sound technology that expands on existing surround sound systems by adding height channels as well as free-moving sound objects. Audible delivers Dolby Atmos in two formats: E-AC-3 and AC-4.
Your device's ability to play audio from these formats does not necessarily mean that the audio you are hearing is Atmos (spatial). For instance, downloading the AC-4 codec for Windows ([links in the [Supported media Players](#supported-media-players) section) will enable you to play AC-4 audiobooks, but you'll still need to download [Dolby Access](https://apps.microsoft.com/detail/9n0866fs04w8?hl=en-US&gl=US) and pay $15 to enable _Dolby Atmos For Headphones_. Please refer to [this comment](https://github.com/rmcrackan/Libation/pull/1331#discussion_r2268660524) for additional context.
### E-AC-3
#### _Full Name_
Dolby Digital Plus (a.k.a Enhanced AC-3, DDP, DD+, and EC-3)
#### _Description_
A proprietary digital audio compression scheme developed by Dolby Digital for the transport and storage of multichannel audio. This format can be extended to add support for Atmos, making the codec _Dolby Digital Plus Atmos_. _Dolby Digital Plus Atmos_ is backwards compatible with Dolby Digital Plus, so any media player capable of playing Dolby Digital Plus can play _Dolby Digital Plus Atmos_. Audible spatial audiobooks downloaded in the E-AC-3 format are _Dolby Digital Plus Atmos_. If they are played by a media player that supports Atmos, they will play as Atmos audio. If they are played by a media player that does not support Atmos, they will be played as traditional 5.1 surround audio.
### AC-4
#### _Full Name_
Dolby AC-4
#### _Description_
A proprietary audio compression technology developed by Dolby Digital for the transport and storage of audio channels and/or audio objects. Audible spatial audiobooks downloaded in the AC-4 format are 2-channel AC-4 Immersive Stereo (AC4-IMS) audio, intended for playback in headphones or earbuds (though apparently [not supported on Apple devices](https://github.com/rmcrackan/Libation/issues/996#issuecomment-3169574514)).
# Supported Media Players
Below is an incomplete matrix of codec support across various media players and platforms.
| 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

@ -35,9 +35,19 @@ Self-hosting online:
* [audiobookshelf](https://www.audiobookshelf.org). On [reddit](https://www.reddit.com/r/audiobookshelf/) * [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) * [plex](https://www.plex.tv/). Listen with [Prologue](https://prologue.audio/) (iOS)
## Q: I'm having trouble playing my non-spatial audiobook, how can I fix this?
**A:** If you enabled the [Request xHE-AAC Codec](AudioFileFormats.md#request-xhe-aac-codec) option in settings, then the audiobook is 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 the [Request xHE-AAC Codec](AudioFileFormats.md#request-xhe-aac-codec) option in settings and re-download the audiobook. This will cause Libation to download audiobooks in the [AAC-LC codec](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. ## Q: I'm having trouble loggin into my Brazil account.
For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. The external browser login option is not possible for Brazil. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103) **A:** For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. The external browser login option is not possible for Brazil. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
## Q: How do I use Libation with a South Africa account? ## Q: How do I use Libation with a South Africa account?

View File

@ -81,17 +81,39 @@ Anything between the opening tag (`<tagname->`) and closing tag (`<-tagname>`) w
|\<if podcast-\>...\<-if podcast\>|Only include if part of a podcast|Conditional| |\<if podcast-\>...\<-if podcast\>|Only include if part of a podcast|Conditional|
|\<if bookseries-\>...\<-if bookseries\>|Only include if part of a book series|Conditional| |\<if bookseries-\>...\<-if bookseries\>|Only include if part of a book series|Conditional|
|\<if podcastparent-\>...\<-if podcastparent\>**†**|Only include if item is a podcast series parent|Conditional| |\<if podcastparent-\>...\<-if podcastparent\>**†**|Only include if item is a podcast series parent|Conditional|
|\<has PROPERTY-\>...\<-has\>|Only include if the PROPERTY has a value (i.e. not null or empty)|Conditional|
**†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked. **†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked.
For example, <if podcast-\>\<series\>\<-if podcast\> will evaluate to the podcast's series name if the file is a podcast. For audiobooks that are not podcasts, that tag will be blank. For example, `<if podcast-><series><-if podcast>` will evaluate to the podcast's series name if the file is a podcast. For audiobooks that are not podcasts, that tag will be blank.
You can invert the condition (instead of displaying the text when the condition is true, display the text when it is false) by playing a '!' symbol before the opening tag name. You can invert the condition (instead of displaying the text when the condition is true, display the text when it is false) by playing a `!` symbol before the opening tag name.
|Inverted Tag|Description|Type|
|-|-|-|
|\<!if series-\>...\<-if series\>|Only include if *not* part of a book series or podcast|Conditional|
|\<!if podcast-\>...\<-if podcast\>|Only include if *not* part of a podcast|Conditional|
|\<!if bookseries-\>...\<-if bookseries\>|Only include if *not* part of a book series|Conditional|
|\<!if podcastparent-\>...\<-if podcastparent\>**†**|Only include if item is *not* a podcast series parent|Conditional|
|\<!has PROPERTY-\>...\<-has\>|Only include if the PROPERTY *does not* have a value (i.e. is null or empty)|Conditional|
**†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked.
As an example, this folder template will place all Liberated podcasts into a "Podcasts" folder and all liberated books (not podcasts) into a "Books" folder. As an example, this folder template will place all Liberated podcasts into a "Podcasts" folder and all liberated books (not podcasts) into a "Books" folder.
\<if podcast-\>Podcasts<-if podcast\>\<!if podcast-\>Books\<-if podcast\>\\\<title\> `<if podcast->Podcasts<-if podcast><!if podcast->Books<-if podcast>\<title>`
This example will add a number if the `<series#\>` tag has a value:
`<has series#><series#><-has>`
This example will put non-series books in a "Standalones" folder:
`<!if series->Standalones/<-if series>`
And this example will customize the title based on whether the book has a subtitle:
`<audible title><has audible subtitle->-<audible subtitle><-has>`
# Tag Formatters # Tag Formatters
**Text**, **Name List**, **Number**, and **DateTime** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type. **Text**, **Name List**, **Number**, and **DateTime** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type.
@ -105,13 +127,13 @@ As an example, this folder template will place all Liberated podcasts into a "Po
## Series Formatters ## Series Formatters
|Formatter|Description|Example Usage|Example Result| |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 ## Series List Formatters
|Formatter|Description|Example Usage|Example Result| |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| |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| |max(#)|Only use the first # of series<br><br>Default is all series|`<series[max(1)]>`|Sherlock Holmes|
## Name Formatters ## Name Formatters

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -34,6 +34,7 @@
- [Custom File Naming](Documentation/NamingTemplates.md) - [Custom File Naming](Documentation/NamingTemplates.md)
- [Command Line Interface](Documentation/Advanced.md#command-line-interface) - [Command Line Interface](Documentation/Advanced.md#command-line-interface)
- [Custom Theme Colors](Documentation/Advanced.md#custom-theme-colors) (Chardonnay Only) - [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) - [Docker](Documentation/Docker.md)
- [Frequently Asked Questions](Documentation/FrequentlyAskedQuestions.md) - [Frequently Asked Questions](Documentation/FrequentlyAskedQuestions.md)

View File

@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>12.5.0.1</Version> <Version>12.5.3.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Octokit" Version="14.0.0" /> <PackageReference Include="Octokit" Version="14.0.0" />

View File

@ -5,8 +5,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AudibleApi" Version="9.4.2.1" /> <PackageReference Include="AudibleApi" Version="9.4.5.1" />
<PackageReference Include="Google.Protobuf" Version="3.31.1" /> <PackageReference Include="Google.Protobuf" Version="3.32.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Numerics;
using System.Security.Cryptography; using System.Security.Cryptography;
#nullable enable #nullable enable
@ -56,18 +57,99 @@ internal class Device
public byte[] SignMessage(byte[] message) public byte[] SignMessage(byte[] message)
{ {
using var sha1 = SHA1.Create(); var digestion = SHA1.HashData(message);
var digestion = sha1.ComputeHash(message); return PssSha1Signer.SignHash(CdmKey, digestion);
return CdmKey.SignHash(digestion, HashAlgorithmName.SHA1, RSASignaturePadding.Pss);
} }
public bool VerifyMessage(byte[] message, byte[] signature) public bool VerifyMessage(byte[] message, byte[] signature)
{ {
using var sha1 = SHA1.Create(); var digestion = SHA1.HashData(message);
var digestion = sha1.ComputeHash(message);
return CdmKey.VerifyHash(digestion, signature, HashAlgorithmName.SHA1, RSASignaturePadding.Pss); return CdmKey.VerifyHash(digestion, signature, HashAlgorithmName.SHA1, RSASignaturePadding.Pss);
} }
public byte[] DecryptSessionKey(byte[] sessionKey) public byte[] DecryptSessionKey(byte[] sessionKey)
=> CdmKey.Decrypt(sessionKey, RSAEncryptionPadding.OaepSHA1); => CdmKey.Decrypt(sessionKey, RSAEncryptionPadding.OaepSHA1);
/// <summary>
/// Completely managed implementation of RSASSA-PSS using SHA-1.
/// https://github.com/bcgit/bc-csharp/blob/master/crypto/src/crypto/signers/PssSigner.cs
///
/// Absolutely nobody anywhere should use this RSASSA-PSS implementation in anything where they care about security at all. We completely skipped the random salt part of it because libation doesn't need security; it only needs to satisfy Audible server's challenge-response requirements.
/// </summary>
private static class PssSha1Signer
{
private const int Sha1DigestSize = 20;
private const int Trailer = 0xBC;
public static byte[] SignHash(RSA rsa, ReadOnlySpan<byte> hash)
{
ArgumentOutOfRangeException.ThrowIfNotEqual(hash.Length, Sha1DigestSize);
var parameters = rsa.ExportParameters(true);
var Modulus = new BigInteger(parameters.Modulus, isUnsigned: true, isBigEndian: true);
var Exponent = new BigInteger(parameters.D, isUnsigned: true, isBigEndian: true);
var emBits = rsa.KeySize - 1;
var block = new byte[(emBits + 7) / 8];
var firstByteMask = (byte)(0xFFU >> ((block.Length * 8) - emBits));
Span<byte> mDash = new byte[8 + 2 * Sha1DigestSize];
hash.CopyTo(mDash.Slice(8));
var h = SHA1.HashData(mDash);
block[^(2 * (Sha1DigestSize + 1))] = 1;
byte[] dbMask = MaskGeneratorFunction1(h, 0, h.Length, block.Length - Sha1DigestSize - 1);
for (int i = 0; i != dbMask.Length; i++)
block[i] ^= dbMask[i];
h.CopyTo(block, block.Length - Sha1DigestSize - 1);
block[0] &= firstByteMask;
block[^1] = Trailer;
var input = new BigInteger(block, isUnsigned: true, isBigEndian: true);
var result = BigInteger.ModPow(input, Exponent, Modulus);
return result.ToByteArray(isUnsigned: true, isBigEndian: true);
}
private static byte[] MaskGeneratorFunction1(byte[] Z, int zOff, int zLen, int length)
{
byte[] mask = new byte[length];
byte[] hashBuf = new byte[Sha1DigestSize];
byte[] C = new byte[4];
int counter = 0;
using var sha = SHA1.Create();
for (; counter < (length / Sha1DigestSize); counter++)
{
ItoOSP(counter, C);
sha.TransformBlock(Z, zOff, zLen, null, 0);
sha.TransformFinalBlock(C, 0, C.Length);
sha.Hash!.CopyTo(mask, counter * Sha1DigestSize);
}
if ((counter * Sha1DigestSize) < length)
{
ItoOSP(counter, C);
sha.TransformBlock(Z, zOff, zLen, null, 0);
sha.TransformFinalBlock(C, 0, C.Length);
Array.Copy(sha.Hash!, 0, mask, counter * Sha1DigestSize, mask.Length - (counter * Sha1DigestSize));
}
return mask;
}
private static void ItoOSP(int i, byte[] sp)
{
sp[0] = (byte)((uint)i >> 24);
sp[1] = (byte)((uint)i >> 16);
sp[2] = (byte)((uint)i >> 8);
sp[3] = (byte)((uint)i >> 0);
}
}
} }

View File

@ -10,14 +10,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dinah.Core" Version="9.0.1.1" /> <PackageReference Include="Dinah.Core" Version="9.0.3.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -18,9 +18,6 @@ namespace FileLiberator;
public partial class DownloadOptions public partial class DownloadOptions
{ {
private const string Ec3Codec = "ec+3";
private const string Ac4Codec = "ac-4";
/// <summary> /// <summary>
/// Initiate an audiobook download from the audible api. /// Initiate an audiobook download from the audible api.
/// </summary> /// </summary>
@ -71,8 +68,10 @@ public partial class DownloadOptions
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
try try
{ {
//try to request a widevine content license using the user's spatial audio settings //try to request a widevine content license using the user's audio settings
var codecChoice = config.SpatialAudioCodec is Configuration.SpatialCodec.AC_4 ? Ac4Codec : Ec3Codec; 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 var contentLic
= await api.GetDownloadLicenseAsync( = await api.GetDownloadLicenseAsync(
@ -81,7 +80,8 @@ public partial class DownloadOptions
ChapterTitlesType.Tree, ChapterTitlesType.Tree,
DrmType.Widevine, DrmType.Widevine,
config.RequestSpatial, config.RequestSpatial,
codecChoice); aacCodecChoice,
spatialCodecChoice);
if (contentLic.DrmType is not DrmType.Widevine) if (contentLic.DrmType is not DrmType.Widevine)
return new LicenseInfo(contentLic); return new LicenseInfo(contentLic);

View File

@ -26,7 +26,7 @@ namespace FileLiberator
public string Language => LibraryBook.Book.Language; public string Language => LibraryBook.Book.Language;
public string? AudibleProductId => LibraryBookDto.AudibleProductId; public string? AudibleProductId => LibraryBookDto.AudibleProductId;
public string? SeriesName => LibraryBookDto.FirstSeries?.Name; 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 NAudio.Lame.LameConfig? LameConfig { get; }
public string UserAgent => AudibleApi.Resources.Download_User_Agent; public string UserAgent => AudibleApi.Resources.Download_User_Agent;
public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged; 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. //If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3.
OutputFormat OutputFormat
= licInfo.DrmType is not AudibleApi.Common.DrmType.Adrm and not AudibleApi.Common.DrmType.Widevine || = 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.Mp3
: OutputFormat.M4b; : OutputFormat.M4b;

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dinah.Core" Version="9.0.1.1" /> <PackageReference Include="Dinah.Core" Version="9.0.3.1" />
<PackageReference Include="Polly" Version="8.6.2" /> <PackageReference Include="Polly" Version="8.6.2" />
</ItemGroup> </ItemGroup>

View File

@ -22,6 +22,8 @@ internal interface IClosingPropertyTag : IPropertyTag
bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag); bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag);
} }
public delegate bool Conditional<T>(ITemplateTag templateTag, T value, string condition);
public class ConditionalTagCollection<TClass> : TagCollection public class ConditionalTagCollection<TClass> : TagCollection
{ {
public ConditionalTagCollection(bool caseSensative = true) :base(typeof(TClass), caseSensative) { } public ConditionalTagCollection(bool caseSensative = true) :base(typeof(TClass), caseSensative) { }
@ -32,21 +34,49 @@ public class ConditionalTagCollection<TClass> : TagCollection
/// <param name="propertyGetter">A Func to get the condition's <see cref="bool"/> value from <see cref="TClass"/></param> /// <param name="propertyGetter">A Func to get the condition's <see cref="bool"/> value from <see cref="TClass"/></param>
public void Add(ITemplateTag templateTag, Func<TClass, bool> propertyGetter) public void Add(ITemplateTag templateTag, Func<TClass, bool> propertyGetter)
{ {
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); var target = propertyGetter.Target is null ? null : Expression.Constant(propertyGetter.Target);
var expr = Expression.Call(target, propertyGetter.Method, Parameter);
AddPropertyTag(new ConditionalTag(templateTag, Options, expr)); AddPropertyTag(new ConditionalTag(templateTag, Options, expr));
} }
/// <summary>
/// Register a conditional tag.
/// </summary>
/// <param name="conditional">A <see cref="Conditional{TClass}"/> to get the condition's <see cref="bool"/> value</param>
public void Add(ITemplateTag templateTag, Conditional<TClass> conditional)
{
AddPropertyTag(new ConditionalTag(templateTag, Options, Parameter, conditional));
}
private class ConditionalTag : TagBase, IClosingPropertyTag private class ConditionalTag : TagBase, IClosingPropertyTag
{ {
public override Regex NameMatcher { get; } public override Regex NameMatcher { get; }
public Regex NameCloseMatcher { get; } public Regex NameCloseMatcher { get; }
private Func<string?, Expression> CreateConditionExpression { get; }
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression) public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression)
: base(templateTag, conditionExpression) : base(templateTag, conditionExpression)
{ {
NameMatcher = new Regex($"^<(!)?{templateTag.TagName}->", options); NameMatcher = new Regex(@$"^<(!)?{templateTag.TagName}->", options);
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options); NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
CreateConditionExpression = _ => conditionExpression;
}
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, ParameterExpression parameter, Conditional<TClass> conditional)
: base(templateTag, Expression.Constant(false))
{
NameMatcher = new Regex(@$"^<(!)?{templateTag.TagName}(?:\s+?(.*?)\s*?)?->", options);
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
var target = conditional.Target is null ? null : Expression.Constant(conditional.Target);
CreateConditionExpression = condition
=> Expression.Call(
conditional.Target is null ? null : Expression.Constant(conditional.Target),
conditional.Method,
Expression.Constant(templateTag),
parameter,
Expression.Constant(condition));
} }
public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag) public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag)
@ -64,6 +94,13 @@ public class ConditionalTagCollection<TClass> : TagCollection
return false; return false;
} }
protected override Expression GetTagExpression(string exactName, string formatter) => formatter == "!" ? Expression.Not(ValueExpression) : ValueExpression; protected override Expression GetTagExpression(string exactName, string[] extraData)
{
if (extraData.Length is not (1 or 2) || extraData[0] is not ("!" or "") || extraData.Length == 2 && string.IsNullOrWhiteSpace(extraData[1]))
return Expression.Constant(false);
var getBool = extraData.Length == 2 ? CreateConditionExpression(extraData[1]) : CreateConditionExpression(null);
return extraData[0] == "!" ? Expression.Not(getBool) : getBool;
}
} }
} }

View File

@ -30,7 +30,7 @@ public class NamingTemplate
/// Invoke the <see cref="NamingTemplate"/> /// Invoke the <see cref="NamingTemplate"/>
/// </summary> /// </summary>
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param> /// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
public TemplatePart Evaluate(params object[] propertyClasses) public TemplatePart Evaluate(params object?[] propertyClasses)
{ {
if (templateToString is null) if (templateToString is null)
throw new InvalidOperationException(); throw new InvalidOperationException();
@ -39,7 +39,7 @@ public class NamingTemplate
// First parameter is "this", so ignore it. // First parameter is "this", so ignore it.
var delegateArgTypes = templateToString.Method.GetParameters().Skip(1); var delegateArgTypes = templateToString.Method.GetParameters().Skip(1);
object[] args = delegateArgTypes.Join(propertyClasses, o => o.ParameterType, i => i.GetType(), (_, i) => i).ToArray(); object?[] args = delegateArgTypes.Join(propertyClasses, o => o.ParameterType, i => i?.GetType(), (_, i) => i).ToArray();
if (args.Length != delegateArgTypes.Count()) if (args.Length != delegateArgTypes.Count())
throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}"); throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}");

View File

@ -1,6 +1,7 @@
using Dinah.Core; using Dinah.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -109,6 +110,25 @@ public class PropertyTagCollection<TClass> : TagCollection
catch { return null; } catch { return null; }
} }
/// <summary>
/// Try to get the default (unformatted) value of a property tag.
/// </summary>
/// <param name="tagName">Name of the tag value to get</param>
/// <param name="object">The property class from which the tag's value is read</param>
/// <param name="value"><paramref name="tagName"/>'s string value if it is in this collection, otherwise null</param>
/// <returns>True if the <paramref name="tagName"/> is in this collection, otherwise false</returns>
public bool TryGetValue(string tagName, TClass @object, [NotNullWhen(true)] out string? value)
{
value = null;
if (!StartsWith($"<{tagName}>", out var exactName, out var propertyTag, out var valueExpression))
return false;
var func = Expression.Lambda<Func<TClass, string>>(valueExpression, Parameter).Compile();
value = func(@object);
return true;
}
private class PropertyTag<TPropertyValue> : TagBase private class PropertyTag<TPropertyValue> : TagBase
{ {
public override Regex NameMatcher { get; } public override Regex NameMatcher { get; }
@ -138,8 +158,13 @@ public class PropertyTagCollection<TClass> : TagCollection
expVal); expVal);
} }
protected override Expression GetTagExpression(string exactName, string formatString) protected override Expression GetTagExpression(string exactName, string[] extraData)
{ {
if (extraData.Length is not (0 or 1))
return Expression.Constant(exactName);
string formatString = extraData.Length == 1 ? extraData[0] : "";
Expression toStringExpression Expression toStringExpression
= !ReturnType.IsValueType = !ReturnType.IsValueType
? Expression.Condition( ? Expression.Condition(

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -42,8 +43,8 @@ internal abstract class TagBase : IPropertyTag
/// <summary>Create an <see cref="Expression"/> that returns the property's value.</summary> /// <summary>Create an <see cref="Expression"/> that returns the property's value.</summary>
/// <param name="exactName">The exact string that was matched to <see cref="ITemplateTag"/></param> /// <param name="exactName">The exact string that was matched to <see cref="ITemplateTag"/></param>
/// <param name="formatter">The optional format string in the match inside the square brackets</param> /// <param name="extraData">Optional extra data parsed from the tag, such as a format string in the match the square brackets, logical negation, and conditional options</param>
protected abstract Expression GetTagExpression(string exactName, string formatter); protected abstract Expression GetTagExpression(string exactName, string[] extraData);
public bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue) public bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue)
{ {
@ -51,7 +52,7 @@ internal abstract class TagBase : IPropertyTag
if (match.Success) if (match.Success)
{ {
exactName = match.Value; exactName = match.Value;
propertyValue = GetTagExpression(exactName, match.Groups.Count == 2 ? match.Groups[1].Value.Trim() : ""); propertyValue = GetTagExpression(exactName, match.Groups.Values.Skip(1).Select(v => v.Value.Trim()).ToArray());
return true; return true;
} }

View File

@ -18,7 +18,7 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
/// <summary>The <see cref="ParameterExpression"/> of the <see cref="TagCollection"/>'s TClass type.</summary> /// <summary>The <see cref="ParameterExpression"/> of the <see cref="TagCollection"/>'s TClass type.</summary>
internal ParameterExpression Parameter { get; } internal ParameterExpression Parameter { get; }
protected RegexOptions Options { get; } = RegexOptions.Compiled; protected RegexOptions Options { get; } = RegexOptions.Compiled;
private List<IPropertyTag> PropertyTags { get; } = new(); internal List<IPropertyTag> PropertyTags { get; } = new();
protected TagCollection(Type classType, bool caseSensative = true) protected TagCollection(Type classType, bool caseSensative = true)
{ {

View File

@ -71,12 +71,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.2" /> <PackageReference Include="Avalonia" Version="11.3.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.2" /> <PackageReference Include="Avalonia.Desktop" Version="11.3.3" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--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 Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.3" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.2" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.3.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.2" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" /> <ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />

View File

@ -102,6 +102,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation UI", "Libation UI"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation CLI", "Libation CLI", "{47E27674-595D-4F7A-8CFB-127E768E1D1E}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation CLI", "Libation CLI", "{47E27674-595D-4F7A-8CFB-127E768E1D1E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssertionHelper", "_Tests\AssertionHelper\AssertionHelper.csproj", "{CFE7A0E5-37FE-40BE-A70B-41B5104181C4}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -258,6 +264,7 @@ Global
{FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE} = {8679CAC8-9164-4007-BDD2-F004810EDA14} {FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{53758A35-1C7E-4702-9B96-433ABA457B37} = {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} {47E27674-595D-4F7A-8CFB-127E768E1D1E} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{CFE7A0E5-37FE-40BE-A70B-41B5104181C4} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9} SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}

View File

@ -47,53 +47,58 @@
SelectedItem="{CompiledBinding FileDownloadQuality}"/> SelectedItem="{CompiledBinding FileDownloadQuality}"/>
</Grid> </Grid>
<Grid ColumnDefinitions="*,Auto">
<Grid ColumnDefinitions="*,*">
<CheckBox <CheckBox
IsChecked="{CompiledBinding UseWidevine, Mode=TwoWay}" ToolTip.Tip="{CompiledBinding UseWidevineTip}"
IsCheckedChanged="UseWidevine_IsCheckedChanged" IsCheckedChanged="UseWidevine_IsCheckedChanged"
ToolTip.Tip="{CompiledBinding UseWidevineTip}"> IsChecked="{CompiledBinding UseWidevine, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding UseWidevineText}" /> <TextBlock Text="{CompiledBinding UseWidevineText}" />
</CheckBox> </CheckBox>
<CheckBox <CheckBox
Grid.Column="1" 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}" ToolTip.Tip="{CompiledBinding RequestSpatialTip}"
IsEnabled="{CompiledBinding UseWidevine}" IsEnabled="{CompiledBinding UseWidevine}"
IsChecked="{CompiledBinding RequestSpatial, Mode=TwoWay}"> IsChecked="{CompiledBinding RequestSpatial, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding RequestSpatialText}" /> <TextBlock Text="{CompiledBinding RequestSpatialText}" />
</CheckBox> </CheckBox>
</Grid> <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.Column="1" Grid.Column="1"
ItemsSource="{CompiledBinding SpatialAudioCodecs}" ColumnDefinitions="Auto,Auto"
SelectedItem="{CompiledBinding SpatialAudioCodec}"/> 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> </Grid>
<CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}"> <CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding CreateCueSheetText}" /> <TextBlock Text="{CompiledBinding CreateCueSheetText}" />
</CheckBox> </CheckBox>

View File

@ -5,6 +5,7 @@ using LibationAvalonia.ViewModels.Settings;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates; using LibationFileManager.Templates;
using LibationUiBase.Forms; using LibationUiBase.Forms;
using ReactiveUI;
using System.Linq; using System.Linq;
using System.Threading.Tasks; 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) private async void UseWidevine_IsCheckedChanged(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
if (sender is CheckBox cbox && cbox.IsChecked is true) if (sender is CheckBox cbox && cbox.IsChecked is true)
@ -59,6 +69,10 @@ namespace LibationAvalonia.Controls.Settings
_viewModel.UseWidevine = false; _viewModel.UseWidevine = false;
} }
} }
else
{
_viewModel.Request_xHE_AAC = _viewModel.RequestSpatial = false;
}
} }
public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)

View File

@ -48,13 +48,13 @@
</Grid> </Grid>
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,*"> <Grid Grid.Row="1" RowDefinitions="Auto,Auto,*">
<TextBlock Text="NUMBER FIELDS" /> <TextBlock Text="STRING FIELDS" />
<TextBlock Grid.Row="1" Text="{CompiledBinding StringUsage}" /> <TextBlock Grid.Row="1" Text="{CompiledBinding StringUsage}" />
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding StringFields}"/> <ListBox Grid.Row="2" ItemsSource="{CompiledBinding StringFields}"/>
</Grid> </Grid>
<Grid Grid.Row="1" Grid.Column="1" RowDefinitions="Auto,Auto,*"> <Grid Grid.Row="1" Grid.Column="1" RowDefinitions="Auto,Auto,*">
<TextBlock Text="STRING FIELDS" /> <TextBlock Text="NUMBER FIELDS" />
<TextBlock Grid.Row="1" Text="{CompiledBinding NumberUsage}" /> <TextBlock Grid.Row="1" Text="{CompiledBinding NumberUsage}" />
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding NumberFields}"/> <ListBox Grid.Row="2" ItemsSource="{CompiledBinding NumberFields}"/>
</Grid> </Grid>

View File

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

View File

@ -54,7 +54,6 @@ namespace LibationAvalonia.ViewModels.Settings
StripAudibleBrandAudio = config.StripAudibleBrandAudio; StripAudibleBrandAudio = config.StripAudibleBrandAudio;
StripUnabridged = config.StripUnabridged; StripUnabridged = config.StripUnabridged;
_chapterTitleTemplate = config.ChapterTitleTemplate; _chapterTitleTemplate = config.ChapterTitleTemplate;
DecryptToLossy = config.DecryptToLossy;
MoveMoovToBeginning = config.MoveMoovToBeginning; MoveMoovToBeginning = config.MoveMoovToBeginning;
LameTargetBitrate = config.LameTargetBitrate; LameTargetBitrate = config.LameTargetBitrate;
LameDownsampleMono = config.LameDownsampleMono; LameDownsampleMono = config.LameDownsampleMono;
@ -69,6 +68,8 @@ namespace LibationAvalonia.ViewModels.Settings
SelectedEncoderQuality = config.LameEncoderQuality; SelectedEncoderQuality = config.LameEncoderQuality;
UseWidevine = config.UseWidevine; UseWidevine = config.UseWidevine;
RequestSpatial = config.RequestSpatial; RequestSpatial = config.RequestSpatial;
Request_xHE_AAC = config.Request_xHE_AAC;
DecryptToLossy = config.DecryptToLossy;
} }
public void SaveSettings(Configuration config) public void SaveSettings(Configuration config)
@ -100,6 +101,7 @@ namespace LibationAvalonia.ViewModels.Settings
config.SpatialAudioCodec = SpatialAudioCodec?.Value ?? config.SpatialAudioCodec; config.SpatialAudioCodec = SpatialAudioCodec?.Value ?? config.SpatialAudioCodec;
config.UseWidevine = UseWidevine; config.UseWidevine = UseWidevine;
config.RequestSpatial = RequestSpatial; config.RequestSpatial = RequestSpatial;
config.Request_xHE_AAC = Request_xHE_AAC;
} }
public AvaloniaList<EnumDisplay<Configuration.DownloadQuality>> DownloadQualities { get; } = new([ 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 FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality));
public string UseWidevineText { get; } = Configuration.GetDescription(nameof(Configuration.UseWidevine)); public string UseWidevineText { get; } = Configuration.GetDescription(nameof(Configuration.UseWidevine));
public string UseWidevineTip { get; } = Configuration.GetHelpText(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 RequestSpatialText { get; } = Configuration.GetDescription(nameof(Configuration.RequestSpatial));
public string RequestSpatialTip { get; } = Configuration.GetHelpText(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 SpatialAudioCodecTip { get; } = Configuration.GetHelpText(nameof(Configuration.SpatialAudioCodec));
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet)); public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));
public string CombineNestedChapterTitlesText { get; } = Configuration.GetDescription(nameof(Configuration.CombineNestedChapterTitles)); 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 string RetainAaxFileTip => Configuration.GetHelpText(nameof(RetainAaxFile));
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); } public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
private bool _useWidevine, _requestSpatial, _request_xHE_AAC;
private bool _useWidevine;
private bool _requestSpatial;
public bool UseWidevine { get => _useWidevine; set => this.RaiseAndSetIfChanged(ref _useWidevine, value); } 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 bool RequestSpatial { get => _requestSpatial; set => this.RaiseAndSetIfChanged(ref _requestSpatial, value); }
public EnumDisplay<Configuration.DownloadQuality> FileDownloadQuality { get; set; } public EnumDisplay<Configuration.DownloadQuality> FileDownloadQuality { get; set; }
@ -155,7 +157,18 @@ namespace LibationAvalonia.ViewModels.Settings
public string StripAudibleBrandAudioTip => Configuration.GetHelpText(nameof(StripAudibleBrandAudio)); public string StripAudibleBrandAudioTip => Configuration.GetHelpText(nameof(StripAudibleBrandAudio));
public bool StripUnabridged { get; set; } public bool StripUnabridged { get; set; }
public string StripUnabridgedTip => Configuration.GetHelpText(nameof(StripUnabridged)); 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 string DecryptToLossyTip => Configuration.GetHelpText(nameof(DecryptToLossy));
public bool MoveMoovToBeginning { get; set; } public bool MoveMoovToBeginning { get; set; }

View File

@ -89,23 +89,26 @@ namespace LibationFileManager
AC-4 cannot be converted to MP3. AC-4 cannot be converted to MP3.
""" }, """ },
{nameof(UseWidevine), """ {nameof(UseWidevine), """
Some audiobooks are only delivered in the highest Some audiobooks are only delivered in the highest
available quality with special, third-party content available quality with special, third-party content
protection. Enabling this option will make Libation protection. Enabling this option will allows you to
request audiobooks with Widevine DRM, which may request audiobooks in the xHE-AAC codec and in
yield higher quality audiobook files. If they are spatial (Dolby Atmos) audio formats.
higher quality, however, they will also be encoded """ },
with a somewhat uncommon codec (xHE-AAC USAC) {nameof(Request_xHE_AAC), """
which you may have difficulty playing. If selected, Libation will request audiobooks in the
xHE-AAC codec. This codec is generally better quality
This must be enable to download spatial audiobooks. 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), """ {nameof(RequestSpatial), """
If selected, Libation will request audiobooks in the If selected, Libation will request audiobooks in the
Dolby Atmos 'Spatial Audio' format. Audiobooks which Dolby Atmos 'Spatial Audio' format. Audiobooks which
don't have a spatial audio version will be download 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",""" {"LocateAudiobooks","""
Scan the contents a folder to find audio files that Scan the contents a folder to find audio files that

View File

@ -285,9 +285,12 @@ namespace LibationFileManager
AC_4 AC_4
} }
[Description("Use widevine DRM")] [Description("Use Widevine DRM")]
public bool UseWidevine { get => GetNonString(defaultValue: false); set => SetNonString(value); } 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")] [Description("Request Spatial Audio")]
public bool RequestSpatial { get => GetNonString(defaultValue: true); set => SetNonString(value); } 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.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using FileManager; using System.Threading.Tasks;
using Newtonsoft.Json;
#nullable enable #nullable enable
namespace LibationFileManager namespace LibationFileManager
@ -32,6 +33,10 @@ namespace LibationFileManager
{ {
Cache = JsonConvert.DeserializeObject<FileCacheV2<CacheEntry>>(File.ReadAllText(jsonFileV2)) 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."); ?? 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) 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 bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
public static List<(FileType fileType, LongPath path)> GetFiles(string id) public static List<(FileType fileType, LongPath path)> GetFiles(string id)
@ -111,10 +133,20 @@ namespace LibationFileManager
return false; return false;
} }
public static void Insert(string id, string path) public static void Insert(string id, params string[] paths)
{ {
var type = FileTypes.GetFileTypeFromPath(path); var newEntries
Insert(new CacheEntry(id, type, path)); = 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) public static void Insert(CacheEntry entry)
@ -150,9 +182,11 @@ namespace LibationFileManager
private class FileCacheV2<TEntry> private class FileCacheV2<TEntry>
{ {
[JsonProperty] [JsonProperty]
private readonly ConcurrentDictionary<string, List<TEntry>> Dictionary = new(); private readonly ConcurrentDictionary<string, HashSet<TEntry>> Dictionary = new();
private static object lockObject = new(); private static object lockObject = new();
public List<string> GetIDs() => Dictionary.Keys.ToList();
public List<TEntry> GetIdEntries(string id) public List<TEntry> GetIdEntries(string id)
{ {
static List<TEntry> empty() => new(); static List<TEntry> empty() => new();
@ -162,23 +196,34 @@ namespace LibationFileManager
public void Add(string id, TEntry entry) 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) public void AddRange(string id, IEnumerable<TEntry> entries)
{ {
Dictionary.AddOrUpdate(id, entries.ToList(), (id, entries) => Dictionary.AddOrUpdate<IEnumerable<TEntry>>(id,
{ (_, e) => e.ToHashSet(), //Add new Dictionary Value
entries.AddRange(entries); (id, existingEntries, newEntries) => //Update existing Dictionary Value
return entries; {
}); foreach (var entry in newEntries)
existingEntries.Add(entry);
return existingEntries;
},
entries);
} }
public bool Remove(string id, TEntry entry) public bool Remove(string id, TEntry entry)
{ {
lock (lockObject) lock (lockObject)
{ {
if (Dictionary.TryGetValue(id, out List<TEntry>? entries)) if (Dictionary.TryGetValue(id, out HashSet<TEntry>? entries))
{ {
var removed = entries?.Remove(entry) ?? false; var removed = entries?.Remove(entry) ?? false;
if (removed && entries?.Count == 0) if (removed && entries?.Count == 0)

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.8" />
<PackageReference Include="NameParserSharp" Version="1.5.0" /> <PackageReference Include="NameParserSharp" Version="1.5.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" /> <PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,15 @@
using AaxDecrypter;
#nullable enable
namespace LibationFileManager.Templates;
public class CombinedDto
{
public LibraryBookDto LibraryBook { get; }
public MultiConvertFileProperties? MultiConvert { get; }
public CombinedDto(LibraryBookDto libraryBook, MultiConvertFileProperties? multiConvert = null)
{
LibraryBook = libraryBook;
MultiConvert = multiConvert;
}
}

View File

@ -1,27 +1,34 @@
using System; using System;
using System.Text.RegularExpressions;
#nullable enable #nullable enable
namespace LibationFileManager.Templates; namespace LibationFileManager.Templates;
public record SeriesDto : IFormattable public partial record SeriesDto : IFormattable
{ {
public string Name { get; } public string Name { get; }
public string? Number { get; } public SeriesOrder Order { get; }
public string AudibleSeriesId { get; } public string AudibleSeriesId { get; }
public SeriesDto(string name, string? number, string audibleSeriesId) public SeriesDto(string name, string? number, string audibleSeriesId)
{ {
Name = name; Name = name;
Number = number; Order = SeriesOrder.Parse(number);
AudibleSeriesId = audibleSeriesId; AudibleSeriesId = audibleSeriesId;
} }
public override string ToString() => Name.Trim(); public override string ToString() => Name.Trim();
public string ToString(string? format, IFormatProvider? _) public string ToString(string? format, IFormatProvider? _)
=> string.IsNullOrWhiteSpace(format) ? ToString() => string.IsNullOrWhiteSpace(format) ? ToString()
: format : FormatRegex().Replace(format, MatchEvaluator)
.Replace("{N}", Name) .Replace("{N}", Name)
.Replace("{#}", Number?.ToString()) .Replace("{ID}", AudibleSeriesId)
.Replace("{ID}", AudibleSeriesId) .Trim();
.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); : IListFormat<SeriesListFormat>.Join(formatString, series);
/// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary> /// <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(); 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.IsNullOrEmpty(prefix))
parts.Add(prefix);
parts.Add(value);
order = order[range.End.Value..];
}
if (!string.IsNullOrEmpty(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, System.Globalization.CultureInfo.InvariantCulture, out value))
{
range = new Range(s, e);
return true;
}
}
}
range = default;
return false;
}
}

View File

@ -56,5 +56,6 @@ namespace LibationFileManager.Templates
public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<if podcast-><-if podcast>", "<if podcast->...<-if podcast>"); public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<if podcast-><-if podcast>", "<if podcast->...<-if podcast>");
public static TemplateTags IfPodcastParent { get; } = new TemplateTags("if podcastparent", "Only include if item is a podcast series parent", "<if podcastparent-><-if podcastparent>", "<if podcastparent->...<-if podcastparent>"); public static TemplateTags IfPodcastParent { get; } = new TemplateTags("if podcastparent", "Only include if item is a podcast series parent", "<if podcastparent-><-if podcastparent>", "<if podcastparent->...<-if podcastparent>");
public static TemplateTags IfBookseries { get; } = new TemplateTags("if bookseries", "Only include if part of a book series", "<if bookseries-><-if bookseries>", "<if bookseries->...<-if bookseries>"); public static TemplateTags IfBookseries { get; } = new TemplateTags("if bookseries", "Only include if part of a book series", "<if bookseries-><-if bookseries>", "<if bookseries->...<-if bookseries>");
public static TemplateTags Has { get; } = new TemplateTags("has", "Only include if PROPERTY has a value (i.e. not null or empty)", "<has -><-has>", "<has PROPERTY->...<-has>");
} }
} }

View File

@ -111,7 +111,7 @@ namespace LibationFileManager.Templates
{ {
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps)); ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps));
return string.Concat(NamingTemplate.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value)); return string.Concat(NamingTemplate.Evaluate(libraryBookDto, multiChapProps, new CombinedDto(libraryBookDto, multiChapProps)).Select(p => p.Value));
} }
public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false) public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false)
@ -138,11 +138,11 @@ namespace LibationFileManager.Templates
protected virtual IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements) protected virtual IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
=> parts.Select(p => replacements.ReplaceFilenameChars(p.Value)); => parts.Select(p => replacements.ReplaceFilenameChars(p.Value));
private LongPath GetFilename(string baseDir, string fileExtension, ReplacementCharacters replacements, bool returnFirstExisting, params object[] dtos) private LongPath GetFilename(string baseDir, string fileExtension, ReplacementCharacters replacements, bool returnFirstExisting, LibraryBookDto lbDto, MultiConvertFileProperties? multiDto = null)
{ {
fileExtension = FileUtility.GetStandardizedExtension(fileExtension); fileExtension = FileUtility.GetStandardizedExtension(fileExtension);
var parts = NamingTemplate.Evaluate(dtos).ToList(); var parts = NamingTemplate.Evaluate(lbDto, multiDto, new CombinedDto(lbDto, multiDto)).ToList();
var pathParts = GetPathParts(GetTemplatePartsStrings(parts, replacements)); var pathParts = GetPathParts(GetTemplatePartsStrings(parts, replacements));
//Remove 1 character from the end of the longest filename part until //Remove 1 character from the end of the longest filename part until
@ -271,7 +271,7 @@ namespace LibationFileManager.Templates
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator, FormattableFormatter }, { TemplateTags.FirstNarrator, lb => lb.FirstNarrator, FormattableFormatter },
{ TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter }, { TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter },
{ TemplateTags.FirstSeries, lb => lb.FirstSeries, FormattableFormatter }, { TemplateTags.FirstSeries, lb => lb.FirstSeries, FormattableFormatter },
{ TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Number }, { TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Order, FormattableFormatter },
{ TemplateTags.Language, lb => lb.Language }, { TemplateTags.Language, lb => lb.Language },
//Don't allow formatting of LanguageShort //Don't allow formatting of LanguageShort
{ TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort }, { TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort },
@ -323,6 +323,35 @@ namespace LibationFileManager.Templates
{ TemplateTags.IfBookseries, lb => lb.IsSeries && !lb.IsPodcast && !lb.IsPodcastParent }, { TemplateTags.IfBookseries, lb => lb.IsSeries && !lb.IsPodcast && !lb.IsPodcastParent },
}; };
private static readonly ConditionalTagCollection<CombinedDto> combinedConditionalTags = new()
{
{ TemplateTags.Has, HasValue}
};
private static bool HasValue(ITemplateTag tag, CombinedDto dtos, string condition)
{
foreach (var c in chapterPropertyTags.OfType<PropertyTagCollection<LibraryBookDto>>().Append(filePropertyTags).Append(audioFilePropertyTags))
{
if (c.TryGetValue(condition, dtos.LibraryBook, out var value))
{
return !string.IsNullOrWhiteSpace(value);
}
}
if (dtos.MultiConvert is null)
return false;
foreach (var c in chapterPropertyTags.OfType<PropertyTagCollection<MultiConvertFileProperties>>())
{
if (c.TryGetValue(condition, dtos.MultiConvert, out var value))
{
return !string.IsNullOrWhiteSpace(value);
}
}
return false;
}
private static readonly ConditionalTagCollection<LibraryBookDto> folderConditionalTags = new() private static readonly ConditionalTagCollection<LibraryBookDto> folderConditionalTags = new()
{ {
{ TemplateTags.IfPodcastParent, lb => lb.IsPodcastParent } { TemplateTags.IfPodcastParent, lb => lb.IsPodcastParent }
@ -388,7 +417,7 @@ namespace LibationFileManager.Templates
public static string Name { get; } = "Folder Template"; public static string Name { get; } = "Folder Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? ""; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title short> [<id>]"; public static string DefaultTemplate { get; } = "<title short> [<id>]";
public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, audioFilePropertyTags, conditionalTags, folderConditionalTags]; public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, audioFilePropertyTags, conditionalTags, folderConditionalTags, combinedConditionalTags];
public override IEnumerable<string> Errors public override IEnumerable<string> Errors
=> TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors; => TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors;
@ -407,7 +436,7 @@ namespace LibationFileManager.Templates
public static string Name { get; } = "File Template"; public static string Name { get; } = "File Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)) ?? ""; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title> [<id>]"; public static string DefaultTemplate { get; } = "<title> [<id>]";
public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, audioFilePropertyTags, conditionalTags]; public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, audioFilePropertyTags, conditionalTags, combinedConditionalTags];
} }
public class ChapterFileTemplate : Templates, ITemplate public class ChapterFileTemplate : Templates, ITemplate
@ -416,7 +445,7 @@ namespace LibationFileManager.Templates
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)) ?? ""; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
public static IEnumerable<TagCollection> TagCollections { get; } public static IEnumerable<TagCollection> TagCollections { get; }
= chapterPropertyTags.Append(filePropertyTags).Append(audioFilePropertyTags).Append(conditionalTags); = chapterPropertyTags.Append(filePropertyTags).Append(audioFilePropertyTags).Append(conditionalTags).Append(combinedConditionalTags);
public override IEnumerable<string> Warnings public override IEnumerable<string> Warnings
=> NamingTemplate.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName)) => NamingTemplate.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName))
@ -429,7 +458,7 @@ namespace LibationFileManager.Templates
public static string Name { get; } = "Chapter Title Template"; public static string Name { get; } = "Chapter Title Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)) ?? ""; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)) ?? "";
public static string DefaultTemplate => "<ch#> - <title short>: <ch title>"; public static string DefaultTemplate => "<ch#> - <title short>: <ch title>";
public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(conditionalTags); public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(conditionalTags).Append(combinedConditionalTags);
protected override IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements) protected override IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
=> parts.Select(p => p.Value); => parts.Select(p => p.Value);

View File

@ -46,6 +46,7 @@ namespace LibationSearchEngine
{ FieldType.String, lb => lb.Book.UserDefinedItem.Tags, TAGS.FirstCharToUpper() }, { FieldType.String, lb => lb.Book.UserDefinedItem.Tags, TAGS.FirstCharToUpper() },
{ FieldType.String, lb => lb.Book.Locale, "Locale", "Region" }, { FieldType.String, lb => lb.Book.Locale, "Locale", "Region" },
{ FieldType.String, lb => lb.Account, "Account", "Email" }, { FieldType.String, lb => lb.Account, "Account", "Email" },
{ FieldType.String, lb => lb.Book.UserDefinedItem.LastDownloadedFormat?.CodecString, "Codec", "DownloadedCodec" },
{ FieldType.Bool, lb => lb.Book.HasPdf().ToString(), "HasDownloads", "HasDownload", "Downloads" , "Download", "HasPDFs", "HasPDF" , "PDFs", "PDF" }, { FieldType.Bool, lb => lb.Book.HasPdf().ToString(), "HasDownloads", "HasDownload", "Downloads" , "Download", "HasPDFs", "HasPDF" , "PDFs", "PDF" },
{ FieldType.Bool, lb => (lb.Book.UserDefinedItem.Rating.OverallRating > 0f).ToString(), "IsRated", "Rated" }, { FieldType.Bool, lb => (lb.Book.UserDefinedItem.Rating.OverallRating > 0f).ToString(), "IsRated", "Rated" },
{ FieldType.Bool, lb => isAuthorNarrated(lb.Book).ToString(), "IsAuthorNarrated", "AuthorNarrated" }, { FieldType.Bool, lb => isAuthorNarrated(lb.Book).ToString(), "IsAuthorNarrated", "AuthorNarrated" },
@ -65,7 +66,9 @@ namespace LibationSearchEngine
{ FieldType.Number, lb => lb.Book.UserDefinedItem.Rating.OverallRating.ToLuceneString(), "UserRating", "MyRating" }, { FieldType.Number, lb => lb.Book.UserDefinedItem.Rating.OverallRating.ToLuceneString(), "UserRating", "MyRating" },
{ FieldType.Number, lb => lb.Book.DatePublished?.ToLuceneString() ?? "", nameof(Book.DatePublished) }, { FieldType.Number, lb => lb.Book.DatePublished?.ToLuceneString() ?? "", nameof(Book.DatePublished) },
{ FieldType.Number, lb => lb.Book.UserDefinedItem.LastDownloaded.ToLuceneString(), nameof(UserDefinedItem.LastDownloaded), "LastDownload" }, { FieldType.Number, lb => lb.Book.UserDefinedItem.LastDownloaded.ToLuceneString(), nameof(UserDefinedItem.LastDownloaded), "LastDownload" },
{ FieldType.Number, lb => lb.DateAdded.ToLuceneString(), nameof(LibraryBook.DateAdded) } { FieldType.Number, lb => lb.Book.UserDefinedItem.LastDownloadedFormat?.BitRate.ToLuceneString(), "Bitrate", "DownloadedBitrate" },
{ FieldType.Number, lb => lb.Book.UserDefinedItem.LastDownloadedFormat?.SampleRate.ToLuceneString(), "SampleRate", "DownloadedSampleRate" },
{ FieldType.Number, lb => lb.DateAdded.ToLuceneString(), nameof(LibraryBook.DateAdded) }
}; };
#endregion #endregion

View File

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

View File

@ -42,7 +42,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="9.0.1.1" /> <PackageReference Include="Dinah.Core.WindowsDesktop" Version="9.0.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -26,7 +26,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3351.48" /> <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3405.78" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dinah.Core" Version="9.0.1.1" /> <PackageReference Include="Dinah.Core" Version="9.0.3.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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(expectedValue, value);
[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(otherValue, value);
[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(expectedValue, value, 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;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using AssertionHelper;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AudibleApi; using AudibleApi;
using AudibleApi.Authorization; using AudibleApi.Authorization;
using AudibleUtilities; using AudibleUtilities;
using Dinah.Core;
using FluentAssertions;
using FluentAssertions.Common;
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace AccountsTests namespace AccountsTests
{ {
@ -528,7 +518,7 @@ namespace AccountsTests
var a2 = new Account("a") { AccountName = "two", IdentityTokens = idIn }; var a2 = new Account("a") { AccountName = "two", IdentityTokens = idIn };
// violation: validate() // violation: validate()
Assert.ThrowsException<InvalidOperationException>(() => accountsSettings.Add(a2)); Assert.ThrowsExactly<InvalidOperationException>(() => accountsSettings.Add(a2));
} }
[TestMethod] [TestMethod]
@ -545,7 +535,7 @@ namespace AccountsTests
accountsSettings.Add(a2); accountsSettings.Add(a2);
// violation: GetAccount.SingleOrDefault // 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> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
@ -6,10 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="MSTest.TestAdapter" Version="3.9.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.9.3" />
<PackageReference Include="coverlet.collector" Version="6.0.4"> <PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -18,6 +15,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\AudibleUtilities\AudibleUtilities.csproj" /> <ProjectReference Include="..\..\AudibleUtilities\AudibleUtilities.csproj" />
<ProjectReference Include="..\AssertionHelper\AssertionHelper.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
using System.Linq; using System.Linq;
using AssertionHelper;
using FileManager.NamingTemplate; using FileManager.NamingTemplate;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace NamingTemplateTests namespace NamingTemplateTests
@ -15,6 +15,7 @@ namespace NamingTemplateTests
public string Item1 { get; set; } public string Item1 { get; set; }
public string Item2 { get; set; } public string Item2 { get; set; }
public string Item3 { get; set; } public string Item3 { get; set; }
public string NullItem { get; set; }
public int Int1 { get; set; } public int Int1 { get; set; }
public bool Condition { get; set; } public bool Condition { get; set; }
} }
@ -25,6 +26,7 @@ namespace NamingTemplateTests
public string Item2 { get; set; } public string Item2 { get; set; }
public string Item3 { get; set; } public string Item3 { get; set; }
public string Item4 { get; set; } public string Item4 { get; set; }
public string NullItem { get; set; }
public bool Condition { get; set; } public bool Condition { get; set; }
} }
class PropertyClass3 class PropertyClass3
@ -33,6 +35,7 @@ namespace NamingTemplateTests
public string Item2 { get; set; } public string Item2 { get; set; }
public string Item3 { get; set; } public string Item3 { get; set; }
public string Item4 { get; set; } public string Item4 { get; set; }
public string NullItem { get; set; }
public ReferenceType RefType { get; set; } public ReferenceType RefType { get; set; }
public int? Int2 { get; set; } public int? Int2 { get; set; }
public bool Condition { get; set; } public bool Condition { get; set; }
@ -49,41 +52,54 @@ namespace NamingTemplateTests
[TestClass] [TestClass]
public class GetPortionFilename public class GetPortionFilename
{ {
PropertyTagCollection<PropertyClass1> props1 = new() static PropertyTagCollection<PropertyClass1> props1 = new()
{ {
{ new TemplateTag { TagName = "item1" }, i => i.Item1 }, { new TemplateTag { TagName = "item1" }, i => i.Item1 },
{ new TemplateTag { TagName = "item2" }, i => i.Item2 }, { new TemplateTag { TagName = "item2" }, i => i.Item2 },
{ new TemplateTag { TagName = "item3" }, i => i.Item3 } { new TemplateTag { TagName = "item3" }, i => i.Item3 },
{ new TemplateTag { TagName = "null_1" }, i => i.NullItem }
}; };
PropertyTagCollection<PropertyClass2> props2 = new() static PropertyTagCollection<PropertyClass2> props2 = new()
{ {
{ new TemplateTag { TagName = "item1" }, i => i.Item1 }, { new TemplateTag { TagName = "item1" }, i => i.Item1 },
{ new TemplateTag { TagName = "item2" }, i => i.Item2 }, { new TemplateTag { TagName = "item2" }, i => i.Item2 },
{ new TemplateTag { TagName = "item3" }, i => i.Item3 }, { new TemplateTag { TagName = "item3" }, i => i.Item3 },
{ new TemplateTag { TagName = "item4" }, i => i.Item4 }, { new TemplateTag { TagName = "item4" }, i => i.Item4 },
{ new TemplateTag { TagName = "null_2" }, i => i.NullItem }
}; };
PropertyTagCollection<PropertyClass3> props3 = new(true, GetVal) static PropertyTagCollection<PropertyClass3> props3 = new(true, GetVal)
{ {
{ new TemplateTag { TagName = "item3_1" }, i => i.Item1 }, { new TemplateTag { TagName = "item3_1" }, i => i.Item1 },
{ new TemplateTag { TagName = "item3_2" }, i => i.Item2 }, { new TemplateTag { TagName = "item3_2" }, i => i.Item2 },
{ new TemplateTag { TagName = "item3_3" }, i => i.Item3 }, { new TemplateTag { TagName = "item3_3" }, i => i.Item3 },
{ new TemplateTag { TagName = "item3_4" }, i => i.Item4 }, { new TemplateTag { TagName = "item3_4" }, i => i.Item4 },
{ new TemplateTag { TagName = "null_3" }, i => i.NullItem },
{ new TemplateTag { TagName = "reftype" }, i => i.RefType }, { new TemplateTag { TagName = "reftype" }, i => i.RefType },
}; };
ConditionalTagCollection<PropertyClass1> conditional1 = new() ConditionalTagCollection<PropertyClass1> conditional1 = new()
{ {
{ new TemplateTag { TagName = "ifc1" }, i => i.Condition }, { new TemplateTag { TagName = "ifc1" }, i => i.Condition },
{ new TemplateTag { TagName = "has1" }, HasValue }
}; };
ConditionalTagCollection<PropertyClass2> conditional2 = new() ConditionalTagCollection<PropertyClass2> conditional2 = new()
{ {
{ new TemplateTag { TagName = "ifc2" }, i => i.Condition }, { new TemplateTag { TagName = "ifc2" }, i => i.Condition },
{ new TemplateTag { TagName = "has2" }, HasValue }
}; };
ConditionalTagCollection<PropertyClass3> conditional3 = new() ConditionalTagCollection<PropertyClass3> conditional3 = new()
{ {
{ new TemplateTag { TagName = "ifc3" }, i => i.Condition }, { new TemplateTag { TagName = "ifc3" }, i => i.Condition },
{ new TemplateTag { TagName = "has3" }, HasValue }
}; };
private static bool HasValue(ITemplateTag templateTag, PropertyClass1 referenceType, string condition)
=> props1.TryGetValue(condition, referenceType, out var value) && !string.IsNullOrEmpty(value);
private static bool HasValue(ITemplateTag templateTag, PropertyClass2 referenceType, string condition)
=> props2.TryGetValue(condition, referenceType, out var value) && !string.IsNullOrEmpty(value);
private static bool HasValue(ITemplateTag templateTag, PropertyClass3 referenceType, string condition)
=> props3.TryGetValue(condition, referenceType, out var value) && !string.IsNullOrEmpty(value);
PropertyClass1 propertyClass1 = new() PropertyClass1 propertyClass1 = new()
{ {
Item1 = "prop1_item1", Item1 = "prop1_item1",
@ -123,6 +139,8 @@ namespace NamingTemplateTests
[DataRow("<ifc1-><ifc3-><item1><ifc2-><item4><-ifc2><item3_2><-ifc3><-ifc1>", "prop1_item1prop3_item2", 3)] [DataRow("<ifc1-><ifc3-><item1><ifc2-><item4><-ifc2><item3_2><-ifc3><-ifc1>", "prop1_item1prop3_item2", 3)]
[DataRow("<ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3><-ifc1><-ifc2>", "", 3)] [DataRow("<ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3><-ifc1><-ifc2>", "", 3)]
[DataRow("<!ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3><-ifc1><-ifc2>", "prop1_item1prop2_item4prop3_item2", 3)] [DataRow("<!ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3><-ifc1><-ifc2>", "prop1_item1prop2_item4prop3_item2", 3)]
[DataRow("<!has1 null_1-><has2 item1-><has3 item3_2-><item1><item4><item3_2><-has3><-has2><-has1>", "prop1_item1prop2_item4prop3_item2", 3)]
[DataRow("<!has1 null_1->null_1 is null, <-has1><has2 item1-><item1><-has2><has3 item3_2-><item3_2><-has3>", "null_1 is null, prop1_item1prop3_item2", 2)]
public void test(string inStr, string outStr, int numTags) public void test(string inStr, string outStr, int numTags)
{ {
var template = NamingTemplate.Parse(inStr, new TagCollection[] { props1, props2, props3, conditional1, conditional2, conditional3 }); var template = NamingTemplate.Parse(inStr, new TagCollection[] { props1, props2, props3, conditional1, conditional2, conditional3 });
@ -136,8 +154,63 @@ namespace NamingTemplateTests
templateText.Should().Be(outStr); templateText.Should().Be(outStr);
} }
[TestMethod]
[DataRow("<has1->true<-has1>", "" )]
[DataRow("<has2->true<-has2>", "" )]
[DataRow("<has3->true<-has3>", "" )]
[DataRow("<has4->true<-has4>", "<has4->true<-has4>")]
[DataRow("<has1 null_1->true<-has1>", "")]
[DataRow("<has2 null_2->true<-has2>", "")]
[DataRow("<has3 null_3->true<-has3>", "")]
[DataRow("<!has1 null_1->true<-has1>", "true")]
[DataRow("<!has2 null_2->true<-has2>", "true")]
[DataRow("<!has3 null_3->true<-has3>", "true")]
[DataRow("<has1 item1->true<-has1>", "true")]
[DataRow("<has2 item1->true<-has2>", "true")]
[DataRow("<has3 item3_1->true<-has3>", "true")]
[DataRow("<!has1 item1->true<-has1>", "")]
[DataRow("<!has2 item1->true<-has2>", "")]
[DataRow("<!has3 item3_1->true<-has3>", "")]
[DataRow("<has3 item3_1 ->true<-has3>", "true")]
public void Has_test(string inStr, string outStr)
{
var template = NamingTemplate.Parse(inStr, [props1, props2, props3, conditional1, conditional2, conditional3]);
template.Warnings.Should().HaveCount(1);
template.Errors.Should().HaveCount(0);
var templateText = string.Concat(template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value));
templateText.Should().Be(outStr);
}
[TestMethod]
[DataRow("<has3item3_1->true<-has3>", "<has3item3_1->true")]
[DataRow("< has3 item3_1->true<-has3>", "< has3 item3_1->true")]
[DataRow("<has3 item3_1- >true<-has3>", "<has3 item3_1- >true")]
[DataRow("<has3 item3_1 >true<-has3>", "<has3 item3_1 >true")]
[DataRow("<has3 item3_1>true<-has3>", "<has3 item3_1>true")]
[DataRow("<has3 item3_1->true<- has3>", "true<- has3>")]
[DataRow("<has3 item3_1->true< has3>", "true< has3>")]
[DataRow("<has3 item3_1->true<!has3>", "true<!has3>")]
[DataRow("<has3 item3_1->true<has3>", "true<has3>")]
[DataRow("<has3 item3_1->true<has3 >", "true<has3 >")]
[DataRow("<has3 item3_1->true< -has3>", "true< -has3>")]
public void Has_invalid(string inStr, string outStr)
{
var template = NamingTemplate.Parse(inStr, [props1, props2, props3, conditional1, conditional2, conditional3]);
template.Warnings.Should().HaveCount(2);
template.Errors.Should().HaveCount(0);
var templateText = string.Concat(template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value));
templateText.Should().Be(outStr);
}
[TestMethod] [TestMethod]
[DataRow("<ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3><-ifc1><ifc2->", new string[] { "Missing <-ifc2> closing conditional.", "Missing <-ifc2> closing conditional." })] [DataRow("<ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3><-ifc1><ifc2->", new string[] { "Missing <-ifc2> closing conditional.", "Missing <-ifc2> closing conditional." })]
[DataRow("<has2-><has1-><has3-><item1><item4><item3_2><-has3><-has1><has2->", new string[] { "Missing <-has2> closing conditional.", "Missing <-has2> closing conditional." })]
[DataRow("<ifc2-><ifc1-><ifc3-><-ifc3><-ifc1><-ifc2>", new string[] { "Should use tags. Eg: <title>" })] [DataRow("<ifc2-><ifc1-><ifc3-><-ifc3><-ifc1><-ifc2>", new string[] { "Should use tags. Eg: <title>" })]
[DataRow("<ifc1-><ifc3-><item1><-ifc3><-ifc1><-ifc2>", new string[] { "Missing <ifc2-> open conditional." })] [DataRow("<ifc1-><ifc3-><item1><-ifc3><-ifc1><-ifc2>", new string[] { "Missing <ifc2-> open conditional." })]
[DataRow("<ifc1-><ifc3-><-ifc3><-ifc1><-ifc2>", new string[] { "Missing <ifc2-> open conditional.", "Should use tags. Eg: <title>" })] [DataRow("<ifc1-><ifc3-><-ifc3><-ifc1><-ifc2>", new string[] { "Missing <ifc2-> open conditional.", "Should use tags. Eg: <title>" })]

View File

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

View File

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

View File

@ -2,10 +2,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dinah.Core; using AaxDecrypter;
using AssertionHelper;
using FileManager; using FileManager;
using FileManager.NamingTemplate; using FileManager.NamingTemplate;
using FluentAssertions;
using LibationFileManager.Templates; using LibationFileManager.Templates;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -53,8 +53,13 @@ namespace TemplatesTests
BitRate = 128, BitRate = 128,
SampleRate = 44100, SampleRate = 44100,
Channels = 2, Channels = 2,
Language = "English" Language = "English",
}; Subtitle = "An Audible Original Drama",
TitleWithSubtitle = "A Study in Scarlet: An Audible Original Drama",
Codec = "AAC-LC",
FileVersion = "1.0",
LibationVersion = "1.0.0",
};
} }
[TestClass] [TestClass]
@ -374,6 +379,55 @@ namespace TemplatesTests
.Should().Be(expected); .Should().Be(expected);
} }
[TestMethod]
[DataRow("<has id->true<-has>", "true")]
[DataRow("<has title->true<-has>", "true")]
[DataRow("<has title short->true<-has>", "true")]
[DataRow("<has audible title->true<-has>", "true")]
[DataRow("<has audible subtitle->true<-has>", "true")]
[DataRow("<has author->true<-has>", "true")]
[DataRow("<has first author->true<-has>", "true")]
[DataRow("<has narrator->true<-has>", "true")]
[DataRow("<has first narrator->true<-has>", "true")]
[DataRow("<has series->true<-has>", "true")]
[DataRow("<has first series->true<-has>", "true")]
[DataRow("<has series#->true<-has>", "true")]
[DataRow("<has bitrate->true<-has>", "true")]
[DataRow("<has samplerate->true<-has>", "true")]
[DataRow("<has channels->true<-has>", "true")]
[DataRow("<has codec->true<-has>", "true")]
[DataRow("<has file version->true<-has>", "true")]
[DataRow("<has libation version->true<-has>", "true")]
[DataRow("<has account->true<-has>", "true")]
[DataRow("<has account nickname->true<-has>", "true")]
[DataRow("<has locale->true<-has>", "true")]
[DataRow("<has year->true<-has>", "true")]
[DataRow("<has language->true<-has>", "true")]
[DataRow("<has language short->true<-has>", "true")]
[DataRow("<has file date->true<-has>", "true")]
[DataRow("<has pub date->true<-has>", "true")]
[DataRow("<has date added->true<-has>", "true")]
[DataRow("<has ch count->true<-has>", "true")]
[DataRow("<has ch title->true<-has>", "true")]
[DataRow("<has ch#->true<-has>", "true")]
[DataRow("<has ch# 0->true<-has>", "true")]
[DataRow("<has FAKE->true<-has>", "")]
public void HasValue_test(string template, string expected)
{
var bookDto = GetLibraryBook();
var multiDto = new MultiConvertFileProperties
{
PartsPosition = 1,
PartsTotal = 2,
Title = bookDto.Title,
};
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
fileTemplate
.GetFilename(bookDto, multiDto, "", "", Replacements)
.PathWithoutPrefix
.Should().Be(expected);
}
[TestMethod] [TestMethod]
[DataRow("<series>", "Series A, Series B, Series C, Series D")] [DataRow("<series>", "Series A, Series B, Series C, Series D")]
@ -388,6 +442,7 @@ namespace TemplatesTests
[DataRow("<first series>", "Series A")] [DataRow("<first series>", "Series A")]
[DataRow("<first series[]>", "Series A")] [DataRow("<first series[]>", "Series A")]
[DataRow("<first series[{N}, {#}, {ID}]>", "Series A, 1, B1")] [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) public void SeriesFormat_formatters(string template, string expected)
{ {
var bookDto = GetLibraryBook(); var bookDto = GetLibraryBook();
@ -406,6 +461,31 @@ namespace TemplatesTests
.Should().Be(expected); .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")]
[DataRow("<series#>", " 1 6 ", "1 6")]
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] [TestMethod]
[DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)] [DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)]
[DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)] [DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]
@ -496,7 +576,7 @@ namespace Templates_Other
var sb = new System.Text.StringBuilder(); var sb = new System.Text.StringBuilder();
sb.Append('0', 300); sb.Append('0', 300);
var longText = sb.ToString(); 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 private class TemplateTag : ITemplateTag

View File

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

View File

@ -1,20 +1,7 @@
using System; using AssertionHelper;
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 LibationSearchEngine; using LibationSearchEngine;
using Lucene.Net.Analysis.Standard; using Lucene.Net.Analysis.Standard;
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace SearchEngineTests namespace SearchEngineTests
{ {