Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74c76a7414 | ||
|
|
17a0c21453 | ||
|
|
fc9c9dfe48 | ||
|
|
d5f0e39981 | ||
|
|
0f6493f4af | ||
|
|
454b490a06 | ||
|
|
ffea2648aa | ||
|
|
1ac967500c | ||
|
|
ed5afe5d0f | ||
|
|
ab075d0bef | ||
|
|
7fb1adb41b | ||
|
|
9735a8391c | ||
|
|
dbdfdbc536 | ||
|
|
3b86fc405f | ||
|
|
4ea7f04921 | ||
|
|
5b59b442ab | ||
|
|
b5d9c0a27a | ||
|
|
f5cbf89e13 | ||
|
|
00dc9e020d | ||
|
|
bfa0e4d338 | ||
|
|
5ceda408da | ||
|
|
716b1923a4 |
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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'.
|
||||||
|
|||||||
2
.github/workflows/build-linux.yml
vendored
2
.github/workflows/build-linux.yml
vendored
@ -43,7 +43,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- 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:
|
||||||
|
|||||||
2
.github/workflows/build-windows.yml
vendored
2
.github/workflows/build-windows.yml
vendored
@ -44,7 +44,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- 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:
|
||||||
|
|||||||
@ -58,6 +58,11 @@ This is a proprietary codec created by the [Fraunhofer Institute for Integrated
|
|||||||
|
|
||||||
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)
|
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
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@ -37,9 +37,9 @@ Self-hosting online:
|
|||||||
|
|
||||||
## Q: I'm having trouble playing my non-spatial audiobook, how can I fix this?
|
## Q: I'm having trouble playing my non-spatial audiobook, how can I fix this?
|
||||||
|
|
||||||
**A:** If you enabled the [Use Widevine DRM](AudioFileFormats.md#use-widevine-drm) option in settings, the audiobook is most likely being downloaded in the [xHE-AAC codec](AudioFileFormats.md#xhe-aac) which isn't widely supported. You have two options:
|
**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).
|
1. Use a media player which supports the xHE-AAC codec. [See an incomplete list of media players which support xHE-AAC](AudioFileFormats.md#supported-media-players).
|
||||||
2. Disable [Use Widevine DRM](AudioFileFormats.md#use-widevine-drm) option in settings and re-download the audiobook. This will cause Libation to download audiobooks in the [AAC-LC codec](AudioFileFormats.md#aac-lc), which enjoys near-universal media player support.
|
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?
|
## Q: I'm having trouble playing my book with 4D, spatial audio, or Dolby Atmos, how can I fix this?
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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.1.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" />
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AudibleApi" Version="9.4.4.1" />
|
<PackageReference Include="AudibleApi" Version="9.4.5.1" />
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.32.0" />
|
<PackageReference Include="Google.Protobuf" Version="3.32.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="9.0.2.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.8">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="9.0.2.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>
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
@ -38,8 +38,8 @@ public class NamingTemplate
|
|||||||
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
|
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
|
||||||
// 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())}");
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
15
Source/LibationFileManager/Templates/CombinedDto.cs
Normal file
15
Source/LibationFileManager/Templates/CombinedDto.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,7 +28,7 @@ public class SeriesOrder : IFormattable
|
|||||||
while (TryParseNumber(order, out var value, out var range))
|
while (TryParseNumber(order, out var value, out var range))
|
||||||
{
|
{
|
||||||
var prefix = order[..range.Start.Value];
|
var prefix = order[..range.Start.Value];
|
||||||
if(!string.IsNullOrWhiteSpace(prefix))
|
if(!string.IsNullOrEmpty(prefix))
|
||||||
parts.Add(prefix);
|
parts.Add(prefix);
|
||||||
|
|
||||||
parts.Add(value);
|
parts.Add(value);
|
||||||
@ -36,7 +36,7 @@ public class SeriesOrder : IFormattable
|
|||||||
order = order[range.End.Value..];
|
order = order[range.End.Value..];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(order))
|
if (!string.IsNullOrEmpty(order))
|
||||||
parts.Add(order);
|
parts.Add(order);
|
||||||
|
|
||||||
return new(parts.ToArray());
|
return new(parts.ToArray());
|
||||||
@ -74,7 +74,7 @@ public class SeriesOrder : IFormattable
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var substring = numString[s..e];
|
var substring = numString[s..e];
|
||||||
if (float.TryParse(substring, out value))
|
if (float.TryParse(substring, System.Globalization.CultureInfo.InvariantCulture, out value))
|
||||||
{
|
{
|
||||||
range = new Range(s, e);
|
range = new Range(s, e);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -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>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="9.0.2.1" />
|
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="9.0.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="9.0.2.1" />
|
<PackageReference Include="Dinah.Core" Version="9.0.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ public static class AssertionExtensions
|
|||||||
|
|
||||||
[StackTraceHidden]
|
[StackTraceHidden]
|
||||||
public static void Be<T>(this T? value, T? expectedValue) where T : IEquatable<T>
|
public static void Be<T>(this T? value, T? expectedValue) where T : IEquatable<T>
|
||||||
=> Assert.AreEqual(value, expectedValue);
|
=> Assert.AreEqual(expectedValue, value);
|
||||||
|
|
||||||
[StackTraceHidden]
|
[StackTraceHidden]
|
||||||
public static void BeNull<T>(this T? value) where T : class
|
public static void BeNull<T>(this T? value) where T : class
|
||||||
@ -17,7 +17,7 @@ public static class AssertionExtensions
|
|||||||
|
|
||||||
[StackTraceHidden]
|
[StackTraceHidden]
|
||||||
public static void BeSameAs<T>(this T? value, T? otherValue)
|
public static void BeSameAs<T>(this T? value, T? otherValue)
|
||||||
=> Assert.AreSame(value, otherValue);
|
=> Assert.AreSame(otherValue, value);
|
||||||
|
|
||||||
[StackTraceHidden]
|
[StackTraceHidden]
|
||||||
public static void BeFalse(this bool value)
|
public static void BeFalse(this bool value)
|
||||||
@ -33,5 +33,5 @@ public static class AssertionExtensions
|
|||||||
|
|
||||||
[StackTraceHidden]
|
[StackTraceHidden]
|
||||||
public static void BeEquivalentTo<T>(this IEnumerable<T?>? value, IEnumerable<T?>? expectedValue)
|
public static void BeEquivalentTo<T>(this IEnumerable<T?>? value, IEnumerable<T?>? expectedValue)
|
||||||
=> CollectionAssert.AreEquivalent(value, expectedValue, EqualityComparer<T?>.Default);
|
=> CollectionAssert.AreEquivalent(expectedValue, value, EqualityComparer<T?>.Default);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>" })]
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using AaxDecrypter;
|
||||||
using AssertionHelper;
|
using AssertionHelper;
|
||||||
using FileManager;
|
using FileManager;
|
||||||
using FileManager.NamingTemplate;
|
using FileManager.NamingTemplate;
|
||||||
@ -52,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]
|
||||||
@ -373,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")]
|
||||||
@ -418,6 +473,7 @@ namespace TemplatesTests
|
|||||||
[DataRow("<series#[F2]>", " f1g ", "f1.00g")]
|
[DataRow("<series#[F2]>", " f1g ", "f1.00g")]
|
||||||
[DataRow("<series#[]>", "1", "1")]
|
[DataRow("<series#[]>", "1", "1")]
|
||||||
[DataRow("<series#>", "1", "1")]
|
[DataRow("<series#>", "1", "1")]
|
||||||
|
[DataRow("<series#>", " 1 6 ", "1 6")]
|
||||||
public void SeriesOrder_formatters(string template, string seriesOrder, string expected)
|
public void SeriesOrder_formatters(string template, string seriesOrder, string expected)
|
||||||
{
|
{
|
||||||
var bookDto = GetLibraryBook();
|
var bookDto = GetLibraryBook();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user