diff --git a/Documentation/NamingTemplates.md b/Documentation/NamingTemplates.md index b364fa04..d49852e1 100644 --- a/Documentation/NamingTemplates.md +++ b/Documentation/NamingTemplates.md @@ -81,17 +81,22 @@ Anything between the opening tag (``) and closing tag (`<-tagname>`) w |\...\<-if podcast\>|Only include if part of a podcast|Conditional| |\...\<-if bookseries\>|Only include if part of a book series|Conditional| |\...\<-if podcastparent\>**†**|Only include if item is a podcast series parent|Conditional| +|\...\<-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\> 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>` 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. -\Podcasts<-if podcast\>\Books\<-if podcast\>\\\ +`Podcasts<-if podcast>Books<-if podcast>\` +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. diff --git a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs index 1cefe52f..481d5a02 100644 --- a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs @@ -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; + } } } diff --git a/Source/FileManager/NamingTemplate/NamingTemplate.cs b/Source/FileManager/NamingTemplate/NamingTemplate.cs index 0c243398..f6beaba1 100644 --- a/Source/FileManager/NamingTemplate/NamingTemplate.cs +++ b/Source/FileManager/NamingTemplate/NamingTemplate.cs @@ -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())}"); diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs index 9f3acc75..04d349b5 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -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( diff --git a/Source/FileManager/NamingTemplate/TagBase.cs b/Source/FileManager/NamingTemplate/TagBase.cs index 264e48a3..47cccc14 100644 --- a/Source/FileManager/NamingTemplate/TagBase.cs +++ b/Source/FileManager/NamingTemplate/TagBase.cs @@ -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; } diff --git a/Source/FileManager/NamingTemplate/TagCollection.cs b/Source/FileManager/NamingTemplate/TagCollection.cs index 117c3a33..4cf3f725 100644 --- a/Source/FileManager/NamingTemplate/TagCollection.cs +++ b/Source/FileManager/NamingTemplate/TagCollection.cs @@ -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) { diff --git a/Source/LibationAvalonia/Dialogs/SearchSyntaxDialog.axaml b/Source/LibationAvalonia/Dialogs/SearchSyntaxDialog.axaml index 0202d9de..28bba33b 100644 --- a/Source/LibationAvalonia/Dialogs/SearchSyntaxDialog.axaml +++ b/Source/LibationAvalonia/Dialogs/SearchSyntaxDialog.axaml @@ -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> diff --git a/Source/LibationFileManager/Templates/CombinedDto.cs b/Source/LibationFileManager/Templates/CombinedDto.cs new file mode 100644 index 00000000..c72afdf8 --- /dev/null +++ b/Source/LibationFileManager/Templates/CombinedDto.cs @@ -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; + } +} diff --git a/Source/LibationFileManager/Templates/SeriesOrder.cs b/Source/LibationFileManager/Templates/SeriesOrder.cs index 08c4b281..2fe4edbf 100644 --- a/Source/LibationFileManager/Templates/SeriesOrder.cs +++ b/Source/LibationFileManager/Templates/SeriesOrder.cs @@ -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()); diff --git a/Source/LibationFileManager/Templates/TemplateTags.cs b/Source/LibationFileManager/Templates/TemplateTags.cs index 9c370983..ea1e7bac 100644 --- a/Source/LibationFileManager/Templates/TemplateTags.cs +++ b/Source/LibationFileManager/Templates/TemplateTags.cs @@ -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>"); } } diff --git a/Source/LibationFileManager/Templates/Templates.cs b/Source/LibationFileManager/Templates/Templates.cs index e67acd6f..d2a63a86 100644 --- a/Source/LibationFileManager/Templates/Templates.cs +++ b/Source/LibationFileManager/Templates/Templates.cs @@ -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); diff --git a/Source/LibationSearchEngine/SearchEngine.cs b/Source/LibationSearchEngine/SearchEngine.cs index 7d62c849..b922175b 100644 --- a/Source/LibationSearchEngine/SearchEngine.cs +++ b/Source/LibationSearchEngine/SearchEngine.cs @@ -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 diff --git a/Source/_Tests/AssertionHelper/AssertionExtensions.cs b/Source/_Tests/AssertionHelper/AssertionExtensions.cs index c314c614..351a42da 100644 --- a/Source/_Tests/AssertionHelper/AssertionExtensions.cs +++ b/Source/_Tests/AssertionHelper/AssertionExtensions.cs @@ -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); } diff --git a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs index 65dc1218..6e73efd4 100644 --- a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs +++ b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs @@ -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>" })] diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs index bbee3c80..ce77b342 100644 --- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -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();