Merge pull request #1342 from Mbucari/master

Added new <has PROPERTY-><-has> conditional tag
This commit is contained in:
rmcrackan 2025-08-20 08:37:11 -04:00 committed by GitHub
commit dbdfdbc536
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 281 additions and 36 deletions

View File

@ -81,17 +81,22 @@ Anything between the opening tag (`<tagname->`) and closing tag (`<-tagname>`) w
|\<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 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.
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.
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>`
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
**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.

View File

@ -22,6 +22,8 @@ internal interface IClosingPropertyTag : IPropertyTag
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 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>
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));
}
/// <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
{
public override Regex NameMatcher { get; }
public Regex NameCloseMatcher { get; }
private Func<string?, Expression> CreateConditionExpression { get; }
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression)
: base(templateTag, conditionExpression)
{
NameMatcher = new Regex($"^<(!)?{templateTag.TagName}->", options);
NameMatcher = 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)
@ -64,6 +94,13 @@ public class ConditionalTagCollection<TClass> : TagCollection
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"/>
/// </summary>
/// <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)
throw new InvalidOperationException();
@ -38,8 +38,8 @@ public class NamingTemplate
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
// First parameter is "this", so ignore it.
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())
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 System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
@ -109,6 +110,25 @@ public class PropertyTagCollection<TClass> : TagCollection
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
{
public override Regex NameMatcher { get; }
@ -138,8 +158,13 @@ public class PropertyTagCollection<TClass> : TagCollection
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
= !ReturnType.IsValueType
? Expression.Condition(

View File

@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
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>
/// <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>
protected abstract Expression GetTagExpression(string exactName, string formatter);
/// <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[] extraData);
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)
{
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;
}

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>
internal ParameterExpression Parameter { get; }
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)
{

View File

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

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

@ -28,7 +28,7 @@ public class SeriesOrder : IFormattable
while (TryParseNumber(order, out var value, out var range))
{
var prefix = order[..range.Start.Value];
if(!string.IsNullOrWhiteSpace(prefix))
if(!string.IsNullOrEmpty(prefix))
parts.Add(prefix);
parts.Add(value);
@ -36,7 +36,7 @@ public class SeriesOrder : IFormattable
order = order[range.End.Value..];
}
if (!string.IsNullOrWhiteSpace(order))
if (!string.IsNullOrEmpty(order))
parts.Add(order);
return new(parts.ToArray());

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 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 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(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)
@ -138,11 +138,11 @@ namespace LibationFileManager.Templates
protected virtual IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
=> 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);
var parts = NamingTemplate.Evaluate(dtos).ToList();
var parts = NamingTemplate.Evaluate(lbDto, multiDto, new CombinedDto(lbDto, multiDto)).ToList();
var pathParts = GetPathParts(GetTemplatePartsStrings(parts, replacements));
//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 },
};
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()
{
{ TemplateTags.IfPodcastParent, lb => lb.IsPodcastParent }
@ -388,7 +417,7 @@ namespace LibationFileManager.Templates
public static string Name { get; } = "Folder Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? "";
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
=> 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 Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)) ?? "";
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
@ -416,7 +445,7 @@ namespace LibationFileManager.Templates
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
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
=> 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 Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)) ?? "";
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)
=> 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.Locale, "Locale", "Region" },
{ 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.UserDefinedItem.Rating.OverallRating > 0f).ToString(), "IsRated", "Rated" },
{ 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.DatePublished?.ToLuceneString() ?? "", nameof(Book.DatePublished) },
{ 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

View File

@ -9,7 +9,7 @@ public static class AssertionExtensions
[StackTraceHidden]
public static void Be<T>(this T? value, T? expectedValue) where T : IEquatable<T>
=> Assert.AreEqual(value, expectedValue);
=> Assert.AreEqual(expectedValue, value);
[StackTraceHidden]
public static void BeNull<T>(this T? value) where T : class
@ -17,7 +17,7 @@ public static class AssertionExtensions
[StackTraceHidden]
public static void BeSameAs<T>(this T? value, T? otherValue)
=> Assert.AreSame(value, otherValue);
=> Assert.AreSame(otherValue, value);
[StackTraceHidden]
public static void BeFalse(this bool value)
@ -33,5 +33,5 @@ public static class AssertionExtensions
[StackTraceHidden]
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);
}

View File

@ -15,6 +15,7 @@ namespace NamingTemplateTests
public string Item1 { get; set; }
public string Item2 { get; set; }
public string Item3 { get; set; }
public string NullItem { get; set; }
public int Int1 { get; set; }
public bool Condition { get; set; }
}
@ -25,6 +26,7 @@ namespace NamingTemplateTests
public string Item2 { get; set; }
public string Item3 { get; set; }
public string Item4 { get; set; }
public string NullItem { get; set; }
public bool Condition { get; set; }
}
class PropertyClass3
@ -33,6 +35,7 @@ namespace NamingTemplateTests
public string Item2 { get; set; }
public string Item3 { get; set; }
public string Item4 { get; set; }
public string NullItem { get; set; }
public ReferenceType RefType { get; set; }
public int? Int2 { get; set; }
public bool Condition { get; set; }
@ -49,41 +52,54 @@ namespace NamingTemplateTests
[TestClass]
public class GetPortionFilename
{
PropertyTagCollection<PropertyClass1> props1 = new()
static PropertyTagCollection<PropertyClass1> props1 = new()
{
{ new TemplateTag { TagName = "item1" }, i => i.Item1 },
{ 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 = "item2" }, i => i.Item2 },
{ new TemplateTag { TagName = "item3" }, i => i.Item3 },
{ 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_2" }, i => i.Item2 },
{ new TemplateTag { TagName = "item3_3" }, i => i.Item3 },
{ new TemplateTag { TagName = "item3_4" }, i => i.Item4 },
{ new TemplateTag { TagName = "null_3" }, i => i.NullItem },
{ new TemplateTag { TagName = "reftype" }, i => i.RefType },
};
ConditionalTagCollection<PropertyClass1> conditional1 = new()
{
{ new TemplateTag { TagName = "ifc1" }, i => i.Condition },
{ new TemplateTag { TagName = "has1" }, HasValue }
};
ConditionalTagCollection<PropertyClass2> conditional2 = new()
{
{ new TemplateTag { TagName = "ifc2" }, i => i.Condition },
{ new TemplateTag { TagName = "has2" }, HasValue }
};
ConditionalTagCollection<PropertyClass3> conditional3 = new()
{
{ 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()
{
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("<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("<!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)
{
var template = NamingTemplate.Parse(inStr, new TagCollection[] { props1, props2, props3, conditional1, conditional2, conditional3 });
@ -136,8 +154,63 @@ namespace NamingTemplateTests
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]
[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("<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>" })]

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AaxDecrypter;
using AssertionHelper;
using FileManager;
using FileManager.NamingTemplate;
@ -52,8 +53,13 @@ namespace TemplatesTests
BitRate = 128,
SampleRate = 44100,
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]
@ -373,6 +379,55 @@ namespace TemplatesTests
.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]
[DataRow("<series>", "Series A, Series B, Series C, Series D")]
@ -418,6 +473,7 @@ namespace TemplatesTests
[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();