diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs index 9efb9fb0..a79678d2 100644 --- a/Source/FileLiberator/DownloadOptions.cs +++ b/Source/FileLiberator/DownloadOptions.cs @@ -35,7 +35,11 @@ namespace FileLiberator public bool MoveMoovToBeginning => config.MoveMoovToBeginning; public string GetMultipartFileName(MultiConvertFileProperties props) - => Templates.ChapterFile.GetFilename(LibraryBookDto, props); + { + var baseDir = Path.GetDirectoryName(props.OutputFileName); + var extension = Path.GetExtension(props.OutputFileName); + return Templates.ChapterFile.GetFilename(LibraryBookDto, props, baseDir, extension); + } public string GetMultipartTitle(MultiConvertFileProperties props) => Templates.ChapterTitle.GetName(LibraryBookDto, props); diff --git a/Source/FileLiberator/UtilityExtensions.cs b/Source/FileLiberator/UtilityExtensions.cs index 22a728d9..b85eee0c 100644 --- a/Source/FileLiberator/UtilityExtensions.cs +++ b/Source/FileLiberator/UtilityExtensions.cs @@ -40,7 +40,7 @@ namespace FileLiberator Narrators = libraryBook.Book.Narrators.Select(c => c.Name).ToList(), SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name, - SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Order, + SeriesNumber = (int?)libraryBook.Book.SeriesLink.FirstOrDefault()?.Index, IsPodcast = libraryBook.Book.IsEpisodeChild(), BitRate = libraryBook.Book.AudioFormat.Bitrate, diff --git a/Source/FileManager/NamingTemplate/NamingTemplate.cs b/Source/FileManager/NamingTemplate/NamingTemplate.cs index 4d5016da..17ee7ec3 100644 --- a/Source/FileManager/NamingTemplate/NamingTemplate.cs +++ b/Source/FileManager/NamingTemplate/NamingTemplate.cs @@ -47,7 +47,7 @@ public class NamingTemplate /// Parse a template string to a /// The template string to parse - /// A collection of with + /// A collection of with /// properties registered to match to the public static NamingTemplate Parse(string template, IEnumerable tagClasses) { @@ -111,10 +111,10 @@ public class NamingTemplate checkAndAddLiterals(); if (propertyTag is IClosingPropertyTag) - currentNode = AddNewNode(currentNode, BinaryNode.CreateConditional(propertyTag.TemplateTag, valueExpression)); + currentNode = currentNode.AddNewNode(BinaryNode.CreateConditional(propertyTag.TemplateTag, valueExpression)); else { - currentNode = AddNewNode(currentNode, BinaryNode.CreateValue(propertyTag.TemplateTag, valueExpression)); + currentNode = currentNode.AddNewNode(BinaryNode.CreateValue(propertyTag.TemplateTag, valueExpression)); _tagsInUse.Add(propertyTag.TemplateTag); } @@ -170,7 +170,7 @@ public class NamingTemplate { if (literalChars.Count != 0) { - currentNode = AddNewNode(currentNode, BinaryNode.CreateValue(new string(literalChars.ToArray()))); + currentNode = currentNode.AddNewNode(BinaryNode.CreateValue(new string(literalChars.ToArray()))); literalChars.Clear(); } } @@ -201,34 +201,12 @@ public class NamingTemplate return false; } - private static BinaryNode AddNewNode(BinaryNode currentNode, BinaryNode newNode) - { - if (currentNode.LeftChild is null) - { - newNode.Parent = currentNode; - currentNode.LeftChild = newNode; - } - else if (currentNode.RightChild is null) - { - newNode.Parent = currentNode; - currentNode.RightChild = newNode; - } - else - { - currentNode.RightChild = BinaryNode.CreateConcatenation(currentNode.RightChild, newNode); - currentNode.RightChild.Parent = currentNode; - currentNode = currentNode.RightChild; - } - - return newNode.IsConditional ? newNode : currentNode; - } - private class BinaryNode { public string Name { get; } - public BinaryNode Parent { get; set; } - public BinaryNode RightChild { get; set; } - public BinaryNode LeftChild { get; set; } + public BinaryNode Parent { get; private set; } + public BinaryNode RightChild { get; private set; } + public BinaryNode LeftChild { get; private set; } public Expression Expression { get; private init; } public bool IsConditional { get; private init; } = false; public bool IsValue { get; private init; } = false; @@ -253,7 +231,7 @@ public class NamingTemplate Expression = property }; - public static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right) + private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right) { var newNode = new BinaryNode("Concatenation") { @@ -267,5 +245,29 @@ public class NamingTemplate private BinaryNode(string name) => Name = name; public override string ToString() => Name; + + public BinaryNode AddNewNode(BinaryNode newNode) + { + BinaryNode currentNode = this; + + if (LeftChild is null) + { + newNode.Parent = currentNode; + LeftChild = newNode; + } + else if (RightChild is null) + { + newNode.Parent = currentNode; + RightChild = newNode; + } + else + { + RightChild = CreateConcatenation(RightChild, newNode); + RightChild.Parent = currentNode; + currentNode = RightChild; + } + + return newNode.IsConditional ? newNode : currentNode; + } } } diff --git a/Source/FileManager/NamingTemplate/PropertyTagClass[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagClass[TClass].cs index 1d53f7ae..04eac040 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagClass[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagClass[TClass].cs @@ -19,7 +19,7 @@ public class PropertyTagClass : TagClass /// Optional formatting function that accepts the property and a formatting string and returnes the value formatted to string public void RegisterProperty(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) where U : struct - => RegisterProperty(templateTag, propertyGetter, formatter?.Method); + => RegisterPropertyInternal(templateTag, propertyGetter, formatter); /// /// Register a non-nullable value type property @@ -29,7 +29,7 @@ public class PropertyTagClass : TagClass /// Optional formatting function that accepts the property and a formatting string and returnes the value formatted to string public void RegisterProperty(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) where U : struct - => RegisterProperty(templateTag, propertyGetter, formatter?.Method); + => RegisterPropertyInternal(templateTag, propertyGetter, formatter); /// /// Register a string type property. @@ -37,13 +37,16 @@ public class PropertyTagClass : TagClass /// A Func to get the string property from /// Optional formatting function that accepts the string property and a formatting string and returnes the value formatted to string public void RegisterProperty(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) - => RegisterProperty(templateTag, propertyGetter, formatter?.Method); + => RegisterPropertyInternal(templateTag, propertyGetter, formatter); - private void RegisterProperty(ITemplateTag templateTag, Delegate propertyGetter, MethodInfo formatter) + private void RegisterPropertyInternal(ITemplateTag templateTag, Delegate propertyGetter, Delegate formatter) { + if (formatter?.Target is not null) + throw new ArgumentException($"{nameof(formatter)} must be a static method"); + var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); - AddPropertyTag(new PropertyTag(templateTag, Options, expr, formatter)); + AddPropertyTag(new PropertyTag(templateTag, Options, expr, formatter?.Method)); } private class PropertyTag : TagBase diff --git a/Source/LibationFileManager/LibraryBookDto.cs b/Source/LibationFileManager/LibraryBookDto.cs index 60114040..859f18c9 100644 --- a/Source/LibationFileManager/LibraryBookDto.cs +++ b/Source/LibationFileManager/LibraryBookDto.cs @@ -20,7 +20,8 @@ namespace LibationFileManager public string FirstNarrator => Narrators.FirstOrDefault(); public string SeriesName { get; set; } - public string SeriesNumber { get; set; } + public int? SeriesNumber { get; set; } + public bool IsSeries => !string.IsNullOrEmpty(SeriesName); public bool IsPodcast { get; set; } public int BitRate { get; set; } diff --git a/Source/LibationFileManager/TemplateEditor[T].cs b/Source/LibationFileManager/TemplateEditor[T].cs index d20663b9..a7226de2 100644 --- a/Source/LibationFileManager/TemplateEditor[T].cs +++ b/Source/LibationFileManager/TemplateEditor[T].cs @@ -58,7 +58,7 @@ namespace LibationFileManager Authors = new List { "Arthur Conan Doyle", "Stephen Fry - introductions" }, Narrators = new List { "Stephen Fry" }, SeriesName = "Sherlock Holmes", - SeriesNumber = "1", + SeriesNumber = 1, BitRate = 128, SampleRate = 44100, Channels = 2, diff --git a/Source/LibationFileManager/TemplateTags.cs b/Source/LibationFileManager/TemplateTags.cs index 879363a0..8adcd4d6 100644 --- a/Source/LibationFileManager/TemplateTags.cs +++ b/Source/LibationFileManager/TemplateTags.cs @@ -45,7 +45,8 @@ namespace LibationFileManager public static TemplateTags FileDate { get; } = new TemplateTags("file date", "File date/time. e.g. yyyy-MM-dd HH-mm", $"", ""); public static TemplateTags DatePublished { get; } = new TemplateTags("pub date", "Publication date. e.g. yyyy-MM-dd", $"", ""); public static TemplateTags DateAdded { get; } = new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"", ""); - public static TemplateTags IfSeries { get; } = new TemplateTags("if series", "Only include if part of a series", "<-if series>", "...<-if series>"); + public static TemplateTags IfSeries { get; } = new TemplateTags("if series", "Only include if part of a book series or podcast", "<-if series>", "...<-if series>"); public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<-if podcast>", "...<-if podcast>"); + public static TemplateTags IfBookseries { get; } = new TemplateTags("if bookseries", "Only include if part of a book series", "<-if bookseries>", "...<-if bookseries>"); } } diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index d42e70a0..e62a62b2 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -6,6 +6,7 @@ using AaxDecrypter; using Dinah.Core; using FileManager; using FileManager.NamingTemplate; +using Serilog.Formatting; namespace LibationFileManager { @@ -105,24 +106,24 @@ namespace LibationFileManager ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension)); replacements ??= Configuration.Instance.ReplacementCharacters; - return GetFilename(baseDir, fileExtension, returnFirstExisting, replacements, libraryBookDto); + return GetFilename(baseDir, fileExtension,replacements, returnFirstExisting, libraryBookDto); } - public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir = "", string fileExtension = null, ReplacementCharacters replacements = null) + public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false) { ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps)); ArgumentValidator.EnsureNotNull(baseDir, nameof(baseDir)); + ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension)); replacements ??= Configuration.Instance.ReplacementCharacters; - fileExtension ??= Path.GetExtension(multiChapProps.OutputFileName); - return GetFilename(baseDir, fileExtension, false, replacements, libraryBookDto, multiChapProps); + return GetFilename(baseDir, fileExtension, replacements, returnFirstExisting, libraryBookDto, multiChapProps); } protected virtual IEnumerable GetTemplatePartsStrings(List parts, ReplacementCharacters replacements) => parts.Select(p => replacements.ReplaceFilenameChars(p.Value)); - private LongPath GetFilename(string baseDir, string fileExtension, bool returnFirstExisting, ReplacementCharacters replacements, params object[] dtos) + private LongPath GetFilename(string baseDir, string fileExtension, ReplacementCharacters replacements, bool returnFirstExisting, params object[] dtos) { fileExtension = FileUtility.GetStandardizedExtension(fileExtension); @@ -151,14 +152,15 @@ namespace LibationFileManager part.Insert(maxIndex, maxEntry.Remove(maxLength - 1, 1)); } } - - var fullPath = Path.Combine(pathParts.Select(p => string.Join("", p)).Prepend(baseDir).ToArray()); + //Any + var fullPath = Path.Combine(pathParts.Select(fileParts => string.Join("", fileParts)).Prepend(baseDir).ToArray()); return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting); } /// - /// Organize template parts into directories. + /// Organize template parts into directories. Any Extra slashes will be + /// returned as empty directories and are taken care of by Path.Combine() /// /// A List of template directories. Each directory is a list of template part strings private List> GetPathParts(IEnumerable templateParts) @@ -196,8 +198,9 @@ namespace LibationFileManager { ConditionalTagClass lbConditions = new(); - lbConditions.RegisterCondition(TemplateTags.IfSeries, lb => !string.IsNullOrWhiteSpace(lb.SeriesName)); + lbConditions.RegisterCondition(TemplateTags.IfSeries, lb => lb.IsSeries); lbConditions.RegisterCondition(TemplateTags.IfPodcast, lb => lb.IsPodcast); + lbConditions.RegisterCondition(TemplateTags.IfBookseries, lb => lb.IsSeries && !lb.IsPodcast); return lbConditions; } @@ -206,16 +209,16 @@ namespace LibationFileManager { PropertyTagClass lbProperties = new(); lbProperties.RegisterProperty(TemplateTags.Id, lb => lb.AudibleProductId); - lbProperties.RegisterProperty(TemplateTags.Title, lb => lb.Title ?? "", StringFormatter); + lbProperties.RegisterProperty(TemplateTags.Title, lb => lb.Title, StringFormatter); lbProperties.RegisterProperty(TemplateTags.TitleShort, lb => lb.Title.IndexOf(':') < 1 ? lb.Title : lb.Title.Substring(0, lb.Title.IndexOf(':')), StringFormatter); lbProperties.RegisterProperty(TemplateTags.Author, lb => lb.AuthorNames, StringFormatter); lbProperties.RegisterProperty(TemplateTags.FirstAuthor, lb => lb.FirstAuthor, StringFormatter); lbProperties.RegisterProperty(TemplateTags.Narrator, lb => lb.NarratorNames, StringFormatter); lbProperties.RegisterProperty(TemplateTags.FirstNarrator, lb => lb.FirstNarrator, StringFormatter); - lbProperties.RegisterProperty(TemplateTags.Series, lb => lb.SeriesName ?? "", StringFormatter); - lbProperties.RegisterProperty(TemplateTags.SeriesNumber, lb => lb.SeriesNumber); - lbProperties.RegisterProperty(TemplateTags.Language, lb => lb.Language); - lbProperties.RegisterProperty(TemplateTags.LanguageShort, lb => getLanguageShort(lb.Language)); + lbProperties.RegisterProperty(TemplateTags.Series, lb => lb.SeriesName, StringFormatter); + lbProperties.RegisterProperty(TemplateTags.SeriesNumber, lb => lb.SeriesNumber, IntegerFormatter); + lbProperties.RegisterProperty(TemplateTags.Language, lb => lb.Language, StringFormatter); + lbProperties.RegisterProperty(TemplateTags.LanguageShort, lb => getLanguageShort(lb.Language), StringFormatter); lbProperties.RegisterProperty(TemplateTags.Bitrate, lb => lb.BitRate, IntegerFormatter); lbProperties.RegisterProperty(TemplateTags.SampleRate, lb => lb.SampleRate, IntegerFormatter); lbProperties.RegisterProperty(TemplateTags.Channels, lb => lb.Channels, IntegerFormatter); @@ -233,14 +236,15 @@ namespace LibationFileManager PropertyTagClass lbProperties = new(); PropertyTagClass multiConvertProperties = new(); - lbProperties.RegisterProperty(TemplateTags.Title, lb => lb.Title ?? ""); - lbProperties.RegisterProperty(TemplateTags.TitleShort, lb => lb.Title.IndexOf(':') < 1 ? lb.Title : lb.Title.Substring(0, lb.Title.IndexOf(':'))); - lbProperties.RegisterProperty(TemplateTags.Series, lb => lb.SeriesName ?? ""); + lbProperties.RegisterProperty(TemplateTags.Title, lb => lb.Title, StringFormatter); + lbProperties.RegisterProperty(TemplateTags.TitleShort, lb => lb?.Title?.IndexOf(':') > 0 ? lb.Title.Substring(0, lb.Title.IndexOf(':')) : lb.Title, StringFormatter); + lbProperties.RegisterProperty(TemplateTags.Series, lb => lb.SeriesName, StringFormatter); multiConvertProperties.RegisterProperty(TemplateTags.ChCount, lb => lb.PartsTotal, IntegerFormatter); multiConvertProperties.RegisterProperty(TemplateTags.ChNumber, lb => lb.PartsPosition, IntegerFormatter); multiConvertProperties.RegisterProperty(TemplateTags.ChNumber0, m => m.PartsPosition.ToString("D" + ((int)Math.Log10(m.PartsTotal) + 1))); - multiConvertProperties.RegisterProperty(TemplateTags.ChTitle, m => m.Title ?? "", StringFormatter); + multiConvertProperties.RegisterProperty(TemplateTags.ChTitle, m => m.Title, StringFormatter); + multiConvertProperties.RegisterProperty(TemplateTags.FileDate, lb => lb.FileDate, DateTimeFormatter); return new List { lbProperties, multiConvertProperties }; } @@ -303,15 +307,6 @@ namespace LibationFileManager else tp.Value = replacements.ReplaceFilenameChars(tp.Value); } - if (parts.Count > 0) - { - //Remove DirectorySeparatorChar at beginning and end of template - if (parts[0].Value.Length > 0 && parts[0].Value[0] == Path.DirectorySeparatorChar) - parts[0].Value = parts[0].Value.Remove(0,1); - - if (parts[^1].Value.Length > 0 && parts[^1].Value[^1] == Path.DirectorySeparatorChar) - parts[^1].Value = parts[^1].Value.Remove(parts[^1].Value.Length - 1, 1); - } return parts.Select(p => p.Value).ToList(); } } diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs index 46398993..394c7ecc 100644 --- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -37,7 +37,7 @@ namespace TemplatesTests Authors = new List { "Arthur Conan Doyle", "Stephen Fry - introductions" }, Narrators = new List { "Stephen Fry" }, SeriesName = seriesName ?? "", - SeriesNumber = "1", + SeriesNumber = 1, BitRate = 128, SampleRate = 44100, Channels = 2, @@ -297,8 +297,8 @@ namespace Templates_Other static ReplacementCharacters Replacements = ReplacementCharacters.Default; [TestMethod] - [DataRow(@"C:\foo\bar", @"\Folder\\[<id>]\", @"C:\foo\bar\Folder\my꞉ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\[ID123456].txt", PlatformID.Win32NT)] - [DataRow("/foo/bar", "/Folder/<title>/[<id>]/", @" / foo/bar/Folder/my: book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/[ID123456].txt", PlatformID.Unix)] + [DataRow(@"C:\foo\bar", @"\\Folder\<title>\[<id>]\\", @"C:\foo\bar\Folder\my꞉ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\[ID123456].txt", PlatformID.Win32NT)] + [DataRow("/foo/bar", "/Folder/<title>/[<id>]/", @"/foo/bar/Folder/my: book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/[ID123456].txt", PlatformID.Unix)] [DataRow(@"C:\foo\bar", @"\Folder\<title> [<id>]", @"C:\foo\bar\Folder\my꞉ book 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt", PlatformID.Win32NT)] [DataRow("/foo/bar", "/Folder/<title> [<id>]", @"/foo/bar/Folder/my: book 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt", PlatformID.Unix)] [DataRow(@"C:\foo\bar", @"\Folder\<title> <title> <title> <title> <title> <title> <title> <title> <title> [<id>]", @"C:\foo\bar\Folder\my꞉ book 0000000000000000 my꞉ book 0000000000000000 my꞉ book 0000000000000000 my꞉ book 0000000000000000 my꞉ book 0000000000000000 my꞉ book 0000000000000000 my꞉ book 0000000000000000 my꞉ book 00000000000000000 my꞉ book 00000000000000000 [ID123456].txt", PlatformID.Win32NT)]