From 3479dbc3f056a54db82dae276c80e3442c79c4ba Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Fri, 20 Jan 2023 00:37:59 -0700 Subject: [PATCH] Date format naming templates --- .../MultiConvertFileProperties.cs | 2 +- Source/FileLiberator/UtilityExtensions.cs | 2 + Source/FileManager/FileNamingTemplate.cs | 12 +- .../Dialogs/EditTemplateDialog.axaml.cs | 17 +- Source/LibationFileManager/LibraryBookDto.cs | 10 +- Source/LibationFileManager/TemplateTags.cs | 15 +- Source/LibationFileManager/Templates.cs | 113 ++++++++++--- .../LibationFileManager/UtilityExtensions.cs | 3 + .../Dialogs/EditTemplateDialog.cs | 12 +- .../FileNamingTemplateTests.cs | 12 +- .../TemplatesTests.cs | 153 ++++++++++++++++-- 11 files changed, 291 insertions(+), 60 deletions(-) diff --git a/Source/AaxDecrypter/MultiConvertFileProperties.cs b/Source/AaxDecrypter/MultiConvertFileProperties.cs index 71bafe21..1618884b 100644 --- a/Source/AaxDecrypter/MultiConvertFileProperties.cs +++ b/Source/AaxDecrypter/MultiConvertFileProperties.cs @@ -10,6 +10,6 @@ namespace AaxDecrypter public int PartsPosition { get; set; } public int PartsTotal { get; set; } public string Title { get; set; } - + public DateTime FileDate { get; } = DateTime.Now; } } diff --git a/Source/FileLiberator/UtilityExtensions.cs b/Source/FileLiberator/UtilityExtensions.cs index 37fba4c8..d1820922 100644 --- a/Source/FileLiberator/UtilityExtensions.cs +++ b/Source/FileLiberator/UtilityExtensions.cs @@ -27,11 +27,13 @@ namespace FileLiberator public static LibraryBookDto ToDto(this LibraryBook libraryBook) => new() { Account = libraryBook.Account, + DateAdded = libraryBook.DateAdded, AudibleProductId = libraryBook.Book.AudibleProductId, Title = libraryBook.Book.Title ?? "", Locale = libraryBook.Book.Locale, YearPublished = libraryBook.Book.DatePublished?.Year, + DatePublished = libraryBook.Book.DatePublished, Authors = libraryBook.Book.Authors.Select(c => c.Name).ToList(), diff --git a/Source/FileManager/FileNamingTemplate.cs b/Source/FileManager/FileNamingTemplate.cs index 29bde7e6..2ce85c0d 100644 --- a/Source/FileManager/FileNamingTemplate.cs +++ b/Source/FileManager/FileNamingTemplate.cs @@ -9,11 +9,15 @@ namespace FileManager /// Get valid filename. Advanced features incl. parameterized template public class FileNamingTemplate : NamingTemplate { + public ReplacementCharacters ReplacementCharacters { get; } /// Proposed file name with optional html-styled template tags. - public FileNamingTemplate(string template) : base(template) { } + public FileNamingTemplate(string template, ReplacementCharacters replacement) : base(template) + { + ReplacementCharacters = replacement ?? ReplacementCharacters.Default; + } /// Generate a valid path for this file or directory - public LongPath GetFilePath(ReplacementCharacters replacements, string fileExtension, bool returnFirstExisting = false) + public LongPath GetFilePath(string fileExtension, bool returnFirstExisting = false) { string fileName = Template.EndsWith(Path.DirectorySeparatorChar) || Template.EndsWith(Path.AltDirectorySeparatorChar) ? @@ -22,7 +26,7 @@ namespace FileManager List pathParts = new(); - var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, replacements)); + var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, ReplacementCharacters)); while (!string.IsNullOrEmpty(fileName)) { @@ -54,7 +58,7 @@ namespace FileManager return FileUtility .GetValidFilename( Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - fileExtension.Length - 5)) + fileExtension, - replacements, + ReplacementCharacters, fileExtension, returnFirstExisting ); diff --git a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs index 14c7ce99..5bbf9eb4 100644 --- a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs @@ -50,7 +50,9 @@ namespace LibationAvalonia.Dialogs { var dataGrid = sender as DataGrid; - var item = (dataGrid.SelectedItem as Tuple).Item1.Replace("\x200C", "").Replace("...", ""); + var item = (dataGrid.SelectedItem as Tuple).Item3; + if (string.IsNullOrWhiteSpace(item)) return; + var text = userEditTbox.Text; userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item); @@ -84,13 +86,14 @@ namespace LibationAvalonia.Dialogs Template = templates; Description = templates.Description; ListItems - = new AvaloniaList>( + = new AvaloniaList>( Template .GetTemplateTags() .Select( - t => new Tuple( + t => new Tuple( $"<{t.TagName.Replace("->", "-\x200C>").Replace("<-", "<\x200C-")}>", - t.Description) + t.Description, + t.DefaultValue) ) ); @@ -108,13 +111,13 @@ namespace LibationAvalonia.Dialogs } } - public string workingTemplateText => Template.Sanitize(UserTemplateText); + public string workingTemplateText => Template.Sanitize(UserTemplateText, Configuration.Instance.ReplacementCharacters); private string _warningText; public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); } public string Description { get; } - public AvaloniaList> ListItems { get; set; } + public AvaloniaList> ListItems { get; set; } public void resetTextBox(string value) => UserTemplateText = value; @@ -138,6 +141,8 @@ namespace LibationAvalonia.Dialogs var libraryBookDto = new LibraryBookDto { Account = "my account", + DateAdded = new DateTime(2022, 6, 9, 0, 0, 0), + DatePublished = new DateTime(2017, 2, 27, 0, 0, 0), AudibleProductId = "123456789", Title = "A Study in Scarlet: A Sherlock Holmes Novel", Locale = "us", diff --git a/Source/LibationFileManager/LibraryBookDto.cs b/Source/LibationFileManager/LibraryBookDto.cs index d24d7ef7..31fc1565 100644 --- a/Source/LibationFileManager/LibraryBookDto.cs +++ b/Source/LibationFileManager/LibraryBookDto.cs @@ -25,10 +25,14 @@ namespace LibationFileManager public int BitRate { get; set; } public int SampleRate { get; set; } public int Channels { get; set; } - } + public DateTime FileDate { get; set; } = DateTime.Now; + public DateTime? DatePublished { get; set; } + + } public class LibraryBookDto : BookDto - { - public string Account { get; set; } + { + public DateTime? DateAdded { get; set; } + public string Account { get; set; } } } diff --git a/Source/LibationFileManager/TemplateTags.cs b/Source/LibationFileManager/TemplateTags.cs index 764f1ea8..e4146d28 100644 --- a/Source/LibationFileManager/TemplateTags.cs +++ b/Source/LibationFileManager/TemplateTags.cs @@ -7,16 +7,19 @@ namespace LibationFileManager { public sealed class TemplateTags : Enumeration { - public string TagName => DisplayName; + public string TagName => DisplayName; + public string DefaultValue { get; } public string Description { get; } public bool IsChapterOnly { get; } private static int value = 0; - private TemplateTags(string tagName, string description, bool isChapterOnly = false) : base(value++, tagName) + private TemplateTags(string tagName, string description, bool isChapterOnly = false, string defaultValue = null) : base(value++, tagName) { Description = description; IsChapterOnly = isChapterOnly; - } + DefaultValue = defaultValue ?? $"<{tagName}>"; + + } // putting these first is the incredibly lazy way to make them show up first in the EditTemplateDialog public static TemplateTags ChCount { get; } = new TemplateTags("ch count", "Number of chapters", true); @@ -43,7 +46,9 @@ namespace LibationFileManager // Special cases. Aren't mapped to replacements in Templates.cs // Included here for display by EditTemplateDialog - public static TemplateTags Date { get; } = new TemplateTags("date[...]", "File date/time. e.g. yyyy-MM-dd HH-mm"); - public static TemplateTags IfSeries { get; } = new TemplateTags("if series->...<-if series", "Only include if part of a series"); + public static TemplateTags FileDate { get; } = new TemplateTags("file date [...]", "File date/time. e.g. yyyy-MM-dd HH-mm", false, $""); + public static TemplateTags DatePublished { get; } = new TemplateTags("pub date [...]", "Publication date. e.g. yyyy-MM-dd", false, $""); + public static TemplateTags DateAdded { get; } = new TemplateTags("date added [...]", "Date added to you Audible account. e.g. yyyy-MM-dd", false, $""); + public static TemplateTags IfSeries { get; } = new TemplateTags("if series->...<-if series", "Only include if part of a series", false, "<-if series>"); } } diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index 6047cfe9..19dc62d9 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using Dinah.Core; +using Dinah.Core.Collections.Generic; using FileManager; namespace LibationFileManager @@ -102,17 +103,21 @@ namespace LibationFileManager public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, string fileExtension) => string.IsNullOrWhiteSpace(template) ? "" - : getFileNamingTemplate(libraryBookDto, template, null, fileExtension) - .GetFilePath(Configuration.Instance.ReplacementCharacters, fileExtension).PathWithoutPrefix; + : getFileNamingTemplate(libraryBookDto, template, null, fileExtension, Configuration.Instance.ReplacementCharacters) + .GetFilePath(fileExtension).PathWithoutPrefix; - private static Regex dateFormatRegex { get; } = new Regex(@"", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public const string DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; + private static Regex fileDateTagRegex { get; } = new Regex(@"", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static Regex dateAddedTagRegex { get; } = new Regex(@"", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static Regex datePublishedTagRegex { get; } = new Regex(@"", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static Regex ifSeriesRegex { get; } = new Regex("(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase); - internal static FileNamingTemplate getFileNamingTemplate(LibraryBookDto libraryBookDto, string template, string dirFullPath, string extension) + internal static FileNamingTemplate getFileNamingTemplate(LibraryBookDto libraryBookDto, string template, string dirFullPath, string extension, ReplacementCharacters replacements) { ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template)); ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); + replacements ??= Configuration.Instance.ReplacementCharacters; dirFullPath = dirFullPath?.Trim() ?? ""; // for non-series, remove and <-if series> tags and everything in between @@ -121,12 +126,16 @@ namespace LibationFileManager template, string.IsNullOrWhiteSpace(libraryBookDto.SeriesName) ? "" : "$1"); - template = dateFormatRegex.Replace(template, dateMatchEvaluator); + //Get date replacement parameters. Sanitizes the format text and replaces + //the template with the sanitized text before creating FileNamingTemplate + var fileDateParams = getSanitizeDateReplacementParameters(fileDateTagRegex, ref template, replacements, libraryBookDto.FileDate); + var dateAddedParams = getSanitizeDateReplacementParameters(dateAddedTagRegex, ref template, replacements, libraryBookDto.DateAdded); + var pubDateParams = getSanitizeDateReplacementParameters(datePublishedTagRegex, ref template, replacements, libraryBookDto.DatePublished); var t = template + FileUtility.GetStandardizedExtension(extension); var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t); - var fileNamingTemplate = new FileNamingTemplate(fullfilename); + var fileNamingTemplate = new FileNamingTemplate(fullfilename, replacements); var title = libraryBookDto.Title ?? ""; var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':')); @@ -147,31 +156,92 @@ namespace LibationFileManager fileNamingTemplate.AddParameterReplacement(TemplateTags.Locale, libraryBookDto.Locale); fileNamingTemplate.AddParameterReplacement(TemplateTags.YearPublished, libraryBookDto.YearPublished?.ToString() ?? "1900"); - return fileNamingTemplate; + //Add the sanitized replacement parameters + foreach (var param in fileDateParams) + fileNamingTemplate.ParameterReplacements.AddIfNotContains(param); + foreach (var param in dateAddedParams) + fileNamingTemplate.ParameterReplacements.AddIfNotContains(param); + foreach (var param in pubDateParams) + fileNamingTemplate.ParameterReplacements.AddIfNotContains(param); + + return fileNamingTemplate; } #endregion - private static string dateMatchEvaluator(Match match) + #region DateTime Tags + + /// the file naming template. Any found date tags will be sanitized, + /// and the template's original date tag will be replaced with the sanitized tag. + /// A list of parameter replacement key-value pairs + private static List> getSanitizeDateReplacementParameters(Regex datePattern, ref string template, ReplacementCharacters replacements, DateTime? dateTime) { + List> dateParams = new(); + + foreach (Match dateTag in datePattern.Matches(template)) + { + var sanitizedTag = sanitizeDateParameterTag(dateTag, replacements, out var sanitizedFormatter); + if (tryFormatDateTime(dateTime, sanitizedFormatter, replacements, out var formattedDateString)) + { + dateParams.Add(new(sanitizedTag, formattedDateString)); + template = template.Replace(dateTag.Value, sanitizedTag); + } + } + return dateParams; + } + + /// a date parameter replacement tag with the format string sanitized + private static string sanitizeDateParameterTag(Match dateTag, ReplacementCharacters replacements, out string sanitizedFormatter) + { + if (dateTag.Groups.Count < 2 || string.IsNullOrWhiteSpace(dateTag.Groups[1].Value)) + { + sanitizedFormatter = DEFAULT_DATE_FORMAT; + return dateTag.Value; + } + + var formatter = dateTag.Groups[1].Value; + + sanitizedFormatter = replacements.ReplaceFilenameChars(formatter).Trim(); + + return dateTag.Value.Replace(formatter, sanitizedFormatter); + } + + private static bool tryFormatDateTime(DateTime? dateTime, string sanitizedFormatter, ReplacementCharacters replacements, out string formattedDateString) + { + if (!dateTime.HasValue) + { + formattedDateString = string.Empty; + return true; + } + try { - return DateTime.Now.ToString(match.Groups[1].Value); + formattedDateString = replacements.ReplaceFilenameChars(dateTime.Value.ToString(sanitizedFormatter)).Trim(); + return true; } catch { - return match.Value; + formattedDateString = null; + return false; } } + #endregion public virtual IEnumerable GetTemplateTags() => TemplateTags.GetAll() // yeah, this line is a little funky but it works when you think through it. also: trust the unit tests .Where(t => IsChapterized || !t.IsChapterOnly); - public string Sanitize(string template) + public string Sanitize(string template, ReplacementCharacters replacements) { var value = template ?? ""; + // Replace invalid filename characters in the DateTime format provider so we don't trip any alarms. + // Illegal filename characters in the formatter are allowed because they will be replaced by + // getFileNamingTemplate() + value = fileDateTagRegex.Replace(value, m => sanitizeDateParameterTag(m, replacements, out _)); + value = dateAddedTagRegex.Replace(value, m => sanitizeDateParameterTag(m, replacements, out _)); + value = datePublishedTagRegex.Replace(value, m => sanitizeDateParameterTag(m, replacements, out _)); + // don't use alt slash value = value.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); @@ -218,7 +288,7 @@ namespace LibationFileManager // must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save. if (ReplacementCharacters.ContainsInvalidPathChar(template.Replace("<", "").Replace(">", ""))) - return new[] { ERROR_INVALID_FILE_NAME_CHAR }; + return new[] { ERROR_INVALID_FILE_NAME_CHAR }; return Valid; } @@ -229,8 +299,8 @@ namespace LibationFileManager #region to file name /// USES LIVE CONFIGURATION VALUES public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null) - => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null) - .GetFilePath(Configuration.Instance.ReplacementCharacters, string.Empty); + => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null, Configuration.Instance.ReplacementCharacters) + .GetFilePath(string.Empty); #endregion } @@ -252,8 +322,8 @@ namespace LibationFileManager #region to file name /// USES LIVE CONFIGURATION VALUES public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false) - => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension) - .GetFilePath(Configuration.Instance.ReplacementCharacters, extension, returnFirstExisting); + => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension, Configuration.Instance.ReplacementCharacters) + .GetFilePath(extension, returnFirstExisting); #endregion } @@ -294,14 +364,21 @@ namespace LibationFileManager replacements ??= Configuration.Instance.ReplacementCharacters; var fileExtension = Path.GetExtension(props.OutputFileName); - var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, fileExtension); + var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, fileExtension, replacements); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber, props.PartsPosition); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal)); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? ""); - return fileNamingTemplate.GetFilePath(replacements, fileExtension).PathWithoutPrefix; + foreach (Match dateTag in fileDateTagRegex.Matches(fileNamingTemplate.Template)) + { + var sanitizedTag = sanitizeDateParameterTag(dateTag, replacements, out string sanitizedFormatter); + if (tryFormatDateTime(props.FileDate, sanitizedFormatter, replacements, out var formattedDateString)) + fileNamingTemplate.ParameterReplacements[sanitizedTag] = formattedDateString; + } + + return fileNamingTemplate.GetFilePath(fileExtension).PathWithoutPrefix; } #endregion } diff --git a/Source/LibationFileManager/UtilityExtensions.cs b/Source/LibationFileManager/UtilityExtensions.cs index 5c322a03..8ffe8c9e 100644 --- a/Source/LibationFileManager/UtilityExtensions.cs +++ b/Source/LibationFileManager/UtilityExtensions.cs @@ -9,5 +9,8 @@ namespace LibationFileManager { public static void AddParameterReplacement(this NamingTemplate fileNamingTemplate, TemplateTags templateTags, object value) => fileNamingTemplate.AddParameterReplacement(templateTags.TagName, value); + + public static void AddUniqueParameterReplacement(this NamingTemplate namingTemplate, string key, object value) + => namingTemplate.ParameterReplacements[key] = value; } } diff --git a/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs b/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs index a14005d0..e44c6e17 100644 --- a/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs +++ b/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs @@ -18,7 +18,7 @@ namespace LibationWinForms.Dialogs private string workingTemplateText { get => _workingTemplateText; - set => _workingTemplateText = template.Sanitize(value); + set => _workingTemplateText = template.Sanitize(value, Configuration.Instance.ReplacementCharacters); } private void resetTextBox(string value) => this.templateTb.Text = workingTemplateText = value; @@ -59,7 +59,7 @@ namespace LibationWinForms.Dialogs // populate list view foreach (var tag in template.GetTemplateTags()) - listView1.Items.Add(new ListViewItem(new[] { $"<{tag.TagName}>", tag.Description })); + listView1.Items.Add(new ListViewItem(new[] { $"<{tag.TagName}>", tag.Description }) { Tag = tag.DefaultValue }); } private void resetToDefaultBtn_Click(object sender, EventArgs e) => resetTextBox(template.DefaultTemplate); @@ -73,6 +73,8 @@ namespace LibationWinForms.Dialogs var libraryBookDto = new LibraryBookDto { Account = "my account", + DateAdded = new DateTime(2022, 6, 9, 0, 0, 0), + DatePublished = new DateTime(2017, 2, 27, 0, 0, 0), AudibleProductId = "123456789", Title = "A Study in Scarlet: A Sherlock Holmes Novel", Locale = "us", @@ -207,9 +209,11 @@ namespace LibationWinForms.Dialogs private void listView1_DoubleClick(object sender, EventArgs e) { - var itemText = listView1.SelectedItems[0].Text.Replace("...", ""); - var text = templateTb.Text; + var itemText = listView1.SelectedItems[0].Tag as string; + if (string.IsNullOrEmpty(itemText)) return; + + var text = templateTb.Text; var selStart = Math.Min(Math.Max(0, templateTb.SelectionStart), text.Length); templateTb.Text = text.Insert(selStart, itemText); diff --git a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs index 54b0c375..b1822357 100644 --- a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs +++ b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs @@ -36,10 +36,10 @@ namespace FileNamingTemplateTests extension = FileUtility.GetStandardizedExtension(extension); var fullfilename = Path.Combine(dirFullPath, template + extension); - var fileNamingTemplate = new FileNamingTemplate(fullfilename); + var fileNamingTemplate = new FileNamingTemplate(fullfilename, Replacements); fileNamingTemplate.AddParameterReplacement("title", filename); fileNamingTemplate.AddParameterReplacement("id", metadataSuffix); - return fileNamingTemplate.GetFilePath(Replacements, extension).PathWithoutPrefix; + return fileNamingTemplate.GetFilePath(extension).PathWithoutPrefix; } [TestMethod] @@ -61,10 +61,10 @@ namespace FileNamingTemplateTests var estension = Path.GetExtension(originalPath); var t = Path.ChangeExtension(originalPath, null) + " - - " + estension; - var fileNamingTemplate = new FileNamingTemplate(t); + var fileNamingTemplate = new FileNamingTemplate(t, Replacements); fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros); fileNamingTemplate.AddParameterReplacement("title", suffix); - return fileNamingTemplate.GetFilePath(Replacements, estension).PathWithoutPrefix; + return fileNamingTemplate.GetFilePath(estension).PathWithoutPrefix; } [TestMethod] @@ -74,9 +74,9 @@ namespace FileNamingTemplateTests { if (Environment.OSVersion.Platform == platformID) { - var fileNamingTemplate = new FileNamingTemplate(inStr); + var fileNamingTemplate = new FileNamingTemplate(inStr, Replacements); fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s"); - fileNamingTemplate.GetFilePath(Replacements, "txt").PathWithoutPrefix.Should().Be(outStr); + fileNamingTemplate.GetFilePath("txt").PathWithoutPrefix.Should().Be(outStr); } } } diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs index 98a562f2..93759d85 100644 --- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -26,11 +26,32 @@ namespace TemplatesTests => new() { Account = "my account", + DateAdded = new DateTime(2022, 6, 9, 0, 0, 0), + DatePublished = new DateTime(2017, 2, 27, 0, 0, 0), + FileDate = new DateTime(2023, 1, 28, 0, 0, 0), AudibleProductId = "asin", Title = "A Study in Scarlet: A Sherlock Holmes Novel", Locale = "us", - YearPublished = 2017, - Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" }, + YearPublished = 2017, + Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" }, + Narrators = new List<string> { "Stephen Fry" }, + SeriesName = seriesName ?? "", + SeriesNumber = "1", + BitRate = 128, + SampleRate = 44100, + Channels = 2 + }; + + public static LibraryBookDto GetLibraryBookWithNullDates(string seriesName = "Sherlock Holmes") + => new() + { + Account = "my account", + FileDate = new DateTime(2023, 1, 28, 0, 0, 0), + AudibleProductId = "asin", + Title = "A Study in Scarlet: A Sherlock Holmes Novel", + Locale = "us", + YearPublished = 2017, + Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" }, Narrators = new List<string> { "Stephen Fry" }, SeriesName = seriesName ?? "", SeriesNumber = "1", @@ -71,14 +92,14 @@ namespace TemplatesTests [DataRow(null, @"C:\", "ext")] [ExpectedException(typeof(ArgumentNullException))] public void arg_null_exception(string template, string dirFullPath, string extension) - => Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension); + => Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements); [TestMethod] [DataRow("", @"C:\foo\bar", "ext")] [DataRow(" ", @"C:\foo\bar", "ext")] [ExpectedException(typeof(ArgumentException))] public void arg_exception(string template, string dirFullPath, string extension) - => Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension); + => Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements); [TestMethod] [DataRow("f.txt", @"C:\foo\bar", "", @"C:\foo\bar\f.txt", PlatformID.Win32NT)] @@ -98,20 +119,126 @@ namespace TemplatesTests public void Tests(string template, string dirFullPath, string extension, string expected, PlatformID platformID) { if (Environment.OSVersion.Platform == platformID) - Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension) - .GetFilePath(Replacements, extension) + Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements) + .GetFilePath(extension) .PathWithoutPrefix .Should().Be(expected); } + [TestMethod] + [DataRow("<id> - <filedate[yy-MM-dd]>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 23-01-28.m4b")] + [DataRow("<id> - <filedate [ yy-MM-dd ] >", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 23-01-28.m4b")] + [DataRow("<id> - <file date [yy-MM-dd] >", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 23-01-28.m4b")] + [DataRow("<id> - <file date[yy-MM-dd]>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 23-01-28.m4b")] + [DataRow("<id> - <file date[]>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")] + [DataRow("<id> - <filedate[]>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")] + [DataRow("<id> - <filedate [ ] >", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")] + [DataRow("<id> - <filedate>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")] + [DataRow("<id> - <filedate >", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")] + [DataRow("<id> - <file date>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")] + [DataRow("<id> - <file date>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")] + public void DateFormat_pattern(string template, string dirFullPath, string extension, string expected) + { + if (Environment.OSVersion.Platform is not PlatformID.Win32NT) + { + dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/'); + expected = expected.Replace("C:", "").Replace('\\', '/'); + } + + Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements) + .GetFilePath(extension) + .PathWithoutPrefix + .Should().Be(expected); + } + + [TestMethod] + [DataRow("<filedate[h]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate[h]>.m4b")] + [DataRow("< filedate[yyyy]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\< filedate[yyyy]>.m4b")] + [DataRow("<filedate yyyy]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate yyyy]>.m4b")] + [DataRow("<filedate [yyyy>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate [yyyy>.m4b")] + [DataRow("<filedate yyyy>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate yyyy>.m4b")] + [DataRow("<filedate[yyyy]", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate[yyyy].m4b")] + [DataRow("<fil edate[yyyy]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<fil edate[yyyy]>.m4b")] + [DataRow("<filed ate[yyyy]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filed ate[yyyy]>.m4b")] + public void DateFormat_invalid(string template, string dirFullPath, string extension, string expected) + { + if (Environment.OSVersion.Platform is not PlatformID.Win32NT) + { + dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/'); + expected = expected.Replace("C:", "").Replace('\\', '/').Replace('<', '<').Replace('>','>'); + } + + Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements) + .GetFilePath(extension) + .PathWithoutPrefix + .Should().Be(expected); + } + + [TestMethod] + [DataRow("<filedate[yy-MM-dd]> <date added[yy-MM-dd]> <pubdate[yy-MM]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28 22-06-09 17-02.m4b")] + [DataRow("<filedate[yy-MM-dd]> <filedate[yy-MM-dd]> <filedate[yy-MM-dd]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28 23-01-28 23-01-28.m4b")] + [DataRow("<file date [ yy-MM-dd ] > <filedate [ yy-MM-dd ] > <file date [ yy-MM-dd] >", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28 23-01-28 23-01-28.m4b")] + public void DateFormat_multiple(string template, string dirFullPath, string extension, string expected) + { + if (Environment.OSVersion.Platform is not PlatformID.Win32NT) + { + dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/'); + expected = expected.Replace("C:", "").Replace('\\', '/'); + } + + Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements) + .GetFilePath(extension) + .PathWithoutPrefix + .Should().Be(expected); + } + + [TestMethod] + [DataRow("<id> - <pubdate[MM/dd/yy HH:mm]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\asin - 02∕27∕17 00꞉00.m4b", PlatformID.Win32NT)] + [DataRow("<id> - <pubdate[MM/dd/yy HH:mm]>", @"/foo/bar", ".m4b", @"/foo/bar/asin - 02∕27∕17 00:00.m4b", PlatformID.Unix)] + [DataRow("<id> - <filedate[MM/dd/yy HH:mm]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\asin - 01∕28∕23 00꞉00.m4b", PlatformID.Win32NT)] + [DataRow("<id> - <filedate[MM/dd/yy HH:mm]>", @"/foo/bar", ".m4b", @"/foo/bar/asin - 01∕28∕23 00:00.m4b", PlatformID.Unix)] + [DataRow("<id> - <date added[MM/dd/yy HH:mm]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\asin - 06∕09∕22 00꞉00.m4b", PlatformID.Win32NT)] + [DataRow("<id> - <date added[MM/dd/yy HH:mm]>", @"/foo/bar", ".m4b", @"/foo/bar/asin - 06∕09∕22 00:00.m4b", PlatformID.Unix)] + public void DateFormat_illegal(string template, string dirFullPath, string extension, string expected, PlatformID platformID) + { + if (Environment.OSVersion.Platform == platformID) + { + Templates.File.HasWarnings(template).Should().BeTrue(); + Templates.File.HasWarnings(Templates.File.Sanitize(template, Replacements)).Should().BeFalse(); + Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements) + .GetFilePath(extension) + .PathWithoutPrefix + .Should().Be(expected); + + } + } + + + [TestMethod] + [DataRow("<filedate[yy-MM-dd]> <date added[yy-MM-dd]> <pubdate[yy-MM]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28.m4b")] + public void DateFormat_null(string template, string dirFullPath, string extension, string expected) + { + if (Environment.OSVersion.Platform is not PlatformID.Win32NT) + { + dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/'); + expected = expected.Replace("C:", "").Replace('\\', '/'); + } + + Templates.getFileNamingTemplate(GetLibraryBookWithNullDates(), template, dirFullPath, extension, Replacements) + .GetFilePath(extension) + .PathWithoutPrefix + .Should().Be(expected); + + } + [TestMethod] [DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)] [DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)] public void IfSeries_empty(string directory, string expected, PlatformID platformID) { if (Environment.OSVersion.Platform == platformID) - Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series-><-if series>bar", directory, "ext") - .GetFilePath(Replacements, ".ext") + Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series-><-if series>bar", directory, "ext", Replacements) + .GetFilePath(".ext") .PathWithoutPrefix .Should().Be(expected); } @@ -122,8 +249,8 @@ namespace TemplatesTests public void IfSeries_no_series(string directory, string expected, PlatformID platformID) { if (Environment.OSVersion.Platform == platformID) - Templates.getFileNamingTemplate(GetLibraryBook(null), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext") - .GetFilePath(Replacements, ".ext") + Templates.getFileNamingTemplate(GetLibraryBook(null), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext", Replacements) + .GetFilePath(".ext") .PathWithoutPrefix .Should().Be(expected); } @@ -134,8 +261,8 @@ namespace TemplatesTests public void IfSeries_with_series(string directory, string expected, PlatformID platformID) { if (Environment.OSVersion.Platform == platformID) - Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext") - .GetFilePath(Replacements, ".ext") + Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext", Replacements) + .GetFilePath(".ext") .PathWithoutPrefix .Should().Be(expected); }