Add a more general NamingTemplate
This commit is contained in:
parent
867085600c
commit
20474e0b3c
@ -25,13 +25,12 @@ namespace FileLiberator
|
|||||||
|
|
||||||
if (seriesParent is not null)
|
if (seriesParent is not null)
|
||||||
{
|
{
|
||||||
var baseDir = Templates.Folder.GetFilename(seriesParent.ToDto());
|
var baseDir = Templates.Folder.GetFilename(seriesParent.ToDto(), "", "");
|
||||||
return Templates.Folder.GetFilename(libraryBook.ToDto(), baseDir);
|
return Templates.Folder.GetFilename(libraryBook.ToDto(), baseDir, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Templates.Folder.GetFilename(libraryBook.ToDto(), "", "");
|
||||||
return Templates.Folder.GetFilename(libraryBook.ToDto());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -38,7 +38,7 @@ namespace FileLiberator
|
|||||||
=> Templates.ChapterFile.GetFilename(LibraryBookDto, props);
|
=> Templates.ChapterFile.GetFilename(LibraryBookDto, props);
|
||||||
|
|
||||||
public string GetMultipartTitle(MultiConvertFileProperties props)
|
public string GetMultipartTitle(MultiConvertFileProperties props)
|
||||||
=> Templates.ChapterTitle.GetTitle(LibraryBookDto, props);
|
=> Templates.ChapterTitle.GetName(LibraryBookDto, props);
|
||||||
|
|
||||||
public async Task<string> SaveClipsAndBookmarksAsync(string fileName)
|
public async Task<string> SaveClipsAndBookmarksAsync(string fileName)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -41,6 +41,7 @@ namespace FileLiberator
|
|||||||
|
|
||||||
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
|
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
|
||||||
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Order,
|
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Order,
|
||||||
|
IsPodcast = libraryBook.Book.IsEpisodeChild(),
|
||||||
|
|
||||||
BitRate = libraryBook.Book.AudioFormat.Bitrate,
|
BitRate = libraryBook.Book.AudioFormat.Bitrate,
|
||||||
SampleRate = libraryBook.Book.AudioFormat.SampleRate,
|
SampleRate = libraryBook.Book.AudioFormat.SampleRate,
|
||||||
|
|||||||
@ -1,123 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace FileManager
|
|
||||||
{
|
|
||||||
/// <summary>Get valid filename. Advanced features incl. parameterized template</summary>
|
|
||||||
public class FileNamingTemplate : NamingTemplate
|
|
||||||
{
|
|
||||||
public ReplacementCharacters ReplacementCharacters { get; }
|
|
||||||
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
|
||||||
public FileNamingTemplate(string template, ReplacementCharacters replacement) : base(template)
|
|
||||||
{
|
|
||||||
ReplacementCharacters = replacement ?? ReplacementCharacters.Default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Generate a valid path for this file or directory</summary>
|
|
||||||
public LongPath GetFilePath(string fileExtension, bool returnFirstExisting = false)
|
|
||||||
{
|
|
||||||
string fileName =
|
|
||||||
Template.EndsWith(Path.DirectorySeparatorChar) || Template.EndsWith(Path.AltDirectorySeparatorChar) ?
|
|
||||||
FileUtility.RemoveLastCharacter(Template) :
|
|
||||||
Template;
|
|
||||||
|
|
||||||
List<string> pathParts = new();
|
|
||||||
|
|
||||||
var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, ReplacementCharacters));
|
|
||||||
|
|
||||||
while (!string.IsNullOrEmpty(fileName))
|
|
||||||
{
|
|
||||||
var file = Path.GetFileName(fileName);
|
|
||||||
|
|
||||||
if (Path.IsPathRooted(Template) && file == string.Empty)
|
|
||||||
{
|
|
||||||
pathParts.Add(fileName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pathParts.Add(file);
|
|
||||||
fileName = Path.GetDirectoryName(fileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pathParts.Reverse();
|
|
||||||
var fileNamePart = pathParts[^1];
|
|
||||||
pathParts.Remove(fileNamePart);
|
|
||||||
|
|
||||||
fileNamePart = fileNamePart[..^fileExtension.Length];
|
|
||||||
|
|
||||||
LongPath directory = Path.Join(pathParts.Select(p => replaceFileName(p, paramReplacements, LongPath.MaxFilenameLength)).ToArray());
|
|
||||||
|
|
||||||
//If file already exists, GetValidFilename will append " (n)" to the filename.
|
|
||||||
//This could cause the filename length to exceed MaxFilenameLength, so reduce
|
|
||||||
//allowable filename length by 5 chars, allowing for up to 99 duplicates.
|
|
||||||
return FileUtility
|
|
||||||
.GetValidFilename(
|
|
||||||
Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - fileExtension.Length - 5)) + fileExtension,
|
|
||||||
ReplacementCharacters,
|
|
||||||
fileExtension,
|
|
||||||
returnFirstExisting
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string replaceFileName(string filename, Dictionary<string,string> paramReplacements, int maxFilenameLength)
|
|
||||||
{
|
|
||||||
List<StringBuilder> filenameParts = new();
|
|
||||||
//Build the filename in parts, replacing replacement parameters with
|
|
||||||
//their values, and storing the parts in a list.
|
|
||||||
while (!string.IsNullOrEmpty(filename))
|
|
||||||
{
|
|
||||||
int openIndex = filename.IndexOf('<');
|
|
||||||
int closeIndex = filename.IndexOf('>');
|
|
||||||
|
|
||||||
if (openIndex == 0 && closeIndex > 0)
|
|
||||||
{
|
|
||||||
var key = filename[..(closeIndex + 1)];
|
|
||||||
|
|
||||||
if (paramReplacements.ContainsKey(key))
|
|
||||||
filenameParts.Add(new StringBuilder(paramReplacements[key]));
|
|
||||||
else
|
|
||||||
filenameParts.Add(new StringBuilder(key));
|
|
||||||
|
|
||||||
filename = filename[(closeIndex + 1)..];
|
|
||||||
}
|
|
||||||
else if (openIndex > 0 && closeIndex > openIndex)
|
|
||||||
{
|
|
||||||
var other = filename[..openIndex];
|
|
||||||
filenameParts.Add(new StringBuilder(other));
|
|
||||||
filename = filename[openIndex..];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
filenameParts.Add(new StringBuilder(filename));
|
|
||||||
filename = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Remove 1 character from the end of the longest filename part until
|
|
||||||
//the total filename is less than max filename length
|
|
||||||
while (filenameParts.Sum(p => LongPath.GetFilesystemStringLength(p)) > maxFilenameLength)
|
|
||||||
{
|
|
||||||
int maxLength = filenameParts.Max(p => p.Length);
|
|
||||||
var maxEntry = filenameParts.First(p => p.Length == maxLength);
|
|
||||||
|
|
||||||
maxEntry.Remove(maxLength - 1, 1);
|
|
||||||
}
|
|
||||||
return string.Join("", filenameParts);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string formatValue(object value, ReplacementCharacters replacements)
|
|
||||||
{
|
|
||||||
if (value is null)
|
|
||||||
return "";
|
|
||||||
|
|
||||||
// Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders.
|
|
||||||
// Esp important for file templates.
|
|
||||||
return replacements.ReplaceFilenameChars(value.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -56,9 +56,9 @@ namespace FileManager
|
|||||||
//don't care about encoding, so how unicode characters are encoded is
|
//don't care about encoding, so how unicode characters are encoded is
|
||||||
///a choice made by the linux kernel. As best as I can tell, pretty
|
///a choice made by the linux kernel. As best as I can tell, pretty
|
||||||
//much everyone uses UTF-8.
|
//much everyone uses UTF-8.
|
||||||
public static int GetFilesystemStringLength(StringBuilder filename)
|
public static int GetFilesystemStringLength(string filename)
|
||||||
=> IsWindows ? filename.Length
|
=> IsWindows ? filename.Length
|
||||||
: Encoding.UTF8.GetByteCount(filename.ToString());
|
: Encoding.UTF8.GetByteCount(filename);
|
||||||
|
|
||||||
public static implicit operator LongPath(string path)
|
public static implicit operator LongPath(string path)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace FileManager
|
|
||||||
{
|
|
||||||
public class MetadataNamingTemplate : NamingTemplate
|
|
||||||
{
|
|
||||||
public MetadataNamingTemplate(string template) : base(template) { }
|
|
||||||
|
|
||||||
public string GetTagContents()
|
|
||||||
{
|
|
||||||
var tagValue = Template;
|
|
||||||
|
|
||||||
foreach (var r in ParameterReplacements)
|
|
||||||
tagValue = tagValue.Replace($"<{formatKey(r.Key)}>", r.Value?.ToString() ?? "");
|
|
||||||
|
|
||||||
return tagValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
using Dinah.Core;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace FileManager
|
|
||||||
{
|
|
||||||
public class NamingTemplate
|
|
||||||
{
|
|
||||||
/// <summary>Proposed full name. May contain optional html-styled template tags. Eg: <name></summary>
|
|
||||||
public string Template { get; }
|
|
||||||
|
|
||||||
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
|
||||||
public NamingTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
|
||||||
|
|
||||||
/// <summary>Optional step 1: Replace html-styled template tags with parameters. Eg {"name", "Bill Gates"} => /<name>/ => /Bill Gates/</summary>
|
|
||||||
public Dictionary<string, object> ParameterReplacements { get; } = new Dictionary<string, object>();
|
|
||||||
|
|
||||||
/// <summary>Convenience method</summary>
|
|
||||||
public void AddParameterReplacement(string key, object value)
|
|
||||||
// using .Add() instead of "[key] = value" will make unintended overwriting throw exception
|
|
||||||
=> ParameterReplacements.Add(key, value);
|
|
||||||
|
|
||||||
protected static string formatKey(string key)
|
|
||||||
=> key
|
|
||||||
.Replace("<", "")
|
|
||||||
.Replace(">", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
|
internal interface IClosingPropertyTag : IPropertyTag
|
||||||
|
{
|
||||||
|
/// <summary>The <see cref="Regex"/> used to match the closing <see cref="IPropertyTag.TemplateTag"/> in template strings.</summary>
|
||||||
|
public Regex NameCloseMatcher { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine if the template string starts with <see cref="IPropertyTag.TemplateTag"/>'s closing tag signature,
|
||||||
|
/// and if it does output the matching tag's <see cref="ITemplateTag"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="templateString">Template string</param>
|
||||||
|
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||||
|
/// <param name="propertyTag">The registered <see cref="IPropertyTag"/></param>
|
||||||
|
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
||||||
|
bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConditionalTagClass<TClass> : TagClass
|
||||||
|
{
|
||||||
|
public ConditionalTagClass(bool caseSensative = true) :base(typeof(TClass), caseSensative) { }
|
||||||
|
|
||||||
|
public void RegisterCondition(ITemplateTag templateTag, Func<TClass, bool> propertyGetter)
|
||||||
|
{
|
||||||
|
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
||||||
|
|
||||||
|
AddPropertyTag(new ConditionalTag(templateTag, Options, expr));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConditionalTag : TagBase, IClosingPropertyTag
|
||||||
|
{
|
||||||
|
public Regex NameCloseMatcher { get; }
|
||||||
|
|
||||||
|
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression)
|
||||||
|
: base(templateTag, conditionExpression)
|
||||||
|
{
|
||||||
|
NameMatcher = new Regex($"^<{templateTag.TagName}->", options);
|
||||||
|
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag)
|
||||||
|
{
|
||||||
|
var match = NameCloseMatcher.Match(templateString);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
exactName = match.Value;
|
||||||
|
propertyTag = this;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
exactName = null;
|
||||||
|
propertyTag = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression GetTagExpression(string exactName, string formatter) => ExpressionValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Source/FileManager/NamingTemplate/ITemplateTag.cs
Normal file
6
Source/FileManager/NamingTemplate/ITemplateTag.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
|
public interface ITemplateTag
|
||||||
|
{
|
||||||
|
string TagName { get; }
|
||||||
|
}
|
||||||
271
Source/FileManager/NamingTemplate/NamingTemplate.cs
Normal file
271
Source/FileManager/NamingTemplate/NamingTemplate.cs
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
|
public class NamingTemplate
|
||||||
|
{
|
||||||
|
public string TemplateText { get; private set; }
|
||||||
|
public IEnumerable<ITemplateTag> TagsInUse => _tagsInUse;
|
||||||
|
public IEnumerable<ITemplateTag> TagsRegistered => Classes.SelectMany(p => p.TemplateTags).DistinctBy(f => f.TagName);
|
||||||
|
public IEnumerable<string> Warnings => errors.Concat(warnings);
|
||||||
|
public IEnumerable<string> Errors => errors;
|
||||||
|
|
||||||
|
private Delegate templateToString;
|
||||||
|
private readonly List<string> warnings = new();
|
||||||
|
private readonly List<string> errors = new();
|
||||||
|
private readonly IEnumerable<TagClass> Classes;
|
||||||
|
private readonly List<ITemplateTag> _tagsInUse = new();
|
||||||
|
|
||||||
|
public const string ERROR_NULL_IS_INVALID = "Null template is invalid.";
|
||||||
|
public const string WARNING_EMPTY = "Template is empty.";
|
||||||
|
public const string WARNING_WHITE_SPACE = "Template is white space.";
|
||||||
|
public const string WARNING_NO_TAGS = "Should use tags. Eg: <title>";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke the <see cref="NamingTemplate"/> to
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagClass{TClass}"/> and <see cref="ConditionalTagClass{TClass}"/></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public TemplatePart Evaluate(params object[] propertyClasses)
|
||||||
|
{
|
||||||
|
//Match propertyClasses to the arguments required by templateToString.DynamicInvoke()
|
||||||
|
var delegateArgTypes = templateToString.GetType().GenericTypeArguments[..^1];
|
||||||
|
|
||||||
|
object[] args = new object[delegateArgTypes.Length];
|
||||||
|
|
||||||
|
for (int i = 0; i < delegateArgTypes.Length; i++)
|
||||||
|
args[i] = propertyClasses.First(o => o.GetType() == delegateArgTypes[i]);
|
||||||
|
|
||||||
|
if (args.Any(a => a is null))
|
||||||
|
throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}");
|
||||||
|
|
||||||
|
return ((TemplatePart)templateToString.DynamicInvoke(args)).FirstPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary>
|
||||||
|
/// <param name="template">The template string to parse</param>
|
||||||
|
/// <param name="tagClasses">A collection of <see cref="ITagClass"/> with
|
||||||
|
/// properties registered to match to the <paramref name="template"/></param>
|
||||||
|
public static NamingTemplate Parse(string template, IEnumerable<TagClass> tagClasses)
|
||||||
|
{
|
||||||
|
var namingTemplate = new NamingTemplate(tagClasses);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BinaryNode intermediate = namingTemplate.IntermediateParse(template);
|
||||||
|
Expression evalTree = GetExpressionTree(intermediate);
|
||||||
|
|
||||||
|
List<ParameterExpression> parameters = new();
|
||||||
|
|
||||||
|
foreach (var tagclass in tagClasses)
|
||||||
|
parameters.Add(tagclass.Parameter);
|
||||||
|
|
||||||
|
namingTemplate.templateToString = Expression.Lambda(evalTree, parameters).Compile();
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
namingTemplate.errors.Add(ex.Message);
|
||||||
|
}
|
||||||
|
return namingTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NamingTemplate(IEnumerable<TagClass> properties)
|
||||||
|
{
|
||||||
|
Classes = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary>
|
||||||
|
private static Expression GetExpressionTree(BinaryNode node)
|
||||||
|
{
|
||||||
|
if (node is null) return TemplatePart.Blank;
|
||||||
|
else if (node.IsValue) return node.Expression;
|
||||||
|
else if (node.IsConditional) return Expression.Condition(node.Expression, concatExpression(node), TemplatePart.Blank);
|
||||||
|
else return concatExpression(node);
|
||||||
|
|
||||||
|
Expression concatExpression(BinaryNode node)
|
||||||
|
=> TemplatePart.CreateConcatenation(GetExpressionTree(node.LeftChild), GetExpressionTree(node.RightChild));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parse a template string into a <see cref="BinaryNode"/> tree</summary>
|
||||||
|
private BinaryNode IntermediateParse(string templateString)
|
||||||
|
{
|
||||||
|
if (templateString is null)
|
||||||
|
throw new NullReferenceException(ERROR_NULL_IS_INVALID);
|
||||||
|
else if (string.IsNullOrEmpty(templateString))
|
||||||
|
warnings.Add(WARNING_EMPTY);
|
||||||
|
else if (string.IsNullOrWhiteSpace(templateString))
|
||||||
|
warnings.Add(WARNING_WHITE_SPACE);
|
||||||
|
|
||||||
|
TemplateText = templateString;
|
||||||
|
|
||||||
|
BinaryNode currentNode = BinaryNode.CreateRoot();
|
||||||
|
BinaryNode topNode = currentNode;
|
||||||
|
List<char> literalChars = new();
|
||||||
|
|
||||||
|
while (templateString.Length > 0)
|
||||||
|
{
|
||||||
|
if (StartsWith(Classes, templateString, out string exactPropertyName, out var propertyTag, out var valueExpression))
|
||||||
|
{
|
||||||
|
checkAndAddLiterals();
|
||||||
|
|
||||||
|
if (propertyTag is IClosingPropertyTag)
|
||||||
|
currentNode = AddNewNode(currentNode, BinaryNode.CreateConditional(propertyTag.TemplateTag, valueExpression));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentNode = AddNewNode(currentNode, BinaryNode.CreateValue(propertyTag.TemplateTag, valueExpression));
|
||||||
|
_tagsInUse.Add(propertyTag.TemplateTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
templateString = templateString[exactPropertyName.Length..];
|
||||||
|
}
|
||||||
|
else if (StartsWithClosing(Classes, templateString, out exactPropertyName, out var closingPropertyTag))
|
||||||
|
{
|
||||||
|
checkAndAddLiterals();
|
||||||
|
|
||||||
|
BinaryNode lastParenth = currentNode;
|
||||||
|
|
||||||
|
while (lastParenth?.IsConditional is false)
|
||||||
|
lastParenth = lastParenth.Parent;
|
||||||
|
|
||||||
|
if (lastParenth?.Parent is null)
|
||||||
|
{
|
||||||
|
warnings.Add($"Missing <{closingPropertyTag.TemplateTag.TagName}-> open conditional.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (lastParenth.Name != closingPropertyTag.TemplateTag.TagName)
|
||||||
|
{
|
||||||
|
warnings.Add($"Missing <-{lastParenth.Name}> closing conditional.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode = lastParenth.Parent;
|
||||||
|
templateString = templateString[exactPropertyName.Length..];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//templateString does not start with a tag, so the first
|
||||||
|
//character is a literal and not part of a tag expression.
|
||||||
|
literalChars.Add(templateString[0]);
|
||||||
|
templateString = templateString[1..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkAndAddLiterals();
|
||||||
|
|
||||||
|
//Check for any conditionals that haven't been closed
|
||||||
|
while (currentNode is not null)
|
||||||
|
{
|
||||||
|
if (currentNode.IsConditional)
|
||||||
|
warnings.Add($"Missing <-{currentNode.Name}> closing conditional.");
|
||||||
|
currentNode = currentNode.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_tagsInUse.Any())
|
||||||
|
warnings.Add(WARNING_NO_TAGS);
|
||||||
|
|
||||||
|
return topNode;
|
||||||
|
|
||||||
|
void checkAndAddLiterals()
|
||||||
|
{
|
||||||
|
if (literalChars.Count != 0)
|
||||||
|
{
|
||||||
|
currentNode = AddNewNode(currentNode, BinaryNode.CreateValue(new string(literalChars.ToArray())));
|
||||||
|
literalChars.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool StartsWith(IEnumerable<TagClass> propertyClasses, string template, out string exactName, out IPropertyTag propertyTag, out Expression valueExpression)
|
||||||
|
{
|
||||||
|
foreach (var pc in propertyClasses)
|
||||||
|
{
|
||||||
|
if (pc.StartsWith(template, out exactName, out propertyTag, out valueExpression))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
exactName = null;
|
||||||
|
valueExpression = null;
|
||||||
|
propertyTag = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool StartsWithClosing(IEnumerable<TagClass> conditionalGroups, string template, out string exactName, out IClosingPropertyTag closingPropertyTag)
|
||||||
|
{
|
||||||
|
foreach (var pc in conditionalGroups)
|
||||||
|
{
|
||||||
|
if (pc.StartsWithClosing(template, out exactName, out closingPropertyTag))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
exactName = null;
|
||||||
|
closingPropertyTag = null;
|
||||||
|
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 Expression Expression { get; private init; }
|
||||||
|
public bool IsConditional { get; private init; } = false;
|
||||||
|
public bool IsValue { get; private init; } = false;
|
||||||
|
|
||||||
|
public static BinaryNode CreateRoot() => new("Root");
|
||||||
|
|
||||||
|
public static BinaryNode CreateValue(string literal) => new("Literal")
|
||||||
|
{
|
||||||
|
IsValue = true,
|
||||||
|
Expression = TemplatePart.CreateLiteral(literal)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property) => new(templateTag.TagName)
|
||||||
|
{
|
||||||
|
IsValue = true,
|
||||||
|
Expression = TemplatePart.CreateProperty(templateTag, property)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property) => new(templateTag.TagName)
|
||||||
|
{
|
||||||
|
IsConditional = true,
|
||||||
|
Expression = property
|
||||||
|
};
|
||||||
|
|
||||||
|
public static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right)
|
||||||
|
{
|
||||||
|
var newNode = new BinaryNode("Concatenation")
|
||||||
|
{
|
||||||
|
LeftChild = left,
|
||||||
|
RightChild = right
|
||||||
|
};
|
||||||
|
newNode.LeftChild.Parent = newNode;
|
||||||
|
newNode.RightChild.Parent = newNode;
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BinaryNode(string name) => Name = name;
|
||||||
|
public override string ToString() => Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
|
public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString);
|
||||||
|
|
||||||
|
public class PropertyTagClass<TClass> : TagClass
|
||||||
|
{
|
||||||
|
public PropertyTagClass(bool caseSensative = true) : base(typeof(TClass), caseSensative) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a nullable value type property.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="U">Type of the property from <see cref="TClass"/></typeparam>
|
||||||
|
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
|
||||||
|
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="U"/> property and a formatting string and returnes the value formatted to string</param>
|
||||||
|
public void RegisterProperty<U>(ITemplateTag templateTag, Func<TClass, U?> propertyGetter, PropertyFormatter<U> formatter = null)
|
||||||
|
where U : struct
|
||||||
|
=> RegisterProperty(templateTag, propertyGetter, formatter?.Method);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a non-nullable value type property
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="U">Type of the property from <see cref="TClass"/></typeparam>
|
||||||
|
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
|
||||||
|
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="U"/> property and a formatting string and returnes the value formatted to string</param>
|
||||||
|
public void RegisterProperty<U>(ITemplateTag templateTag, Func<TClass, U> propertyGetter, PropertyFormatter<U> formatter = null)
|
||||||
|
where U : struct
|
||||||
|
=> RegisterProperty(templateTag, propertyGetter, formatter?.Method);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a string type property.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param>
|
||||||
|
/// <param name="formatter">Optional formatting function that accepts the string property and a formatting string and returnes the value formatted to string</param>
|
||||||
|
public void RegisterProperty(ITemplateTag templateTag, Func<TClass, string> propertyGetter, PropertyFormatter<string> formatter = null)
|
||||||
|
=> RegisterProperty(templateTag, propertyGetter, formatter?.Method);
|
||||||
|
|
||||||
|
private void RegisterProperty(ITemplateTag templateTag, Delegate propertyGetter, MethodInfo formatter)
|
||||||
|
{
|
||||||
|
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
||||||
|
|
||||||
|
AddPropertyTag(new PropertyTag(templateTag, Options, expr, formatter));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PropertyTag : TagBase
|
||||||
|
{
|
||||||
|
private readonly Func<Expression, Type, string, Expression> createToStringExpression;
|
||||||
|
|
||||||
|
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyExpression, MethodInfo formatter)
|
||||||
|
: base(templateTag, propertyExpression)
|
||||||
|
{
|
||||||
|
var regexStr = formatter is null ? @$"^<{TemplateTag.TagName}>" : @$"^<{TemplateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>";
|
||||||
|
NameMatcher = new Regex(regexStr, options);
|
||||||
|
|
||||||
|
//Create the ToString() expression for the TagBase.ExpressionValue's type.
|
||||||
|
//If a formatter delegate was registered for this property, use that.
|
||||||
|
//Otherwise use the object.Tostring() method.
|
||||||
|
createToStringExpression
|
||||||
|
= formatter is null
|
||||||
|
? (expValue, retTyp, format) => Expression.Call(expValue, retTyp.GetMethod(nameof(object.ToString), Array.Empty<Type>()))
|
||||||
|
: (expValue, retTyp, format) => Expression.Call(null, formatter, Expression.Constant(templateTag), expValue, Expression.Constant(format));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression GetTagExpression(string exactName, string formatString)
|
||||||
|
{
|
||||||
|
var underlyingType = Nullable.GetUnderlyingType(ReturnType);
|
||||||
|
|
||||||
|
Expression toStringExpression
|
||||||
|
= ReturnType == typeof(string)
|
||||||
|
? createToStringExpression(Expression.Coalesce(ExpressionValue, Expression.Constant("")), ReturnType, formatString)
|
||||||
|
: underlyingType is null
|
||||||
|
? createToStringExpression(ExpressionValue, ReturnType, formatString)
|
||||||
|
: Expression.Condition(
|
||||||
|
Expression.PropertyOrField(ExpressionValue, "HasValue"),
|
||||||
|
createToStringExpression(Expression.PropertyOrField(ExpressionValue, "Value"), underlyingType, formatString),
|
||||||
|
Expression.Constant(""));
|
||||||
|
|
||||||
|
return Expression.TryCatch(toStringExpression, Expression.Catch(typeof(Exception), Expression.Constant(exactName)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
Source/FileManager/NamingTemplate/TagBase.cs
Normal file
67
Source/FileManager/NamingTemplate/TagBase.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
|
internal interface IPropertyTag
|
||||||
|
{
|
||||||
|
/// <summary>The tag that will be matched in a tag string</summary>
|
||||||
|
ITemplateTag TemplateTag { get; }
|
||||||
|
|
||||||
|
/// <summary><see cref="TemplateTag"/>'s <see cref="Type"/></summary>
|
||||||
|
Type ReturnType { get; }
|
||||||
|
|
||||||
|
/// <summary>The <see cref="Regex"/> used to match <see cref="TemplateTag"/> in template strings.</summary>
|
||||||
|
public Regex NameMatcher { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine if the template string starts with <see cref="TemplateTag"/>, and if it does parse the tag to an <see cref="ITagExpression"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="templateString">Template string</param>
|
||||||
|
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||||
|
/// <param name="propertyValue">The <see cref="Expression"/> that returns the property's value</param>
|
||||||
|
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
||||||
|
bool StartsWith(string templateString, out string exactName, out Expression propertyValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal abstract class TagBase : IPropertyTag
|
||||||
|
{
|
||||||
|
public ITemplateTag TemplateTag { get; }
|
||||||
|
public Regex NameMatcher { get; protected init; }
|
||||||
|
public Type ReturnType => ExpressionValue.Type;
|
||||||
|
protected Expression ExpressionValue { get; }
|
||||||
|
|
||||||
|
protected TagBase(ITemplateTag templateTag, Expression propertyExpression)
|
||||||
|
{
|
||||||
|
TemplateTag = templateTag;
|
||||||
|
ExpressionValue = propertyExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
|
||||||
|
public bool StartsWith(string templateString, out string exactName, out Expression propertyValue)
|
||||||
|
{
|
||||||
|
var match = NameMatcher.Match(templateString);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
exactName = match.Value;
|
||||||
|
propertyValue = GetTagExpression(exactName, match.Groups.Count == 2 ? match.Groups[1].Value.Trim() : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
exactName = null;
|
||||||
|
propertyValue = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"[Name = {TemplateTag.TagName}, Type = {ReturnType.Name}]";
|
||||||
|
}
|
||||||
|
}
|
||||||
77
Source/FileManager/NamingTemplate/TagClass.cs
Normal file
77
Source/FileManager/NamingTemplate/TagClass.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>A collection of <see cref="IPropertyTag"/>s registered to a single <see cref="Type"/>.</summary>
|
||||||
|
public abstract class TagClass
|
||||||
|
{
|
||||||
|
/// <summary>The <see cref="ParameterExpression"/> of the <see cref="TagClass"/>'s TClass type.</summary>
|
||||||
|
public ParameterExpression Parameter { get; }
|
||||||
|
/// <summary>The <see cref="ITemplateTag"/>s registered with this <see cref="TagClass"/> </summary>
|
||||||
|
public IEnumerable<ITemplateTag> TemplateTags => PropertyTags.Select(p => p.TemplateTag);
|
||||||
|
|
||||||
|
protected RegexOptions Options { get; } = RegexOptions.Compiled;
|
||||||
|
private protected List<IPropertyTag> PropertyTags { get; } = new();
|
||||||
|
|
||||||
|
protected TagClass(Type classType, bool caseSensative = true)
|
||||||
|
{
|
||||||
|
Parameter = Expression.Parameter(classType, classType.Name);
|
||||||
|
Options |= caseSensative ? RegexOptions.None : RegexOptions.IgnoreCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine if the template string starts with any of the <see cref="TemplateTags"/>s' <see cref="ITemplateTag"/> signatures,
|
||||||
|
/// and if it does parse the tag to an <see cref="Expression"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="templateString">Template string</param>
|
||||||
|
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||||
|
/// <param name="propertyValue">The <see cref="Expression"/> that returns the <paramref name="propertyTag"/>'s value</param>
|
||||||
|
/// <returns>True if the <paramref name="templateString"/> starts with a tag registered in this class.</returns>
|
||||||
|
internal bool StartsWith(string templateString, out string exactName, out IPropertyTag propertyTag, out Expression propertyValue)
|
||||||
|
{
|
||||||
|
foreach (var p in PropertyTags)
|
||||||
|
{
|
||||||
|
if (p.StartsWith(templateString, out exactName, out propertyValue))
|
||||||
|
{
|
||||||
|
propertyTag = p;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
propertyValue = null;
|
||||||
|
propertyTag = null;
|
||||||
|
exactName = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine if the template string starts with <see cref="IPropertyTag.TemplateTag"/>'s closing tag signature,
|
||||||
|
/// and if it does output the matching tag's <see cref="ITemplateTag"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="templateString">Template string</param>
|
||||||
|
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||||
|
/// <param name="closingPropertyTag">The registered <see cref="IClosingPropertyTag"/></param>
|
||||||
|
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
||||||
|
internal bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag closingPropertyTag)
|
||||||
|
{
|
||||||
|
foreach (var cg in PropertyTags.OfType<IClosingPropertyTag>())
|
||||||
|
{
|
||||||
|
if (cg.StartsWithClosing(templateString, out exactName, out closingPropertyTag))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
closingPropertyTag = null;
|
||||||
|
exactName = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private protected void AddPropertyTag(IPropertyTag propertyTag)
|
||||||
|
{
|
||||||
|
if (!PropertyTags.Any(c => c.TemplateTag.TagName == propertyTag.TemplateTag.TagName))
|
||||||
|
PropertyTags.Add(propertyTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
Source/FileManager/NamingTemplate/TemplatePart.cs
Normal file
112
Source/FileManager/NamingTemplate/TemplatePart.cs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
|
/// <summary>Represents one part of an evaluated <see cref="NamingTemplate"/>.</summary>
|
||||||
|
public class TemplatePart : IEnumerable<TemplatePart>
|
||||||
|
{
|
||||||
|
/// <summary>The <see cref="TemplatePart"/> name. If <see cref="TemplatePart"/> is
|
||||||
|
/// a registered property, this value is <see cref="ITemplateTag.TagName"/></summary>
|
||||||
|
public string TagName { get; }
|
||||||
|
|
||||||
|
/// <summary> The <see cref="IPropertyTag"/>'s <see cref="ITemplateTag"/> if <see cref="TemplatePart"/> is
|
||||||
|
/// a registered property, otherwise <see cref="null"/> for string literals. </summary>
|
||||||
|
public ITemplateTag TemplateTag { get; }
|
||||||
|
|
||||||
|
/// <summary>The evaluated string.</summary>
|
||||||
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
private TemplatePart previous;
|
||||||
|
private TemplatePart next;
|
||||||
|
private TemplatePart(string name, string value)
|
||||||
|
{
|
||||||
|
TagName = name;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
private TemplatePart(ITemplateTag templateTag, string value)
|
||||||
|
{
|
||||||
|
TemplateTag = templateTag;
|
||||||
|
TagName = templateTag.TagName;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Expression Blank
|
||||||
|
=> CreateExpression("Blank", Expression.Constant(""));
|
||||||
|
|
||||||
|
internal static Expression CreateLiteral(string constant)
|
||||||
|
=> CreateExpression("Literal", Expression.Constant(constant));
|
||||||
|
|
||||||
|
internal static Expression CreateProperty(ITemplateTag templateTag, Expression property)
|
||||||
|
=> Expression.New(tagTemplateConstructorInfo, Expression.Constant(templateTag), property);
|
||||||
|
|
||||||
|
internal static Expression CreateConcatenation(Expression left, Expression right)
|
||||||
|
{
|
||||||
|
if (left.Type != typeof(TemplatePart) || right.Type != typeof(TemplatePart))
|
||||||
|
throw new InvalidOperationException($"Cannot concatenate expressions of types {left.Type.Name} and {right.Type.Name}");
|
||||||
|
return Expression.Add(left, right, addMethodInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression CreateExpression(string name, Expression value)
|
||||||
|
=> Expression.New(constructorInfo, Expression.Constant(name), value);
|
||||||
|
|
||||||
|
private static readonly ConstructorInfo constructorInfo
|
||||||
|
= typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(string), typeof(string) });
|
||||||
|
|
||||||
|
private static readonly ConstructorInfo tagTemplateConstructorInfo
|
||||||
|
= typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(ITemplateTag), typeof(string) });
|
||||||
|
|
||||||
|
private static readonly MethodInfo addMethodInfo
|
||||||
|
= typeof(TemplatePart).GetMethod(nameof(Concatenate), BindingFlags.NonPublic | BindingFlags.Static, new Type[] { typeof(TemplatePart), typeof(TemplatePart) });
|
||||||
|
|
||||||
|
public IEnumerator<TemplatePart> GetEnumerator()
|
||||||
|
{
|
||||||
|
var firstPart = FirstPart;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (firstPart.TemplateTag is not null || firstPart.TagName is not "Blank")
|
||||||
|
yield return firstPart;
|
||||||
|
firstPart = firstPart.next;
|
||||||
|
}
|
||||||
|
while (firstPart is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
internal TemplatePart FirstPart
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var part = this;
|
||||||
|
while (part.previous is not null)
|
||||||
|
part = part.previous;
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TemplatePart LastPart
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var part = this;
|
||||||
|
while (part.next is not null)
|
||||||
|
part = part.next;
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TemplatePart Concatenate(TemplatePart left, TemplatePart right)
|
||||||
|
{
|
||||||
|
var last = left.LastPart;
|
||||||
|
last.next = right;
|
||||||
|
right.previous = last;
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TemplatePart operator +(TemplatePart left, TemplatePart right)
|
||||||
|
=> Concatenate(left, right);
|
||||||
|
}
|
||||||
@ -11,14 +11,12 @@ using ReactiveUI;
|
|||||||
using Avalonia.Controls.Documents;
|
using Avalonia.Controls.Documents;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml.Templates;
|
||||||
|
|
||||||
namespace LibationAvalonia.Dialogs
|
namespace LibationAvalonia.Dialogs
|
||||||
{
|
{
|
||||||
public partial class EditTemplateDialog : DialogWindow
|
public partial class EditTemplateDialog : DialogWindow
|
||||||
{
|
{
|
||||||
// final value. post-validity check
|
|
||||||
public string TemplateText { get; private set; }
|
|
||||||
|
|
||||||
private EditTemplateViewModel _viewModel;
|
private EditTemplateViewModel _viewModel;
|
||||||
|
|
||||||
public EditTemplateDialog()
|
public EditTemplateDialog()
|
||||||
@ -28,20 +26,21 @@ namespace LibationAvalonia.Dialogs
|
|||||||
if (Design.IsDesignMode)
|
if (Design.IsDesignMode)
|
||||||
{
|
{
|
||||||
_ = Configuration.Instance.LibationFiles;
|
_ = Configuration.Instance.LibationFiles;
|
||||||
_viewModel = new(Configuration.Instance, Templates.File);
|
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||||
_viewModel.resetTextBox(_viewModel.Template.DefaultTemplate);
|
_viewModel = new(Configuration.Instance, editor);
|
||||||
Title = $"Edit {_viewModel.Template.Name}";
|
_viewModel.resetTextBox(editor.EditingTemplate.TemplateText);
|
||||||
|
Title = $"Edit {editor.EditingTemplate.Name}";
|
||||||
DataContext = _viewModel;
|
DataContext = _viewModel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public EditTemplateDialog(Templates template, string inputTemplateText) : this()
|
public EditTemplateDialog(ITemplateEditor templateEditor) : this()
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(template, nameof(template));
|
ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
||||||
|
|
||||||
_viewModel = new EditTemplateViewModel(Configuration.Instance, template);
|
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
||||||
_viewModel.resetTextBox(inputTemplateText);
|
_viewModel.resetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||||
Title = $"Edit {template.Name}";
|
Title = $"Edit {templateEditor.EditingTemplate.Name}";
|
||||||
DataContext = _viewModel;
|
DataContext = _viewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +63,6 @@ namespace LibationAvalonia.Dialogs
|
|||||||
if (!await _viewModel.Validate())
|
if (!await _viewModel.Validate())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
TemplateText = _viewModel.workingTemplateText;
|
|
||||||
await base.SaveAndCloseAsync();
|
await base.SaveAndCloseAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,23 +70,25 @@ namespace LibationAvalonia.Dialogs
|
|||||||
=> await SaveAndCloseAsync();
|
=> await SaveAndCloseAsync();
|
||||||
|
|
||||||
public void ResetButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public void ResetButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
=> _viewModel.resetTextBox(_viewModel.Template.DefaultTemplate);
|
=> _viewModel.resetTextBox(_viewModel.TemplateEditor.DefaultTemplate);
|
||||||
|
|
||||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly Configuration config;
|
private readonly Configuration config;
|
||||||
public FontFamily FontFamily { get; } = FontManager.Current.DefaultFontFamilyName;
|
public FontFamily FontFamily { get; } = FontManager.Current.DefaultFontFamilyName;
|
||||||
public InlineCollection Inlines { get; } = new();
|
public InlineCollection Inlines { get; } = new();
|
||||||
public Templates Template { get; }
|
public ITemplateEditor TemplateEditor { get; }
|
||||||
public EditTemplateViewModel(Configuration configuration, Templates templates)
|
public EditTemplateViewModel(Configuration configuration, ITemplateEditor templates)
|
||||||
{
|
{
|
||||||
config = configuration;
|
config = configuration;
|
||||||
Template = templates;
|
TemplateEditor = templates;
|
||||||
Description = templates.Description;
|
Description = templates.EditingTemplate.Description;
|
||||||
ListItems
|
ListItems
|
||||||
= new AvaloniaList<Tuple<string, string, string>>(
|
= new AvaloniaList<Tuple<string, string, string>>(
|
||||||
Template
|
TemplateEditor
|
||||||
.GetTemplateTags()
|
.EditingTemplate
|
||||||
|
.TagsRegistered
|
||||||
|
.Cast<TemplateTags>()
|
||||||
.Select(
|
.Select(
|
||||||
t => new Tuple<string, string, string>(
|
t => new Tuple<string, string, string>(
|
||||||
$"<{t.TagName.Replace("->", "-\x200C>").Replace("<-", "<\x200C-")}>",
|
$"<{t.TagName.Replace("->", "-\x200C>").Replace("<-", "<\x200C-")}>",
|
||||||
@ -111,7 +111,6 @@ namespace LibationAvalonia.Dialogs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string workingTemplateText => Template.Sanitize(UserTemplateText, Configuration.Instance.ReplacementCharacters);
|
|
||||||
private string _warningText;
|
private string _warningText;
|
||||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||||
|
|
||||||
@ -123,78 +122,22 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
public async Task<bool> Validate()
|
public async Task<bool> Validate()
|
||||||
{
|
{
|
||||||
if (Template.IsValid(workingTemplateText))
|
if (TemplateEditor.EditingTemplate.IsValid)
|
||||||
return true;
|
return true;
|
||||||
var errors = Template
|
|
||||||
.GetErrors(workingTemplateText)
|
var errors
|
||||||
.Select(err => $"- {err}")
|
= TemplateEditor
|
||||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
.EditingTemplate
|
||||||
|
.Errors
|
||||||
|
.Select(err => $"- {err}")
|
||||||
|
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void templateTb_TextChanged()
|
private void templateTb_TextChanged()
|
||||||
{
|
{
|
||||||
var isChapterTitle = Template == Templates.ChapterTitle;
|
TemplateEditor.SetTemplateText(UserTemplateText);
|
||||||
var isFolder = Template == Templates.Folder;
|
|
||||||
|
|
||||||
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",
|
|
||||||
YearPublished = 2017,
|
|
||||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
|
||||||
Narrators = new List<string> { "Stephen Fry" },
|
|
||||||
SeriesName = "Sherlock Holmes",
|
|
||||||
SeriesNumber = "1",
|
|
||||||
BitRate = 128,
|
|
||||||
SampleRate = 44100,
|
|
||||||
Channels = 2,
|
|
||||||
Language = "English"
|
|
||||||
};
|
|
||||||
var chapterName = "A Flight for Life";
|
|
||||||
var chapterNumber = 4;
|
|
||||||
var chaptersTotal = 10;
|
|
||||||
|
|
||||||
var partFileProperties = new AaxDecrypter.MultiConvertFileProperties()
|
|
||||||
{
|
|
||||||
OutputFileName = "",
|
|
||||||
PartsPosition = chapterNumber,
|
|
||||||
PartsTotal = chaptersTotal,
|
|
||||||
Title = chapterName
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Path must be rooted for windows to allow long file paths. This is
|
|
||||||
* only necessary for folder templates because they may contain several
|
|
||||||
* subdirectories. Without rooting, we won't be allowed to create a
|
|
||||||
* relative path longer than MAX_PATH.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var books = config.Books;
|
|
||||||
var folder = Templates.Folder.GetPortionFilename(
|
|
||||||
libraryBookDto,
|
|
||||||
Path.Combine(books, isFolder ? workingTemplateText : config.FolderTemplate), "");
|
|
||||||
|
|
||||||
folder = Path.GetRelativePath(books, folder);
|
|
||||||
|
|
||||||
var file
|
|
||||||
= Template == Templates.ChapterFile
|
|
||||||
? Templates.ChapterFile.GetPortionFilename(
|
|
||||||
libraryBookDto,
|
|
||||||
workingTemplateText,
|
|
||||||
partFileProperties,
|
|
||||||
"")
|
|
||||||
: Templates.File.GetPortionFilename(
|
|
||||||
libraryBookDto,
|
|
||||||
isFolder ? config.FileTemplate : workingTemplateText, "");
|
|
||||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
|
||||||
|
|
||||||
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);
|
|
||||||
|
|
||||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||||
var sing = $"{Path.DirectorySeparatorChar}";
|
var sing = $"{Path.DirectorySeparatorChar}";
|
||||||
@ -207,11 +150,12 @@ namespace LibationAvalonia.Dialogs
|
|||||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||||
|
|
||||||
WarningText
|
WarningText
|
||||||
= !Template.HasWarnings(workingTemplateText)
|
= !TemplateEditor.EditingTemplate.HasWarnings
|
||||||
? ""
|
? ""
|
||||||
: "Warning:\r\n" +
|
: "Warning:\r\n" +
|
||||||
Template
|
TemplateEditor
|
||||||
.GetWarnings(workingTemplateText)
|
.EditingTemplate
|
||||||
|
.Warnings
|
||||||
.Select(err => $"- {err}")
|
.Select(err => $"- {err}")
|
||||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
|
|
||||||
@ -220,20 +164,24 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
Inlines.Clear();
|
Inlines.Clear();
|
||||||
|
|
||||||
if (isChapterTitle)
|
if (!TemplateEditor.IsFilePath)
|
||||||
{
|
{
|
||||||
Inlines.Add(new Run(chapterTitle) { FontWeight = bold });
|
Inlines.Add(new Run(TemplateEditor.GetName()) { FontWeight = bold });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Inlines.Add(new Run(slashWrap(books)) { FontWeight = reg });
|
var folder = TemplateEditor.GetFolderName();
|
||||||
|
var file = TemplateEditor.GetFileName();
|
||||||
|
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||||
|
|
||||||
|
Inlines.Add(new Run(slashWrap(TemplateEditor.BaseDirectory.PathWithoutPrefix)) { FontWeight = reg });
|
||||||
Inlines.Add(new Run(sing) { FontWeight = reg });
|
Inlines.Add(new Run(sing) { FontWeight = reg });
|
||||||
|
|
||||||
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = isFolder ? bold : reg });
|
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = TemplateEditor.IsFolder ? bold : reg });
|
||||||
|
|
||||||
Inlines.Add(new Run(sing));
|
Inlines.Add(new Run(sing));
|
||||||
|
|
||||||
Inlines.Add(new Run(slashWrap(file)) { FontWeight = isFolder ? reg : bold });
|
Inlines.Add(new Run(slashWrap(file)) { FontWeight = TemplateEditor.IsFolder ? reg : bold });
|
||||||
|
|
||||||
Inlines.Add(new Run($".{ext}"));
|
Inlines.Add(new Run($".{ext}"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,21 +52,22 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
public async void EditFolderTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public async void EditFolderTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var newTemplate = await editTemplate(Templates.Folder, settingsDisp.DownloadDecryptSettings.FolderTemplate);
|
var newTemplate = await editTemplate(TemplateEditor<Templates.FolderTemplate>.CreateFilenameEditor(config.Books, settingsDisp.DownloadDecryptSettings.FolderTemplate));
|
||||||
if (newTemplate is not null)
|
if (newTemplate is not null)
|
||||||
settingsDisp.DownloadDecryptSettings.FolderTemplate = newTemplate;
|
settingsDisp.DownloadDecryptSettings.FolderTemplate = newTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void EditFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public async void EditFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var newTemplate = await editTemplate(Templates.File, settingsDisp.DownloadDecryptSettings.FileTemplate);
|
var newTemplate = await editTemplate(TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(config.Books, settingsDisp.DownloadDecryptSettings.FileTemplate));
|
||||||
if (newTemplate is not null)
|
if (newTemplate is not null)
|
||||||
settingsDisp.DownloadDecryptSettings.FileTemplate = newTemplate;
|
settingsDisp.DownloadDecryptSettings.FileTemplate = newTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void EditChapterFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public async void EditChapterFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var newTemplate = await editTemplate(Templates.ChapterFile, settingsDisp.DownloadDecryptSettings.ChapterFileTemplate);
|
|
||||||
|
var newTemplate = await editTemplate(TemplateEditor<Templates.ChapterFileTemplate>.CreateFilenameEditor(config.Books, settingsDisp.DownloadDecryptSettings.ChapterFileTemplate));
|
||||||
if (newTemplate is not null)
|
if (newTemplate is not null)
|
||||||
settingsDisp.DownloadDecryptSettings.ChapterFileTemplate = newTemplate;
|
settingsDisp.DownloadDecryptSettings.ChapterFileTemplate = newTemplate;
|
||||||
}
|
}
|
||||||
@ -79,16 +80,16 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var newTemplate = await editTemplate(Templates.ChapterTitle, settingsDisp.AudioSettings.ChapterTitleTemplate);
|
var newTemplate = await editTemplate(TemplateEditor<Templates.ChapterTitleTemplate>.CreateNameEditor(settingsDisp.AudioSettings.ChapterTitleTemplate));
|
||||||
if (newTemplate is not null)
|
if (newTemplate is not null)
|
||||||
settingsDisp.AudioSettings.ChapterTitleTemplate = newTemplate;
|
settingsDisp.AudioSettings.ChapterTitleTemplate = newTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> editTemplate(Templates template, string existingTemplate)
|
private async Task<string> editTemplate(ITemplateEditor template)
|
||||||
{
|
{
|
||||||
var form = new EditTemplateDialog(template, existingTemplate);
|
var form = new EditTemplateDialog(template);
|
||||||
if (await form.ShowDialog<DialogResult>(this) == DialogResult.OK)
|
if (await form.ShowDialog<DialogResult>(this) == DialogResult.OK)
|
||||||
return form.TemplateText;
|
return template.EditingTemplate.TemplateText;
|
||||||
else return null;
|
else return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,28 +267,8 @@ namespace LibationAvalonia.Dialogs
|
|||||||
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SaveSettingsAsync(Configuration config)
|
public Task<bool> SaveSettingsAsync(Configuration config)
|
||||||
{
|
{
|
||||||
static Task validationError(string text, string caption)
|
|
||||||
=> MessageBox.Show(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
|
|
||||||
// these 3 should do nothing. Configuration will only init these with a valid value. EditTemplateDialog ensures valid before returning
|
|
||||||
if (!Templates.Folder.IsValid(FolderTemplate))
|
|
||||||
{
|
|
||||||
await validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Templates.File.IsValid(FileTemplate))
|
|
||||||
{
|
|
||||||
await validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Templates.ChapterFile.IsValid(ChapterFileTemplate))
|
|
||||||
{
|
|
||||||
await validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.BadBook
|
config.BadBook
|
||||||
= BadBookAbort ? Configuration.BadBookAction.Abort
|
= BadBookAbort ? Configuration.BadBookAction.Abort
|
||||||
: BadBookRetry ? Configuration.BadBookAction.Retry
|
: BadBookRetry ? Configuration.BadBookAction.Retry
|
||||||
@ -301,7 +282,7 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
config.UseCoverAsFolderIcon = UseCoverAsFolderIcon;
|
config.UseCoverAsFolderIcon = UseCoverAsFolderIcon;
|
||||||
|
|
||||||
return true;
|
return Task.FromResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
||||||
|
|||||||
@ -80,7 +80,7 @@ namespace LibationFileManager
|
|||||||
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||||
|
|
||||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||||
public string Books { get => GetString(); set => SetString(value); }
|
public LongPath Books { get => GetString(); set => SetString(value); }
|
||||||
|
|
||||||
// temp/working dir(s) should be outside of dropbox
|
// temp/working dir(s) should be outside of dropbox
|
||||||
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
|
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
|
||||||
@ -223,36 +223,41 @@ namespace LibationFileManager
|
|||||||
[Description("How to format the folders in which files will be saved")]
|
[Description("How to format the folders in which files will be saved")]
|
||||||
public string FolderTemplate
|
public string FolderTemplate
|
||||||
{
|
{
|
||||||
get => Templates.Folder.GetValid(GetString(defaultValue: Templates.Folder.DefaultTemplate));
|
get => getTemplate<Templates.FolderTemplate>();
|
||||||
set => setTemplate(Templates.Folder, value);
|
set => setTemplate<Templates.FolderTemplate>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Description("How to format the saved pdf and audio files")]
|
[Description("How to format the saved pdf and audio files")]
|
||||||
public string FileTemplate
|
public string FileTemplate
|
||||||
{
|
{
|
||||||
get => Templates.File.GetValid(GetString(defaultValue: Templates.File.DefaultTemplate));
|
get => getTemplate<Templates.FileTemplate>();
|
||||||
set => setTemplate(Templates.File, value);
|
set => setTemplate<Templates.FileTemplate>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Description("How to format the saved audio files when split by chapters")]
|
[Description("How to format the saved audio files when split by chapters")]
|
||||||
public string ChapterFileTemplate
|
public string ChapterFileTemplate
|
||||||
{
|
{
|
||||||
get => Templates.ChapterFile.GetValid(GetString(defaultValue: Templates.ChapterFile.DefaultTemplate));
|
get => getTemplate<Templates.ChapterFileTemplate>();
|
||||||
set => setTemplate(Templates.ChapterFile, value);
|
set => setTemplate<Templates.ChapterFileTemplate>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Description("How to format the file's Tile stored in metadata")]
|
[Description("How to format the file's Tile stored in metadata")]
|
||||||
public string ChapterTitleTemplate
|
public string ChapterTitleTemplate
|
||||||
{
|
{
|
||||||
get => Templates.ChapterTitle.GetValid(GetString(defaultValue: Templates.ChapterTitle.DefaultTemplate));
|
get => getTemplate<Templates.ChapterTitleTemplate>();
|
||||||
set => setTemplate(Templates.ChapterTitle, value);
|
set => setTemplate<Templates.ChapterTitleTemplate>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTemplate(Templates templ, string newValue, [CallerMemberName] string propertyName = "")
|
private string getTemplate<T>([CallerMemberName] string propertyName = "")
|
||||||
|
where T : Templates, ITemplate, new()
|
||||||
{
|
{
|
||||||
var template = newValue?.Trim();
|
return Templates.GetTemplate<T>(GetString(defaultValue: T.DefaultTemplate, propertyName)).TemplateText;
|
||||||
if (templ.IsValid(template))
|
}
|
||||||
SetString(template, propertyName);
|
|
||||||
|
private void setTemplate<T>(string newValue, [CallerMemberName] string propertyName = "")
|
||||||
|
where T : Templates, ITemplate, new()
|
||||||
|
{
|
||||||
|
SetString(Templates.GetTemplate<T>(newValue).TemplateText, propertyName);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
public string SeriesName { get; set; }
|
public string SeriesName { get; set; }
|
||||||
public string SeriesNumber { get; set; }
|
public string SeriesNumber { get; set; }
|
||||||
|
public bool IsPodcast { get; set; }
|
||||||
|
|
||||||
public int BitRate { get; set; }
|
public int BitRate { get; set; }
|
||||||
public int SampleRate { get; set; }
|
public int SampleRate { get; set; }
|
||||||
|
|||||||
130
Source/LibationFileManager/TemplateEditor[T].cs
Normal file
130
Source/LibationFileManager/TemplateEditor[T].cs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
using AaxDecrypter;
|
||||||
|
using FileManager;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace LibationFileManager
|
||||||
|
{
|
||||||
|
public interface ITemplateEditor
|
||||||
|
{
|
||||||
|
bool IsFolder { get; }
|
||||||
|
bool IsFilePath { get; }
|
||||||
|
LongPath BaseDirectory { get; }
|
||||||
|
string DefaultTemplate { get; }
|
||||||
|
Templates Folder { get; }
|
||||||
|
Templates File { get; }
|
||||||
|
Templates Name { get; }
|
||||||
|
Templates EditingTemplate { get; }
|
||||||
|
void SetTemplateText(string templateText);
|
||||||
|
string GetFolderName();
|
||||||
|
string GetFileName();
|
||||||
|
string GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TemplateEditor<T> : ITemplateEditor where T : Templates, ITemplate, new()
|
||||||
|
{
|
||||||
|
public bool IsFolder => EditingTemplate is Templates.FolderTemplate;
|
||||||
|
public bool IsFilePath => EditingTemplate is not Templates.ChapterTitleTemplate;
|
||||||
|
public LongPath BaseDirectory { get; private init; }
|
||||||
|
public string DefaultTemplate { get; private init; }
|
||||||
|
public Templates Folder { get; private set; }
|
||||||
|
public Templates File { get; private set; }
|
||||||
|
public Templates Name { get; private set; }
|
||||||
|
public Templates EditingTemplate
|
||||||
|
{
|
||||||
|
get => _editingTemplate;
|
||||||
|
private set => _editingTemplate = !IsFilePath ? Name = value : IsFolder ? Folder = value : File = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Templates _editingTemplate;
|
||||||
|
|
||||||
|
public void SetTemplateText(string templateText)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<T>(templateText, out var template);
|
||||||
|
EditingTemplate = template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly LibraryBookDto libraryBookDto
|
||||||
|
= new()
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
YearPublished = 2017,
|
||||||
|
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||||
|
Narrators = new List<string> { "Stephen Fry" },
|
||||||
|
SeriesName = "Sherlock Holmes",
|
||||||
|
SeriesNumber = "1",
|
||||||
|
BitRate = 128,
|
||||||
|
SampleRate = 44100,
|
||||||
|
Channels = 2,
|
||||||
|
Language = "English"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly MultiConvertFileProperties partFileProperties
|
||||||
|
= new()
|
||||||
|
{
|
||||||
|
OutputFileName = "",
|
||||||
|
PartsPosition = 4,
|
||||||
|
PartsTotal = 10,
|
||||||
|
Title = "A Flight for Life"
|
||||||
|
};
|
||||||
|
|
||||||
|
public string GetFolderName()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Path must be rooted for windows to allow long file paths. This is
|
||||||
|
* only necessary for folder templates because they may contain several
|
||||||
|
* subdirectories. Without rooting, we won't be allowed to create a
|
||||||
|
* relative path longer than MAX_PATH.
|
||||||
|
*/
|
||||||
|
var dir = Folder.GetFilename(libraryBookDto, BaseDirectory, "");
|
||||||
|
return Path.GetRelativePath(BaseDirectory, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetFileName()
|
||||||
|
=> File.GetFilename(libraryBookDto, partFileProperties, "", "");
|
||||||
|
public string GetName()
|
||||||
|
=> Name.GetName(libraryBookDto, partFileProperties);
|
||||||
|
|
||||||
|
public static ITemplateEditor CreateFilenameEditor(LongPath baseDir, string templateText)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<T>(templateText, out var template);
|
||||||
|
|
||||||
|
var templateEditor = new TemplateEditor<T>
|
||||||
|
{
|
||||||
|
_editingTemplate = template,
|
||||||
|
BaseDirectory = baseDir,
|
||||||
|
DefaultTemplate = T.DefaultTemplate
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!templateEditor.IsFolder && !templateEditor.IsFilePath)
|
||||||
|
throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates");
|
||||||
|
|
||||||
|
templateEditor.Folder = templateEditor.IsFolder ? template : Templates.Folder;
|
||||||
|
templateEditor.File = templateEditor.IsFolder ? Templates.File : template;
|
||||||
|
|
||||||
|
return templateEditor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ITemplateEditor CreateNameEditor(string templateText)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<T>(templateText, out var nameTemplate);
|
||||||
|
|
||||||
|
var templateEditor = new TemplateEditor<T>
|
||||||
|
{
|
||||||
|
_editingTemplate = nameTemplate,
|
||||||
|
DefaultTemplate = T.DefaultTemplate
|
||||||
|
};
|
||||||
|
|
||||||
|
if (templateEditor.IsFolder || templateEditor.IsFilePath)
|
||||||
|
throw new InvalidOperationException($"This method is only for name templates. Use {nameof(CreateFilenameEditor)} for file templates");
|
||||||
|
|
||||||
|
return templateEditor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +1,27 @@
|
|||||||
using System;
|
using FileManager.NamingTemplate;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Dinah.Core;
|
|
||||||
|
|
||||||
namespace LibationFileManager
|
namespace LibationFileManager
|
||||||
{
|
{
|
||||||
public sealed class TemplateTags : Enumeration<TemplateTags>
|
public sealed class TemplateTags : ITemplateTag
|
||||||
{
|
{
|
||||||
public string TagName => DisplayName;
|
public const string DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||||
public string DefaultValue { get; }
|
public string TagName { get; }
|
||||||
|
public string DefaultValue { get; }
|
||||||
public string Description { get; }
|
public string Description { get; }
|
||||||
public bool IsChapterOnly { get; }
|
public string Display { get; }
|
||||||
|
|
||||||
private static int value = 0;
|
|
||||||
private TemplateTags(string tagName, string description, bool isChapterOnly = false, string defaultValue = null) : base(value++, tagName)
|
|
||||||
{
|
|
||||||
Description = description;
|
|
||||||
IsChapterOnly = isChapterOnly;
|
|
||||||
DefaultValue = defaultValue ?? $"<{tagName}>";
|
|
||||||
|
|
||||||
|
private TemplateTags(string tagName, string description, string defaultValue = null, string display = null)
|
||||||
|
{
|
||||||
|
TagName = tagName;
|
||||||
|
Description = description;
|
||||||
|
DefaultValue = defaultValue ?? $"<{tagName}>";
|
||||||
|
Display = display ?? $"<{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");
|
||||||
public static TemplateTags ChCount { get; } = new TemplateTags("ch count", "Number of chapters", true);
|
public static TemplateTags ChTitle { get; } = new TemplateTags("ch title", "Chapter title");
|
||||||
public static TemplateTags ChTitle { get; } = new TemplateTags("ch title", "Chapter title", true);
|
public static TemplateTags ChNumber { get; } = new TemplateTags("ch#", "Chapter #");
|
||||||
public static TemplateTags ChNumber { get; } = new TemplateTags("ch#", "Chapter #", true);
|
public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter # with leading zeros");
|
||||||
public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter # with leading zeros", true);
|
|
||||||
|
|
||||||
public static TemplateTags Id { get; } = new TemplateTags("id", "Audible ID");
|
public static TemplateTags Id { get; } = new TemplateTags("id", "Audible ID");
|
||||||
public static TemplateTags Title { get; } = new TemplateTags("title", "Full title");
|
public static TemplateTags Title { get; } = new TemplateTags("title", "Full title");
|
||||||
@ -41,16 +37,15 @@ namespace LibationFileManager
|
|||||||
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate");
|
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate");
|
||||||
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels");
|
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels");
|
||||||
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
|
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
|
||||||
public static TemplateTags Locale { get; } = new("locale", "Region/country");
|
public static TemplateTags Locale { get; } = new ("locale", "Region/country");
|
||||||
public static TemplateTags YearPublished { get; } = new("year", "Year published");
|
public static TemplateTags YearPublished { get; } = new("year", "Year published");
|
||||||
public static TemplateTags Language { get; } = new("language", "Book's language");
|
public static TemplateTags Language { get; } = new("language", "Book's language");
|
||||||
public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG");
|
public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG");
|
||||||
|
|
||||||
// Special cases. Aren't mapped to replacements in Templates.cs
|
public static TemplateTags FileDate { get; } = new TemplateTags("file date", "File date/time. e.g. yyyy-MM-dd HH-mm", $"<file date [{DEFAULT_DATE_FORMAT}]>", "<file date [...]>");
|
||||||
// Included here for display by EditTemplateDialog
|
public static TemplateTags DatePublished { get; } = new TemplateTags("pub date", "Publication date. e.g. yyyy-MM-dd", $"<pub date [{DEFAULT_DATE_FORMAT}]>", "<pub date [...]>");
|
||||||
public static TemplateTags FileDate { get; } = new TemplateTags("file date [...]", "File date/time. e.g. yyyy-MM-dd HH-mm", false, $"<file date [{Templates.DEFAULT_DATE_FORMAT}]>");
|
public static TemplateTags DateAdded { get; } = new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"<date added [{DEFAULT_DATE_FORMAT}]>", "<date added [...]>");
|
||||||
public static TemplateTags DatePublished { get; } = new TemplateTags("pub date [...]", "Publication date. e.g. yyyy-MM-dd", false, $"<pub date [{Templates.DEFAULT_DATE_FORMAT}]>");
|
public static TemplateTags IfSeries { get; } = new TemplateTags("if series", "Only include if part of a series", "<if series-><-if series>", "<if series->...<-if series>");
|
||||||
public static TemplateTags DateAdded { get; } = new TemplateTags("date added [...]", "Date added to your Audible account. e.g. yyyy-MM-dd", false, $"<date added [{Templates.DEFAULT_DATE_FORMAT}]>");
|
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 IfSeries { get; } = new TemplateTags("if series->...<-if series", "Only include if part of a series", false, "<if series-><-if series>");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,115 +2,253 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using AaxDecrypter;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Dinah.Core.Collections.Generic;
|
|
||||||
using FileManager;
|
using FileManager;
|
||||||
|
using FileManager.NamingTemplate;
|
||||||
|
|
||||||
namespace LibationFileManager
|
namespace LibationFileManager
|
||||||
{
|
{
|
||||||
|
public interface ITemplate
|
||||||
|
{
|
||||||
|
static abstract string DefaultTemplate { get; }
|
||||||
|
static abstract IEnumerable<TagClass> TagClass { get; }
|
||||||
|
}
|
||||||
|
|
||||||
public abstract class Templates
|
public abstract class Templates
|
||||||
{
|
{
|
||||||
protected static string[] Valid => Array.Empty<string>();
|
|
||||||
public const string ERROR_NULL_IS_INVALID = "Null template is invalid.";
|
|
||||||
public const string ERROR_FULL_PATH_IS_INVALID = @"No colons or full paths allowed. Eg: should not start with C:\";
|
public const string ERROR_FULL_PATH_IS_INVALID = @"No colons or full paths allowed. Eg: should not start with C:\";
|
||||||
public const string ERROR_INVALID_FILE_NAME_CHAR = @"Only file name friendly characters allowed. Eg: no colons or slashes";
|
|
||||||
|
|
||||||
public const string WARNING_EMPTY = "Template is empty.";
|
|
||||||
public const string WARNING_WHITE_SPACE = "Template is white space.";
|
|
||||||
public const string WARNING_NO_TAGS = "Should use tags. Eg: <title>";
|
|
||||||
public const string WARNING_HAS_CHAPTER_TAGS = "Chapter tags should only be used in the template used for naming files which are split by chapter. Eg: <ch title>";
|
|
||||||
public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>";
|
public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>";
|
||||||
|
|
||||||
public static FolderTemplate Folder { get; } = new FolderTemplate();
|
//Assign the properties in the static constructor will require all
|
||||||
public static FileTemplate File { get; } = new FileTemplate();
|
//Templates users to have a valid configuration file. To allow tests
|
||||||
public static ChapterFileTemplate ChapterFile { get; } = new ChapterFileTemplate();
|
//to work without access to Configuration, only load templates on demand.
|
||||||
public static ChapterTitleTemplate ChapterTitle { get; } = new ChapterTitleTemplate();
|
private static FolderTemplate _folder;
|
||||||
|
private static FileTemplate _file;
|
||||||
|
private static ChapterFileTemplate _chapterFile;
|
||||||
|
private static ChapterTitleTemplate _chapterTitle;
|
||||||
|
|
||||||
|
public static FolderTemplate Folder => _folder ??= GetTemplate<FolderTemplate>(Configuration.Instance.FolderTemplate);
|
||||||
|
public static FileTemplate File => _file ??= GetTemplate<FileTemplate>(Configuration.Instance.FileTemplate);
|
||||||
|
public static ChapterFileTemplate ChapterFile => _chapterFile ??= GetTemplate<ChapterFileTemplate>(Configuration.Instance.ChapterFileTemplate);
|
||||||
|
public static ChapterTitleTemplate ChapterTitle => _chapterTitle ??= GetTemplate<ChapterTitleTemplate>(Configuration.Instance.ChapterTitleTemplate);
|
||||||
|
|
||||||
|
#region Template Parsing
|
||||||
|
public static T GetTemplate<T>(string templateText) where T : Templates, ITemplate, new()
|
||||||
|
=> TryGetTemplate<T>(templateText, out var template) ? template : GetDefaultTemplate<T>();
|
||||||
|
|
||||||
|
public static bool TryGetTemplate<T>(string templateText, out T template) where T : Templates, ITemplate, new()
|
||||||
|
{
|
||||||
|
var namingTemplate = NamingTemplate.Parse(templateText, T.TagClass);
|
||||||
|
|
||||||
|
template = new() { Template = namingTemplate };
|
||||||
|
return !namingTemplate.Errors.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T GetDefaultTemplate<T>() where T : Templates, ITemplate, new()
|
||||||
|
=> new() { Template = NamingTemplate.Parse(T.DefaultTemplate, T.TagClass) };
|
||||||
|
|
||||||
|
static Templates()
|
||||||
|
{
|
||||||
|
Configuration.Instance.PropertyChanged += FolderTemplate_PropertyChanged;
|
||||||
|
Configuration.Instance.PropertyChanged += FileTemplate_PropertyChanged;
|
||||||
|
Configuration.Instance.PropertyChanged += ChapterFileTemplate_PropertyChanged;
|
||||||
|
Configuration.Instance.PropertyChanged += ChapterTitleTemplate_PropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
[PropertyChangeFilter(nameof(Configuration.FolderTemplate))]
|
||||||
|
private static void FolderTemplate_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||||
|
{
|
||||||
|
_folder = GetTemplate<FolderTemplate>((string)e.NewValue);
|
||||||
|
}
|
||||||
|
[PropertyChangeFilter(nameof(Configuration.FileTemplate))]
|
||||||
|
private static void FileTemplate_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||||
|
{
|
||||||
|
_file = GetTemplate<FileTemplate>((string)e.NewValue);
|
||||||
|
}
|
||||||
|
[PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))]
|
||||||
|
private static void ChapterFileTemplate_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||||
|
{
|
||||||
|
_chapterFile = GetTemplate<ChapterFileTemplate>((string)e.NewValue);
|
||||||
|
}
|
||||||
|
[PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))]
|
||||||
|
private static void ChapterTitleTemplate_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||||
|
{
|
||||||
|
_chapterTitle = GetTemplate<ChapterTitleTemplate>((string)e.NewValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Template Properties
|
||||||
|
public IEnumerable<TemplateTags> TagsRegistered => Template.TagsRegistered.Cast<TemplateTags>();
|
||||||
|
public IEnumerable<TemplateTags> TagsInUse => Template.TagsInUse.Cast<TemplateTags>();
|
||||||
public abstract string Name { get; }
|
public abstract string Name { get; }
|
||||||
public abstract string Description { get; }
|
public abstract string Description { get; }
|
||||||
public abstract string DefaultTemplate { get; }
|
public string TemplateText => Template.TemplateText;
|
||||||
protected abstract bool IsChapterized { get; }
|
protected NamingTemplate Template { get; private set; }
|
||||||
|
|
||||||
protected Templates() { }
|
#endregion
|
||||||
|
|
||||||
#region validation
|
#region validation
|
||||||
internal string GetValid(string configValue)
|
|
||||||
{
|
|
||||||
var value = configValue?.Trim();
|
|
||||||
return IsValid(value) ? value : DefaultTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract IEnumerable<string> GetErrors(string template);
|
public virtual IEnumerable<string> Errors => Template.Errors;
|
||||||
public bool IsValid(string template) => !GetErrors(template).Any();
|
public bool IsValid => !Errors.Any();
|
||||||
|
|
||||||
public abstract IEnumerable<string> GetWarnings(string template);
|
public virtual IEnumerable<string> Warnings => Template.Warnings;
|
||||||
public bool HasWarnings(string template) => GetWarnings(template).Any();
|
public bool HasWarnings => Warnings.Any();
|
||||||
|
|
||||||
protected static string[] GetFileErrors(string template)
|
|
||||||
{
|
|
||||||
// File name only; not path. all other path chars are valid enough to pass this check and will be handled on final save.
|
|
||||||
|
|
||||||
// null is invalid. whitespace is valid but not recommended
|
|
||||||
if (template is null)
|
|
||||||
return new[] { ERROR_NULL_IS_INVALID };
|
|
||||||
|
|
||||||
if (ReplacementCharacters.ContainsInvalidFilenameChar(template.Replace("<","").Replace(">","")))
|
|
||||||
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
|
|
||||||
|
|
||||||
return Valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected IEnumerable<string> GetStandardWarnings(string template)
|
|
||||||
{
|
|
||||||
var warnings = GetErrors(template).ToList();
|
|
||||||
if (template is null)
|
|
||||||
return warnings;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(template))
|
|
||||||
warnings.Add(WARNING_EMPTY);
|
|
||||||
else if (string.IsNullOrWhiteSpace(template))
|
|
||||||
warnings.Add(WARNING_WHITE_SPACE);
|
|
||||||
|
|
||||||
if (TagCount(template) == 0)
|
|
||||||
warnings.Add(WARNING_NO_TAGS);
|
|
||||||
|
|
||||||
if (!IsChapterized && ContainsChapterOnlyTags(template))
|
|
||||||
warnings.Add(WARNING_HAS_CHAPTER_TAGS);
|
|
||||||
|
|
||||||
return warnings;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal int TagCount(string template)
|
|
||||||
=> GetTemplateTags()
|
|
||||||
// for <id><id> == 1, use:
|
|
||||||
// .Count(t => template.Contains($"<{t.TagName}>"))
|
|
||||||
// .Sum() impl: <id><id> == 2
|
|
||||||
.Sum(t => template.Split($"<{t.TagName}>").Length - 1);
|
|
||||||
|
|
||||||
internal static bool ContainsChapterOnlyTags(string template)
|
|
||||||
=> TemplateTags.GetAll()
|
|
||||||
.Where(t => t.IsChapterOnly)
|
|
||||||
.Any(t => ContainsTag(template, t.TagName));
|
|
||||||
|
|
||||||
internal static bool ContainsTag(string template, string tag) => template.Contains($"<{tag}>");
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region to file name
|
#region to file name
|
||||||
/// <summary>
|
|
||||||
/// EditTemplateDialog: Get template generated filename for portion of path
|
|
||||||
/// </summary>
|
|
||||||
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, string fileExtension)
|
|
||||||
=> string.IsNullOrWhiteSpace(template)
|
|
||||||
? ""
|
|
||||||
: getFileNamingTemplate(libraryBookDto, template, null, fileExtension, Configuration.Instance.ReplacementCharacters)
|
|
||||||
.GetFilePath(fileExtension).PathWithoutPrefix;
|
|
||||||
|
|
||||||
public const string DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
public string GetName(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps)
|
||||||
private static Regex fileDateTagRegex { get; } = new Regex(@"<file\s*?date\s*?(?:\[([^\[\]]*?)\]\s*?)?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
{
|
||||||
private static Regex dateAddedTagRegex { get; } = new Regex(@"<date\s*?added\s*?(?:\[([^\[\]]*?)\]\s*?)?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||||
private static Regex datePublishedTagRegex { get; } = new Regex(@"<pub\s*?date\s*?(?:\[([^\[\]]*?)\]\s*?)?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps));
|
||||||
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
return string.Join("", Template.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||||
|
ArgumentValidator.EnsureNotNull(baseDir, nameof(baseDir));
|
||||||
|
ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension));
|
||||||
|
|
||||||
|
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||||
|
return GetFilename(baseDir, fileExtension, returnFirstExisting, replacements, libraryBookDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir = "", string fileExtension = null, ReplacementCharacters replacements = null)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||||
|
ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps));
|
||||||
|
ArgumentValidator.EnsureNotNull(baseDir, nameof(baseDir));
|
||||||
|
|
||||||
|
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||||
|
fileExtension ??= Path.GetExtension(multiChapProps.OutputFileName);
|
||||||
|
return GetFilename(baseDir, fileExtension, false, replacements, libraryBookDto, multiChapProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
|
||||||
|
=> parts.Select(p => replacements.ReplaceFilenameChars(p.Value));
|
||||||
|
|
||||||
|
private LongPath GetFilename(string baseDir, string fileExtension, bool returnFirstExisting, ReplacementCharacters replacements, params object[] dtos)
|
||||||
|
{
|
||||||
|
var parts = Template.Evaluate(dtos).ToList();
|
||||||
|
|
||||||
|
var pathParts = GetPathParts(GetTemplatePartsStrings(parts, replacements));
|
||||||
|
|
||||||
|
//Remove 1 character from the end of the longest filename part until
|
||||||
|
//the total filename is less than max filename length
|
||||||
|
foreach (var part in pathParts)
|
||||||
|
{
|
||||||
|
while (part.Sum(LongPath.GetFilesystemStringLength) > LongPath.MaxFilenameLength)
|
||||||
|
{
|
||||||
|
int maxLength = part.Max(p => p.Length);
|
||||||
|
var maxEntry = part.First(p => p.Length == maxLength);
|
||||||
|
|
||||||
|
var maxIndex = part.IndexOf(maxEntry);
|
||||||
|
part.RemoveAt(maxIndex);
|
||||||
|
part.Insert(maxIndex, maxEntry.Remove(maxLength - 1, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fullPath = Path.Combine(pathParts.Select(p => string.Join("", p)).Prepend(baseDir).ToArray());
|
||||||
|
|
||||||
|
return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Organize template parts into directories.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A List of template directories. Each directory is a list of template part strings</returns>
|
||||||
|
private List<List<string>> GetPathParts(IEnumerable<string> templateParts)
|
||||||
|
{
|
||||||
|
List<List<string>> directories = new();
|
||||||
|
List<string> dir = new();
|
||||||
|
|
||||||
|
foreach (var part in templateParts)
|
||||||
|
{
|
||||||
|
int slashIndex, lastIndex = 0;
|
||||||
|
while((slashIndex = part.IndexOf(Path.DirectorySeparatorChar, lastIndex)) > -1)
|
||||||
|
{
|
||||||
|
dir.Add(part[lastIndex..slashIndex]);
|
||||||
|
directories.Add(dir);
|
||||||
|
dir = new();
|
||||||
|
|
||||||
|
lastIndex = slashIndex + 1;
|
||||||
|
}
|
||||||
|
dir.Add(part[lastIndex..]);
|
||||||
|
}
|
||||||
|
directories.Add(dir);
|
||||||
|
|
||||||
|
return directories;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Registered Template Properties
|
||||||
|
|
||||||
|
private static readonly PropertyTagClass<LibraryBookDto> filePropertyTags = GetFilePropertyTags();
|
||||||
|
private static readonly ConditionalTagClass<LibraryBookDto> conditionalTags = GetConditionalTags();
|
||||||
|
private static readonly List<TagClass> chapterPropertyTags = GetChapterPropertyTags();
|
||||||
|
|
||||||
|
private static ConditionalTagClass<LibraryBookDto> GetConditionalTags()
|
||||||
|
{
|
||||||
|
ConditionalTagClass<LibraryBookDto> lbConditions = new();
|
||||||
|
|
||||||
|
lbConditions.RegisterCondition(TemplateTags.IfSeries, lb => !string.IsNullOrWhiteSpace(lb.SeriesName));
|
||||||
|
lbConditions.RegisterCondition(TemplateTags.IfPodcast, lb => lb.IsPodcast);
|
||||||
|
|
||||||
|
return lbConditions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PropertyTagClass<LibraryBookDto> GetFilePropertyTags()
|
||||||
|
{
|
||||||
|
PropertyTagClass<LibraryBookDto> lbProperties = new();
|
||||||
|
lbProperties.RegisterProperty(TemplateTags.Id, lb => lb.AudibleProductId);
|
||||||
|
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.Bitrate, lb => lb.BitRate, IntegerFormatter);
|
||||||
|
lbProperties.RegisterProperty(TemplateTags.SampleRate, lb => lb.SampleRate, IntegerFormatter);
|
||||||
|
lbProperties.RegisterProperty(TemplateTags.Channels, lb => lb.Channels, IntegerFormatter);
|
||||||
|
lbProperties.RegisterProperty(TemplateTags.Account, lb => lb.Account, StringFormatter);
|
||||||
|
lbProperties.RegisterProperty(TemplateTags.Locale, lb => lb.Locale, StringFormatter);
|
||||||
|
lbProperties.RegisterProperty(TemplateTags.YearPublished, lb => lb.YearPublished, IntegerFormatter);
|
||||||
|
lbProperties.RegisterProperty(TemplateTags.DatePublished, lb => lb.DatePublished, DateTimeFormatter);
|
||||||
|
lbProperties.RegisterProperty(TemplateTags.DateAdded, lb => lb.DateAdded, DateTimeFormatter);
|
||||||
|
lbProperties.RegisterProperty(TemplateTags.FileDate, lb => lb.FileDate, DateTimeFormatter);
|
||||||
|
return lbProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TagClass> GetChapterPropertyTags()
|
||||||
|
{
|
||||||
|
PropertyTagClass<LibraryBookDto> lbProperties = new();
|
||||||
|
PropertyTagClass<MultiConvertFileProperties> 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 ?? "");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
return new List<TagClass> { lbProperties, multiConvertProperties };
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Tag Formatters
|
||||||
|
|
||||||
private static string getLanguageShort(string language)
|
private static string getLanguageShort(string language)
|
||||||
{
|
{
|
||||||
@ -123,330 +261,94 @@ namespace LibationFileManager
|
|||||||
return language[..3].ToUpper();
|
return language[..3].ToUpper();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static FileNamingTemplate getFileNamingTemplate(LibraryBookDto libraryBookDto, string template, string dirFullPath, string extension, ReplacementCharacters replacements)
|
private static string StringFormatter(ITemplateTag templateTag, string value, string formatString)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
if (string.Compare(formatString, "u", ignoreCase: true) == 0) return value?.ToUpper();
|
||||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
else if (string.Compare(formatString, "l", ignoreCase: true) == 0) return value?.ToLower();
|
||||||
|
else return value;
|
||||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
|
||||||
dirFullPath = dirFullPath?.Trim() ?? "";
|
|
||||||
|
|
||||||
// for non-series, remove <if series-> and <-if series> tags and everything in between
|
|
||||||
// for series, remove <if series-> and <-if series> tags, what's in between will remain
|
|
||||||
template = ifSeriesRegex.Replace(
|
|
||||||
template,
|
|
||||||
string.IsNullOrWhiteSpace(libraryBookDto.SeriesName) ? "" : "$1");
|
|
||||||
|
|
||||||
//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, replacements);
|
|
||||||
|
|
||||||
var title = libraryBookDto.Title ?? "";
|
|
||||||
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
|
|
||||||
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Id, libraryBookDto.AudibleProductId);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Title, title);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.TitleShort, titleShort);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Author, libraryBookDto.AuthorNames);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.FirstAuthor, libraryBookDto.FirstAuthor);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Narrator, libraryBookDto.NarratorNames);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.FirstNarrator, libraryBookDto.FirstNarrator);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Series, libraryBookDto.SeriesName);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.SeriesNumber, libraryBookDto.SeriesNumber);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Bitrate, libraryBookDto.BitRate);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.SampleRate, libraryBookDto.SampleRate);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Channels, libraryBookDto.Channels);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Account, libraryBookDto.Account);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Locale, libraryBookDto.Locale);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.YearPublished, libraryBookDto.YearPublished?.ToString() ?? "1900");
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Language, libraryBookDto.Language);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.LanguageShort, getLanguageShort(libraryBookDto.Language));
|
|
||||||
|
|
||||||
//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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string IntegerFormatter(ITemplateTag templateTag, int value, string formatString)
|
||||||
|
{
|
||||||
|
if (int.TryParse(formatString, out var numDigits))
|
||||||
|
return value.ToString($"D{numDigits}");
|
||||||
|
return value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DateTimeFormatter(ITemplateTag templateTag, DateTime value, string formatString)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(formatString))
|
||||||
|
return value.ToString(TemplateTags.DEFAULT_DATE_FORMAT);
|
||||||
|
return value.ToString(formatString);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region DateTime Tags
|
public class FolderTemplate : Templates, ITemplate
|
||||||
|
|
||||||
/// <param name="template">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.</param>
|
|
||||||
/// <returns>A list of parameter replacement key-value pairs</returns>
|
|
||||||
private static List<KeyValuePair<string, object>> getSanitizeDateReplacementParameters(Regex datePattern, ref string template, ReplacementCharacters replacements, DateTime? dateTime)
|
|
||||||
{
|
|
||||||
List<KeyValuePair<string, object>> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <returns>a date parameter replacement tag with the format string sanitized</returns>
|
|
||||||
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
|
|
||||||
{
|
|
||||||
formattedDateString = replacements.ReplaceFilenameChars(dateTime.Value.ToString(sanitizedFormatter)).Trim();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
formattedDateString = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public virtual IEnumerable<TemplateTags> 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, 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);
|
|
||||||
|
|
||||||
// don't allow double slashes
|
|
||||||
var sing = $"{Path.DirectorySeparatorChar}";
|
|
||||||
var dbl = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}";
|
|
||||||
while (value.Contains(dbl))
|
|
||||||
value = value.Replace(dbl, sing);
|
|
||||||
|
|
||||||
// trim. don't start or end with slash
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var start = value.Length;
|
|
||||||
value = value
|
|
||||||
.Trim()
|
|
||||||
.Trim(Path.DirectorySeparatorChar);
|
|
||||||
var end = value.Length;
|
|
||||||
if (start == end)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class FolderTemplate : Templates
|
|
||||||
{
|
{
|
||||||
public override string Name => "Folder Template";
|
public override string Name => "Folder Template";
|
||||||
public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||||
public override string DefaultTemplate { get; } = "<title short> [<id>]";
|
public static string DefaultTemplate { get; } = "<title short> [<id>]";
|
||||||
protected override bool IsChapterized { get; } = false;
|
public static IEnumerable<TagClass> TagClass => new TagClass[] { filePropertyTags, conditionalTags };
|
||||||
|
|
||||||
internal FolderTemplate() : base() { }
|
public override IEnumerable<string> Errors
|
||||||
|
=> TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors;
|
||||||
|
|
||||||
#region validation
|
protected override List<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
|
||||||
public override IEnumerable<string> GetErrors(string template)
|
|
||||||
{
|
{
|
||||||
// null is invalid. whitespace is valid but not recommended
|
foreach (var tp in parts)
|
||||||
if (template is null)
|
{
|
||||||
return new[] { ERROR_NULL_IS_INVALID };
|
//FolderTemplate literals can have directory separator characters
|
||||||
|
if (tp.TemplateTag is null)
|
||||||
|
tp.Value = replacements.ReplacePathChars(tp.Value.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar));
|
||||||
|
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);
|
||||||
|
|
||||||
// must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save.
|
if (parts[^1].Value.Length > 0 && parts[^1].Value[^1] == Path.DirectorySeparatorChar)
|
||||||
if (template.Contains(':'))
|
parts[^1].Value = parts[^1].Value.Remove(parts[^1].Value.Length - 1, 1);
|
||||||
return new[] { ERROR_FULL_PATH_IS_INVALID };
|
}
|
||||||
|
return parts.Select(p => p.Value).ToList();
|
||||||
// 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 Valid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<string> GetWarnings(string template) => GetStandardWarnings(template);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region to file name
|
|
||||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
|
||||||
public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null)
|
|
||||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null, Configuration.Instance.ReplacementCharacters)
|
|
||||||
.GetFilePath(string.Empty);
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FileTemplate : Templates
|
public class FileTemplate : Templates, ITemplate
|
||||||
{
|
{
|
||||||
public override string Name => "File Template";
|
public override string Name => "File Template";
|
||||||
public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||||
public override string DefaultTemplate { get; } = "<title> [<id>]";
|
public static string DefaultTemplate { get; } = "<title> [<id>]";
|
||||||
protected override bool IsChapterized { get; } = false;
|
public static IEnumerable<TagClass> TagClass { get; } = new TagClass[] { filePropertyTags, conditionalTags };
|
||||||
|
|
||||||
internal FileTemplate() : base() { }
|
|
||||||
|
|
||||||
#region validation
|
|
||||||
public override IEnumerable<string> GetErrors(string template) => GetFileErrors(template);
|
|
||||||
|
|
||||||
public override IEnumerable<string> GetWarnings(string template) => GetStandardWarnings(template);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region to file name
|
|
||||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
|
||||||
public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false)
|
|
||||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension, Configuration.Instance.ReplacementCharacters)
|
|
||||||
.GetFilePath(extension, returnFirstExisting);
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ChapterFileTemplate : Templates
|
public class ChapterFileTemplate : Templates, ITemplate
|
||||||
{
|
{
|
||||||
public override string Name => "Chapter File Template";
|
public override string Name => "Chapter File Template";
|
||||||
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||||
public override string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
|
public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
|
||||||
protected override bool IsChapterized { get; } = true;
|
public static IEnumerable<TagClass> TagClass { get; }
|
||||||
|
= chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags);
|
||||||
|
|
||||||
internal ChapterFileTemplate() : base() { }
|
public override IEnumerable<string> Warnings
|
||||||
|
=> Template.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName))
|
||||||
#region validation
|
? base.Warnings
|
||||||
public override IEnumerable<string> GetErrors(string template) => GetFileErrors(template);
|
: base.Warnings.Append(WARNING_NO_CHAPTER_NUMBER_TAG);
|
||||||
|
|
||||||
public override IEnumerable<string> GetWarnings(string template)
|
|
||||||
{
|
|
||||||
var warnings = GetStandardWarnings(template).ToList();
|
|
||||||
if (template is null)
|
|
||||||
return warnings;
|
|
||||||
|
|
||||||
// recommended to incl. <ch#> or <ch# 0>
|
|
||||||
if (!ContainsTag(template, TemplateTags.ChNumber.TagName) && !ContainsTag(template, TemplateTags.ChNumber0.TagName))
|
|
||||||
warnings.Add(WARNING_NO_CHAPTER_NUMBER_TAG);
|
|
||||||
|
|
||||||
return warnings;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region to file name
|
|
||||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
|
||||||
public string GetFilename(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props)
|
|
||||||
=> GetPortionFilename(libraryBookDto, Configuration.Instance.ChapterFileTemplate, props, AudibleFileStorage.DecryptInProgressDirectory);
|
|
||||||
|
|
||||||
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath, ReplacementCharacters replacements = null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(template)) return string.Empty;
|
|
||||||
|
|
||||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
|
||||||
var fileExtension = Path.GetExtension(props.OutputFileName);
|
|
||||||
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 ?? "");
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ChapterTitleTemplate : Templates
|
public class ChapterTitleTemplate : Templates, ITemplate
|
||||||
{
|
{
|
||||||
private List<TemplateTags> _templateTags { get; } = new()
|
|
||||||
{
|
|
||||||
TemplateTags.Title,
|
|
||||||
TemplateTags.TitleShort,
|
|
||||||
TemplateTags.Series,
|
|
||||||
TemplateTags.ChCount,
|
|
||||||
TemplateTags.ChNumber,
|
|
||||||
TemplateTags.ChNumber0,
|
|
||||||
TemplateTags.ChTitle,
|
|
||||||
};
|
|
||||||
public override string Name => "Chapter Title Template";
|
public override string Name => "Chapter Title Template";
|
||||||
|
|
||||||
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
||||||
|
public static string DefaultTemplate => "<ch#> - <title short>: <ch title>";
|
||||||
|
public static IEnumerable<TagClass> TagClass { get; }
|
||||||
|
= chapterPropertyTags.Append(conditionalTags);
|
||||||
|
|
||||||
public override string DefaultTemplate => "<ch#> - <title short>: <ch title>";
|
protected override IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
|
||||||
|
=> parts.Select(p => p.Value);
|
||||||
protected override bool IsChapterized => true;
|
|
||||||
|
|
||||||
public override IEnumerable<string> GetErrors(string template)
|
|
||||||
=> new List<string>();
|
|
||||||
|
|
||||||
public override IEnumerable<string> GetWarnings(string template)
|
|
||||||
=> GetStandardWarnings(template).ToList();
|
|
||||||
|
|
||||||
public string GetTitle(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props)
|
|
||||||
=> GetPortionTitle(libraryBookDto, Configuration.Instance.ChapterTitleTemplate, props);
|
|
||||||
|
|
||||||
public string GetPortionTitle(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(template)) return string.Empty;
|
|
||||||
|
|
||||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
|
||||||
|
|
||||||
var fileNamingTemplate = new MetadataNamingTemplate(template);
|
|
||||||
|
|
||||||
var title = libraryBookDto.Title ?? "";
|
|
||||||
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
|
|
||||||
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Title, title);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.TitleShort, titleShort);
|
|
||||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Series, libraryBookDto.SeriesName);
|
|
||||||
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.GetTagContents();
|
|
||||||
}
|
|
||||||
public override IEnumerable<TemplateTags> GetTemplateTags() => _templateTags;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using FileManager;
|
|
||||||
|
|
||||||
namespace LibationFileManager
|
|
||||||
{
|
|
||||||
public static class UtilityExtensions
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
@ -10,33 +9,19 @@ namespace LibationWinForms.Dialogs
|
|||||||
{
|
{
|
||||||
public partial class EditTemplateDialog : Form
|
public partial class EditTemplateDialog : Form
|
||||||
{
|
{
|
||||||
// final value. post-validity check
|
private void resetTextBox(string value) => this.templateTb.Text = value;
|
||||||
public string TemplateText { get; private set; }
|
|
||||||
|
|
||||||
// hold the work-in-progress value. not guaranteed to be valid
|
|
||||||
private string _workingTemplateText;
|
|
||||||
private string workingTemplateText
|
|
||||||
{
|
|
||||||
get => _workingTemplateText;
|
|
||||||
set => _workingTemplateText = template.Sanitize(value, Configuration.Instance.ReplacementCharacters);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetTextBox(string value) => this.templateTb.Text = workingTemplateText = value;
|
|
||||||
|
|
||||||
private Configuration config { get; } = Configuration.Instance;
|
private Configuration config { get; } = Configuration.Instance;
|
||||||
|
private ITemplateEditor templateEditor { get;}
|
||||||
private Templates template { get; }
|
|
||||||
private string inputTemplateText { get; }
|
|
||||||
|
|
||||||
public EditTemplateDialog()
|
public EditTemplateDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
this.SetLibationIcon();
|
this.SetLibationIcon();
|
||||||
}
|
}
|
||||||
public EditTemplateDialog(Templates template, string inputTemplateText) : this()
|
|
||||||
|
public EditTemplateDialog(ITemplateEditor templateEditor) : this()
|
||||||
{
|
{
|
||||||
this.template = ArgumentValidator.EnsureNotNull(template, nameof(template));
|
this.templateEditor = ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
||||||
this.inputTemplateText = inputTemplateText ?? "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EditTemplateDialog_Load(object sender, EventArgs e)
|
private void EditTemplateDialog_Load(object sender, EventArgs e)
|
||||||
@ -44,89 +29,31 @@ namespace LibationWinForms.Dialogs
|
|||||||
if (this.DesignMode)
|
if (this.DesignMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (template is null)
|
if (templateEditor is null)
|
||||||
{
|
{
|
||||||
MessageBoxLib.ShowAdminAlert(this, $"Programming error. {nameof(EditTemplateDialog)} was not created correctly", "Edit template error", new NullReferenceException($"{nameof(template)} is null"));
|
MessageBoxLib.ShowAdminAlert(this, $"Programming error. {nameof(EditTemplateDialog)} was not created correctly", "Edit template error", new NullReferenceException($"{nameof(templateEditor)} is null"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
warningsLbl.Text = "";
|
warningsLbl.Text = "";
|
||||||
|
|
||||||
this.Text = $"Edit {template.Name}";
|
this.Text = $"Edit {templateEditor.EditingTemplate.Name}";
|
||||||
|
|
||||||
this.templateLbl.Text = template.Description;
|
this.templateLbl.Text = templateEditor.EditingTemplate.Description;
|
||||||
resetTextBox(inputTemplateText);
|
resetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||||
|
|
||||||
// populate list view
|
// populate list view
|
||||||
foreach (var tag in template.GetTemplateTags())
|
foreach (TemplateTags tag in templateEditor.EditingTemplate.TagsRegistered)
|
||||||
listView1.Items.Add(new ListViewItem(new[] { $"<{tag.TagName}>", tag.Description }) { Tag = tag.DefaultValue });
|
listView1.Items.Add(new ListViewItem(new[] { tag.Display, tag.Description }) { Tag = tag.DefaultValue });
|
||||||
|
|
||||||
|
listView1.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetToDefaultBtn_Click(object sender, EventArgs e) => resetTextBox(template.DefaultTemplate);
|
private void resetToDefaultBtn_Click(object sender, EventArgs e) => resetTextBox(templateEditor.DefaultTemplate);
|
||||||
|
|
||||||
private void templateTb_TextChanged(object sender, EventArgs e)
|
private void templateTb_TextChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
workingTemplateText = templateTb.Text;
|
templateEditor.SetTemplateText(templateTb.Text);
|
||||||
var isChapterTitle = template == Templates.ChapterTitle;
|
|
||||||
var isFolder = template == Templates.Folder;
|
|
||||||
|
|
||||||
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",
|
|
||||||
YearPublished = 2017,
|
|
||||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
|
||||||
Narrators = new List<string> { "Stephen Fry" },
|
|
||||||
SeriesName = "Sherlock Holmes",
|
|
||||||
SeriesNumber = "1",
|
|
||||||
BitRate = 128,
|
|
||||||
SampleRate = 44100,
|
|
||||||
Channels = 2,
|
|
||||||
Language = "English"
|
|
||||||
};
|
|
||||||
var chapterName = "A Flight for Life";
|
|
||||||
var chapterNumber = 4;
|
|
||||||
var chaptersTotal = 10;
|
|
||||||
|
|
||||||
var partFileProperties = new AaxDecrypter.MultiConvertFileProperties()
|
|
||||||
{
|
|
||||||
OutputFileName = "",
|
|
||||||
PartsPosition = chapterNumber,
|
|
||||||
PartsTotal = chaptersTotal,
|
|
||||||
Title = chapterName
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Path must be rooted for windows to allow long file paths. This is
|
|
||||||
* only necessary for folder templates because they may contain several
|
|
||||||
* subdirectories. Without rooting, we won't be allowed to create a
|
|
||||||
* relative path longer than MAX_PATH.
|
|
||||||
*/
|
|
||||||
var books = config.Books;
|
|
||||||
var folder = Templates.Folder.GetPortionFilename(
|
|
||||||
libraryBookDto,
|
|
||||||
Path.Combine(books, isFolder ? workingTemplateText : config.FolderTemplate), "");
|
|
||||||
|
|
||||||
folder = Path.GetRelativePath(books, folder);
|
|
||||||
|
|
||||||
var file
|
|
||||||
= template == Templates.ChapterFile
|
|
||||||
? Templates.ChapterFile.GetPortionFilename(
|
|
||||||
libraryBookDto,
|
|
||||||
workingTemplateText,
|
|
||||||
partFileProperties,
|
|
||||||
"")
|
|
||||||
: Templates.File.GetPortionFilename(
|
|
||||||
libraryBookDto,
|
|
||||||
isFolder ? config.FileTemplate : workingTemplateText, "");
|
|
||||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
|
||||||
|
|
||||||
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);
|
|
||||||
|
|
||||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||||
var sing = $"{Path.DirectorySeparatorChar}";
|
var sing = $"{Path.DirectorySeparatorChar}";
|
||||||
@ -139,11 +66,12 @@ namespace LibationWinForms.Dialogs
|
|||||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||||
|
|
||||||
warningsLbl.Text
|
warningsLbl.Text
|
||||||
= !template.HasWarnings(workingTemplateText)
|
= !templateEditor.EditingTemplate.HasWarnings
|
||||||
? ""
|
? ""
|
||||||
: "Warning:\r\n" +
|
: "Warning:\r\n" +
|
||||||
template
|
templateEditor
|
||||||
.GetWarnings(workingTemplateText)
|
.EditingTemplate
|
||||||
|
.Warnings
|
||||||
.Select(err => $"- {err}")
|
.Select(err => $"- {err}")
|
||||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
|
|
||||||
@ -153,51 +81,52 @@ namespace LibationWinForms.Dialogs
|
|||||||
richTextBox1.Clear();
|
richTextBox1.Clear();
|
||||||
richTextBox1.SelectionFont = reg;
|
richTextBox1.SelectionFont = reg;
|
||||||
|
|
||||||
if (isChapterTitle)
|
if (!templateEditor.IsFilePath)
|
||||||
{
|
{
|
||||||
richTextBox1.SelectionFont = bold;
|
richTextBox1.SelectionFont = bold;
|
||||||
richTextBox1.AppendText(chapterTitle);
|
richTextBox1.AppendText(templateEditor.GetName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
richTextBox1.AppendText(slashWrap(books));
|
var folder = templateEditor.GetFolderName();
|
||||||
|
var file = templateEditor.GetFileName();
|
||||||
|
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||||
|
|
||||||
|
richTextBox1.AppendText(slashWrap(templateEditor.BaseDirectory.PathWithoutPrefix));
|
||||||
richTextBox1.AppendText(sing);
|
richTextBox1.AppendText(sing);
|
||||||
|
|
||||||
if (isFolder)
|
if (templateEditor.IsFolder)
|
||||||
richTextBox1.SelectionFont = bold;
|
richTextBox1.SelectionFont = bold;
|
||||||
|
|
||||||
richTextBox1.AppendText(slashWrap(folder));
|
richTextBox1.AppendText(slashWrap(folder));
|
||||||
|
|
||||||
if (isFolder)
|
if (templateEditor.IsFolder)
|
||||||
richTextBox1.SelectionFont = reg;
|
richTextBox1.SelectionFont = reg;
|
||||||
|
|
||||||
richTextBox1.AppendText(sing);
|
richTextBox1.AppendText(sing);
|
||||||
|
|
||||||
if (!isFolder)
|
if (templateEditor.IsFilePath && !templateEditor.IsFolder)
|
||||||
richTextBox1.SelectionFont = bold;
|
richTextBox1.SelectionFont = bold;
|
||||||
|
|
||||||
richTextBox1.AppendText(file);
|
richTextBox1.AppendText(file);
|
||||||
|
|
||||||
if (!isFolder)
|
richTextBox1.SelectionFont = reg;
|
||||||
richTextBox1.SelectionFont = reg;
|
|
||||||
|
|
||||||
richTextBox1.AppendText($".{ext}");
|
richTextBox1.AppendText($".{ext}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveBtn_Click(object sender, EventArgs e)
|
private void saveBtn_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (!template.IsValid(workingTemplateText))
|
if (!templateEditor.EditingTemplate.IsValid)
|
||||||
{
|
{
|
||||||
var errors = template
|
var errors = templateEditor
|
||||||
.GetErrors(workingTemplateText)
|
.EditingTemplate
|
||||||
|
.Errors
|
||||||
.Select(err => $"- {err}")
|
.Select(err => $"- {err}")
|
||||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TemplateText = workingTemplateText;
|
|
||||||
|
|
||||||
this.DialogResult = DialogResult.OK;
|
this.DialogResult = DialogResult.OK;
|
||||||
this.Close();
|
this.Close();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,7 +106,8 @@ namespace LibationWinForms.Dialogs
|
|||||||
chapterTitleTemplateGb.Enabled = splitFilesByChapterCbox.Checked;
|
chapterTitleTemplateGb.Enabled = splitFilesByChapterCbox.Checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void chapterTitleTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterTitle, chapterTitleTemplateTb);
|
private void chapterTitleTemplateBtn_Click(object sender, EventArgs e)
|
||||||
|
=> editTemplate(TemplateEditor<Templates.ChapterTitleTemplate>.CreateNameEditor(chapterTitleTemplateTb.Text), chapterTitleTemplateTb);
|
||||||
|
|
||||||
private void convertFormatRb_CheckedChanged(object sender, EventArgs e)
|
private void convertFormatRb_CheckedChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -7,10 +7,12 @@ namespace LibationWinForms.Dialogs
|
|||||||
{
|
{
|
||||||
public partial class SettingsDialog
|
public partial class SettingsDialog
|
||||||
{
|
{
|
||||||
private void folderTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.Folder, folderTemplateTb);
|
private void folderTemplateBtn_Click(object sender, EventArgs e)
|
||||||
private void fileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.File, fileTemplateTb);
|
=> editTemplate(TemplateEditor<Templates.FolderTemplate>.CreateFilenameEditor(config.Books, folderTemplateTb.Text), folderTemplateTb);
|
||||||
private void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb);
|
private void fileTemplateBtn_Click(object sender, EventArgs e)
|
||||||
|
=> editTemplate(TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(config.Books, fileTemplateTb.Text), fileTemplateTb);
|
||||||
|
private void chapterFileTemplateBtn_Click(object sender, EventArgs e)
|
||||||
|
=> editTemplate(TemplateEditor<Templates.ChapterFileTemplate>.CreateFilenameEditor(config.Books, chapterFileTemplateTb.Text), chapterFileTemplateTb);
|
||||||
|
|
||||||
private void editCharreplacementBtn_Click(object sender, EventArgs e)
|
private void editCharreplacementBtn_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -56,23 +56,6 @@ namespace LibationWinForms.Dialogs
|
|||||||
validationError("Cannot set Books Location to blank", "Location is blank");
|
validationError("Cannot set Books Location to blank", "Location is blank");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// these 3 should do nothing. Configuration will only init these with a valid value. EditTemplateDialog ensures valid before returning
|
|
||||||
if (!Templates.Folder.IsValid(folderTemplateTb.Text))
|
|
||||||
{
|
|
||||||
validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Templates.File.IsValid(fileTemplateTb.Text))
|
|
||||||
{
|
|
||||||
validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Templates.ChapterFile.IsValid(chapterFileTemplateTb.Text))
|
|
||||||
{
|
|
||||||
validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
LongPath lonNewBooks = newBooks;
|
LongPath lonNewBooks = newBooks;
|
||||||
|
|||||||
@ -27,11 +27,11 @@ namespace LibationWinForms.Dialogs
|
|||||||
Load_AudioSettings(config);
|
Load_AudioSettings(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void editTemplate(Templates template, TextBox textBox)
|
private static void editTemplate(ITemplateEditor template, TextBox textBox)
|
||||||
{
|
{
|
||||||
var form = new EditTemplateDialog(template, textBox.Text);
|
var form = new EditTemplateDialog(template);
|
||||||
if (form.ShowDialog() == DialogResult.OK)
|
if (form.ShowDialog() == DialogResult.OK)
|
||||||
textBox.Text = form.TemplateText;
|
textBox.Text = template.EditingTemplate.TemplateText;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveBtn_Click(object sender, EventArgs e)
|
private void saveBtn_Click(object sender, EventArgs e)
|
||||||
|
|||||||
@ -1,82 +1,184 @@
|
|||||||
using System;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
using FileManager.NamingTemplate;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using Dinah.Core;
|
|
||||||
using FileManager;
|
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
namespace FileNamingTemplateTests
|
namespace NamingTemplateTests
|
||||||
{
|
{
|
||||||
[TestClass]
|
class TemplateTag : ITemplateTag
|
||||||
public class GetFilePath
|
|
||||||
{
|
{
|
||||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
|
public string TagName { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
class PropertyClass1
|
||||||
[DataRow(@"C:\foo\bar", @"C:\foo\bar\my꞉ book 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt", PlatformID.Win32NT)]
|
{
|
||||||
[DataRow(@"/foo/bar", @"/foo/bar/my: book 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt", PlatformID.Unix)]
|
public string Item1 { get; set; }
|
||||||
public void equiv_GetValidFilename(string dirFullPath, string expected, PlatformID platformID)
|
public string Item2 { get; set; }
|
||||||
|
public string Item3 { get; set; }
|
||||||
|
public int Int1 { get; set; }
|
||||||
|
public bool Condition { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class PropertyClass2
|
||||||
|
{
|
||||||
|
public string Item1 { get; set; }
|
||||||
|
public string Item2 { get; set; }
|
||||||
|
public string Item3 { get; set; }
|
||||||
|
public string Item4 { get; set; }
|
||||||
|
public bool Condition { get; set; }
|
||||||
|
}
|
||||||
|
class PropertyClass3
|
||||||
|
{
|
||||||
|
public string Item1 { get; set; }
|
||||||
|
public string Item2 { get; set; }
|
||||||
|
public string Item3 { get; set; }
|
||||||
|
public string Item4 { get; set; }
|
||||||
|
public int? Int2 { get; set; }
|
||||||
|
public bool Condition { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class GetPortionFilename
|
||||||
|
{
|
||||||
|
PropertyTagClass<PropertyClass1> props1 = new();
|
||||||
|
PropertyTagClass<PropertyClass2> props2 = new();
|
||||||
|
PropertyTagClass<PropertyClass3> props3 = new();
|
||||||
|
ConditionalTagClass<PropertyClass1> conditional1 = new();
|
||||||
|
ConditionalTagClass<PropertyClass2> conditional2 = new();
|
||||||
|
ConditionalTagClass<PropertyClass3> conditional3 = new();
|
||||||
|
|
||||||
|
PropertyClass1 propertyClass1 = new()
|
||||||
{
|
{
|
||||||
if (Environment.OSVersion.Platform != platformID)
|
Item1 = "prop1_item1",
|
||||||
return;
|
Item2 = "prop1_item2",
|
||||||
|
Item3 = "prop1_item3",
|
||||||
|
Int1 = 55,
|
||||||
|
Condition = true,
|
||||||
|
};
|
||||||
|
|
||||||
var sb = new System.Text.StringBuilder();
|
PropertyClass2 propertyClass2 = new()
|
||||||
sb.Append('0', 300);
|
{
|
||||||
var longText = sb.ToString();
|
Item1 = "prop2_item1",
|
||||||
|
Item3 = "prop2_item3",
|
||||||
|
Item4 = "prop2_item4",
|
||||||
|
Condition = false
|
||||||
|
};
|
||||||
|
|
||||||
NEW_GetValidFilename_FileNamingTemplate(dirFullPath, "my: book " + longText, "txt", "ID123456").Should().Be(expected);
|
PropertyClass3 propertyClass3 = new()
|
||||||
|
{
|
||||||
|
Item1 = "prop3_item1",
|
||||||
|
Item2 = "prop3_item2",
|
||||||
|
Item3 = "Prop3_Item3",
|
||||||
|
Item4 = "prop3_item4",
|
||||||
|
Condition = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public GetPortionFilename()
|
||||||
|
{
|
||||||
|
props1.RegisterProperty(new TemplateTag { TagName = "item1" }, i => i.Item1);
|
||||||
|
props1.RegisterProperty(new TemplateTag { TagName = "item2" }, i => i.Item2);
|
||||||
|
props1.RegisterProperty(new TemplateTag { TagName = "item3" }, i => i.Item3);
|
||||||
|
|
||||||
|
props2.RegisterProperty(new TemplateTag { TagName = "item1" }, i => i.Item1);
|
||||||
|
props2.RegisterProperty(new TemplateTag { TagName = "item2" }, i => i.Item2);
|
||||||
|
props2.RegisterProperty(new TemplateTag { TagName = "item3" }, i => i.Item3);
|
||||||
|
props2.RegisterProperty(new TemplateTag { TagName = "item4" }, i => i.Item4);
|
||||||
|
|
||||||
|
props3.RegisterProperty(new TemplateTag { TagName = "item3_1" }, i => i.Item1);
|
||||||
|
props3.RegisterProperty(new TemplateTag { TagName = "item3_2" }, i => i.Item2);
|
||||||
|
props3.RegisterProperty(new TemplateTag { TagName = "item3_3" }, i => i.Item3);
|
||||||
|
props3.RegisterProperty(new TemplateTag { TagName = "item3_4" }, i => i.Item4);
|
||||||
|
|
||||||
|
conditional1.RegisterCondition(new TemplateTag { TagName = "ifc1" }, i => i.Condition);
|
||||||
|
conditional2.RegisterCondition(new TemplateTag { TagName = "ifc2" }, i => i.Condition);
|
||||||
|
conditional3.RegisterCondition(new TemplateTag { TagName = "ifc3" }, i => i.Condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NEW_GetValidFilename_FileNamingTemplate(string dirFullPath, string filename, string extension, string metadataSuffix)
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow("<item1>", "prop1_item1", 1)]
|
||||||
|
[DataRow("< item1>", "< item1>", 0)]
|
||||||
|
[DataRow("<item1 >", "<item1 >", 0)]
|
||||||
|
[DataRow("< item1 >", "< item1 >", 0)]
|
||||||
|
[DataRow("<item3_1>", "prop3_item1", 1)]
|
||||||
|
[DataRow("<item1> <item2> <item3> <item4>", "prop1_item1 prop1_item2 prop1_item3 prop2_item4", 4)]
|
||||||
|
[DataRow("<item3_1> <item3_2> <item3> <item4>", "prop3_item1 prop3_item2 prop1_item3 prop2_item4", 4)]
|
||||||
|
[DataRow("<ifc1-><item1><-ifc1><ifc2-><item4><-ifc2><ifc3-><item3_2><-ifc3>", "prop1_item1prop3_item2", 3)]
|
||||||
|
[DataRow("<ifc1-><ifc3-><item1><ifc2-><item4><-ifc2><item3_2><-ifc3><-ifc1>", "prop1_item1prop3_item2", 3)]
|
||||||
|
[DataRow("<ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3><-ifc1><-ifc2>", "", 3)]
|
||||||
|
public void test(string inStr, string outStr, int numTags)
|
||||||
{
|
{
|
||||||
var template = $"<title> [<id>]";
|
var template = NamingTemplate.Parse(inStr, new TagClass[] { props1, props2, props3, conditional1, conditional2, conditional3 });
|
||||||
|
|
||||||
extension = FileUtility.GetStandardizedExtension(extension);
|
template.TagsInUse.Should().HaveCount(numTags);
|
||||||
var fullfilename = Path.Combine(dirFullPath, template + extension);
|
template.Warnings.Should().HaveCount(numTags > 0 ? 0 : 1);
|
||||||
|
template.Errors.Should().HaveCount(0);
|
||||||
|
|
||||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename, Replacements);
|
var templateText = string.Join("", template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value));
|
||||||
fileNamingTemplate.AddParameterReplacement("title", filename);
|
|
||||||
fileNamingTemplate.AddParameterReplacement("id", metadataSuffix);
|
templateText.Should().Be(outStr);
|
||||||
return fileNamingTemplate.GetFilePath(extension).PathWithoutPrefix;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(@"C:\foo\bar\my file.txt", @"C:\foo\bar\my file - 002 - title.txt", PlatformID.Win32NT)]
|
[DataRow("<ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3><-ifc1><ifc2->", new string[] { "Missing <-ifc2> closing conditional.", "Missing <-ifc2> closing conditional." })]
|
||||||
[DataRow(@"/foo/bar/my file.txt", @"/foo/bar/my file - 002 - title.txt", PlatformID.Unix)]
|
[DataRow("<ifc2-><ifc1-><ifc3-><-ifc3><-ifc1><-ifc2>", new string[] { "Should use tags. Eg: <title>" })]
|
||||||
public void equiv_GetMultipartFileName(string inStr, string outStr, PlatformID platformID)
|
[DataRow("<ifc1-><ifc3-><item1><-ifc3><-ifc1><-ifc2>", new string[] { "Missing <ifc2-> open conditional." })]
|
||||||
|
[DataRow("<ifc1-><ifc3-><-ifc3><-ifc1><-ifc2>", new string[] { "Missing <ifc2-> open conditional.", "Should use tags. Eg: <title>" })]
|
||||||
|
[DataRow("<ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3><-ifc1>", new string[] { "Missing <-ifc2> closing conditional." })]
|
||||||
|
[DataRow("<ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc3>", new string[] { "Missing <-ifc1> closing conditional.", "Missing <-ifc2> closing conditional." })]
|
||||||
|
[DataRow("<ifc2-><ifc1-><ifc3-><item1><item4>", new string[] { "Missing <-ifc3> closing conditional.", "Missing <-ifc1> closing conditional.", "Missing <-ifc2> closing conditional." })]
|
||||||
|
[DataRow("<ifc2-><ifc1-><ifc3-><item1><item4><item3_2><-ifc1><-ifc2>", new string[] { "Missing <-ifc3> closing conditional.", "Missing <-ifc3> closing conditional.", "Missing <-ifc1> closing conditional.", "Missing <-ifc2> closing conditional." })]
|
||||||
|
public void condition_error(string inStr, string[] warnings)
|
||||||
{
|
{
|
||||||
if (Environment.OSVersion.Platform == platformID)
|
var template = NamingTemplate.Parse(inStr, new TagClass[] { props1, props2, props3, conditional1, conditional2, conditional3 });
|
||||||
NEW_GetMultipartFileName_FileNamingTemplate(inStr, 2, 100, "title").Should().Be(outStr);
|
|
||||||
|
template.Errors.Should().HaveCount(0);
|
||||||
|
template.Warnings.Should().BeEquivalentTo(warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NEW_GetMultipartFileName_FileNamingTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
|
|
||||||
{
|
|
||||||
// 1-9 => 1-9
|
|
||||||
// 10-99 => 01-99
|
|
||||||
// 100-999 => 001-999
|
|
||||||
var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
|
|
||||||
|
|
||||||
var estension = Path.GetExtension(originalPath);
|
|
||||||
var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + estension;
|
|
||||||
|
|
||||||
var fileNamingTemplate = new FileNamingTemplate(t, Replacements);
|
|
||||||
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
|
|
||||||
fileNamingTemplate.AddParameterReplacement("title", suffix);
|
|
||||||
return fileNamingTemplate.GetFilePath(estension).PathWithoutPrefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(@"\foo\<title>.txt", @"\foo\sl∕as∕he∕s.txt", PlatformID.Win32NT)]
|
[DataRow("<int1>", "55")]
|
||||||
[DataRow(@"/foo/<title>.txt", @"/foo/s\l∕a\s∕h\e∕s.txt", PlatformID.Unix)]
|
[DataRow("<int1[]>", "55")]
|
||||||
public void remove_slashes(string inStr, string outStr, PlatformID platformID)
|
[DataRow("<int1[5]>", "00055")]
|
||||||
|
[DataRow("<int2>", "")]
|
||||||
|
[DataRow("<int2[]>", "")]
|
||||||
|
[DataRow("<int2[4]>", "")]
|
||||||
|
[DataRow("<item3_format>", "Prop3_Item3")]
|
||||||
|
[DataRow("<item3_format[]>", "Prop3_Item3")]
|
||||||
|
[DataRow("<item3_format[rtreue5]>", "Prop3_Item3")]
|
||||||
|
[DataRow("<item3_format[l]>", "prop3_item3")]
|
||||||
|
[DataRow("<item3_format[u]>", "PROP3_ITEM3")]
|
||||||
|
[DataRow("<item2_2_null>", "")]
|
||||||
|
[DataRow("<item2_2_null[]>", "")]
|
||||||
|
[DataRow("<item2_2_null[l]>", "")]
|
||||||
|
public void formatting(string inStr, string outStr)
|
||||||
{
|
{
|
||||||
if (Environment.OSVersion.Platform == platformID)
|
props1.RegisterProperty(new TemplateTag { TagName = "int1" }, i => i.Int1, formatInt);
|
||||||
|
props3.RegisterProperty(new TemplateTag { TagName = "int2" }, i => i.Int2, formatInt);
|
||||||
|
props3.RegisterProperty(new TemplateTag { TagName = "item3_format" }, i => i.Item3, formatString);
|
||||||
|
props2.RegisterProperty(new TemplateTag { TagName = "item2_2_null" }, i => i.Item2, formatString);
|
||||||
|
|
||||||
|
var template = NamingTemplate.Parse(inStr, new TagClass[] { props1, props2, props3, conditional1, conditional2, conditional3 });
|
||||||
|
|
||||||
|
template.Warnings.Should().HaveCount(0);
|
||||||
|
template.Errors.Should().HaveCount(0);
|
||||||
|
|
||||||
|
var templateText = string.Join("", template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value));
|
||||||
|
|
||||||
|
templateText.Should().Be(outStr);
|
||||||
|
|
||||||
|
string formatInt(ITemplateTag templateTag, int value, string format)
|
||||||
{
|
{
|
||||||
var fileNamingTemplate = new FileNamingTemplate(inStr, Replacements);
|
if (int.TryParse(format, out var numDecs))
|
||||||
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
|
return value.ToString($"D{numDecs}");
|
||||||
fileNamingTemplate.GetFilePath("txt").PathWithoutPrefix.Should().Be(outStr);
|
return value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
string formatString(ITemplateTag templateTag, string value, string formatString)
|
||||||
|
{
|
||||||
|
if (string.Compare(formatString, "u", ignoreCase: true) == 0) return value?.ToUpper();
|
||||||
|
else if (string.Compare(formatString, "l", ignoreCase: true) == 0) return value?.ToLower();
|
||||||
|
else return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using FileManager;
|
using FileManager;
|
||||||
|
using FileManager.NamingTemplate;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
@ -42,46 +43,21 @@ namespace TemplatesTests
|
|||||||
Channels = 2,
|
Channels = 2,
|
||||||
Language = "English"
|
Language = "English"
|
||||||
};
|
};
|
||||||
|
|
||||||
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",
|
|
||||||
BitRate = 128,
|
|
||||||
SampleRate = 44100,
|
|
||||||
Channels = 2,
|
|
||||||
Language = "English"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestClass]
|
|
||||||
public class ContainsChapterOnlyTags
|
|
||||||
{
|
|
||||||
[TestMethod]
|
|
||||||
[DataRow("<ch>", false)]
|
|
||||||
[DataRow("<ch#>", true)]
|
|
||||||
[DataRow("<id>", false)]
|
|
||||||
[DataRow("<id><ch#>", true)]
|
|
||||||
public void Tests(string template, bool expected) => Templates.ContainsChapterOnlyTags(template).Should().Be(expected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class ContainsTag
|
public class ContainsTag
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow("<ch#>", "ch#", true)]
|
[DataRow("<ch#>", 0)]
|
||||||
[DataRow("<id>", "ch#", false)]
|
[DataRow("<id>", 1)]
|
||||||
[DataRow("<id><ch#>", "ch#", true)]
|
[DataRow("<id><ch#>", 1)]
|
||||||
public void Tests(string template, string tag, bool expected) => Templates.ContainsTag(template, tag).Should().Be(expected);
|
public void Tests(string template, int numTags)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||||
|
|
||||||
|
fileTemplate.TagsInUse.Should().HaveCount(numTags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestClass]
|
[TestClass]
|
||||||
@ -89,19 +65,22 @@ namespace TemplatesTests
|
|||||||
{
|
{
|
||||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
|
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
public void template_null(string template)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var t).Should().BeFalse();
|
||||||
|
t.IsValid.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(null, @"C:\", "ext")]
|
[DataRow("")]
|
||||||
[ExpectedException(typeof(ArgumentNullException))]
|
[DataRow(" ")]
|
||||||
public void arg_null_exception(string template, string dirFullPath, string extension)
|
public void template_empty(string template)
|
||||||
=> Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements);
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var t).Should().BeTrue();
|
||||||
[TestMethod]
|
t.Warnings.Should().HaveCount(2);
|
||||||
[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, Replacements);
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow("f.txt", @"C:\foo\bar", "", @"C:\foo\bar\f.txt")]
|
[DataRow("f.txt", @"C:\foo\bar", "", @"C:\foo\bar\f.txt")]
|
||||||
@ -119,10 +98,26 @@ namespace TemplatesTests
|
|||||||
expected = expected.Replace("C:", "").Replace('\\', '/');
|
expected = expected.Replace("C:", "").Replace('\\', '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements)
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||||
.GetFilePath(extension)
|
|
||||||
.PathWithoutPrefix
|
fileTemplate
|
||||||
.Should().Be(expected);
|
.GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements)
|
||||||
|
.PathWithoutPrefix
|
||||||
|
.Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow("<bitrate>Kbps <samplerate>Hz", "128Kbps 44100Hz")]
|
||||||
|
[DataRow("<bitrate>Kbps <samplerate[6]>Hz", "128Kbps 044100Hz")]
|
||||||
|
[DataRow("<bitrate[4]>Kbps <samplerate>Hz", "0128Kbps 44100Hz")]
|
||||||
|
[DataRow("<bitrate[4]>Kbps <titleshort[u]>", "0128Kbps A STUDY IN SCARLET")]
|
||||||
|
[DataRow("<bitrate[4]>Kbps <titleshort[l]>", "0128Kbps a study in scarlet")]
|
||||||
|
[DataRow("<bitrate[4]>Kbps <samplerate[6]>Hz", "0128Kbps 044100Hz")]
|
||||||
|
[DataRow("<bitrate [ 4 ] >Kbps <samplerate [ 6 ] >Hz", "0128Kbps 044100Hz")]
|
||||||
|
public void FormatTags(string template, string expected)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||||
|
fileTemplate.GetFilename(GetLibraryBook(), "", "", Replacements).PathWithoutPrefix.Should().Be(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@ -145,8 +140,9 @@ namespace TemplatesTests
|
|||||||
expected = expected.Replace("C:", "").Replace('\\', '/');
|
expected = expected.Replace("C:", "").Replace('\\', '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements)
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||||
.GetFilePath(extension)
|
fileTemplate
|
||||||
|
.GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements)
|
||||||
.PathWithoutPrefix
|
.PathWithoutPrefix
|
||||||
.Should().Be(expected);
|
.Should().Be(expected);
|
||||||
}
|
}
|
||||||
@ -170,11 +166,12 @@ namespace TemplatesTests
|
|||||||
if (Environment.OSVersion.Platform is not PlatformID.Win32NT)
|
if (Environment.OSVersion.Platform is not PlatformID.Win32NT)
|
||||||
{
|
{
|
||||||
dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/');
|
dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/');
|
||||||
expected = expected.Replace("C:", "").Replace('\\', '/').Replace('<', '<').Replace('>','>');
|
expected = expected.Replace("C:", "").Replace('\\', '/').Replace('<', '<').Replace('>', '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements)
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||||
.GetFilePath(extension)
|
fileTemplate
|
||||||
|
.GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements)
|
||||||
.PathWithoutPrefix
|
.PathWithoutPrefix
|
||||||
.Should().Be(expected);
|
.Should().Be(expected);
|
||||||
}
|
}
|
||||||
@ -191,8 +188,9 @@ namespace TemplatesTests
|
|||||||
expected = expected.Replace("C:", "").Replace('\\', '/');
|
expected = expected.Replace("C:", "").Replace('\\', '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements)
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||||
.GetFilePath(extension)
|
fileTemplate
|
||||||
|
.GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements)
|
||||||
.PathWithoutPrefix
|
.PathWithoutPrefix
|
||||||
.Should().Be(expected);
|
.Should().Be(expected);
|
||||||
}
|
}
|
||||||
@ -208,13 +206,13 @@ namespace TemplatesTests
|
|||||||
{
|
{
|
||||||
if (Environment.OSVersion.Platform == platformID)
|
if (Environment.OSVersion.Platform == platformID)
|
||||||
{
|
{
|
||||||
Templates.File.HasWarnings(template).Should().BeTrue();
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||||
Templates.File.HasWarnings(Templates.File.Sanitize(template, Replacements)).Should().BeFalse();
|
|
||||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements)
|
fileTemplate.HasWarnings.Should().BeFalse();
|
||||||
.GetFilePath(extension)
|
fileTemplate
|
||||||
|
.GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements)
|
||||||
.PathWithoutPrefix
|
.PathWithoutPrefix
|
||||||
.Should().Be(expected);
|
.Should().Be(expected);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,11 +227,15 @@ namespace TemplatesTests
|
|||||||
expected = expected.Replace("C:", "").Replace('\\', '/');
|
expected = expected.Replace("C:", "").Replace('\\', '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
Templates.getFileNamingTemplate(GetLibraryBookWithNullDates(), template, dirFullPath, extension, Replacements)
|
var lbDto = GetLibraryBook();
|
||||||
.GetFilePath(extension)
|
lbDto.DatePublished = null;
|
||||||
|
lbDto.DateAdded = null;
|
||||||
|
|
||||||
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||||
|
fileTemplate
|
||||||
|
.GetFilename(lbDto, dirFullPath, extension, Replacements)
|
||||||
.PathWithoutPrefix
|
.PathWithoutPrefix
|
||||||
.Should().Be(expected);
|
.Should().Be(expected);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@ -242,10 +244,14 @@ namespace TemplatesTests
|
|||||||
public void IfSeries_empty(string directory, string expected, PlatformID platformID)
|
public void IfSeries_empty(string directory, string expected, PlatformID platformID)
|
||||||
{
|
{
|
||||||
if (Environment.OSVersion.Platform == platformID)
|
if (Environment.OSVersion.Platform == platformID)
|
||||||
Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series-><-if series>bar", directory, "ext", Replacements)
|
{
|
||||||
.GetFilePath(".ext")
|
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series-><-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||||
.PathWithoutPrefix
|
|
||||||
.Should().Be(expected);
|
fileTemplate
|
||||||
|
.GetFilename(GetLibraryBook(), directory, "ext", Replacements)
|
||||||
|
.PathWithoutPrefix
|
||||||
|
.Should().Be(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@ -254,10 +260,13 @@ namespace TemplatesTests
|
|||||||
public void IfSeries_no_series(string directory, string expected, PlatformID platformID)
|
public void IfSeries_no_series(string directory, string expected, PlatformID platformID)
|
||||||
{
|
{
|
||||||
if (Environment.OSVersion.Platform == platformID)
|
if (Environment.OSVersion.Platform == platformID)
|
||||||
Templates.getFileNamingTemplate(GetLibraryBook(null), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext", Replacements)
|
{
|
||||||
.GetFilePath(".ext")
|
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series->-<series>-<id>-<-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||||
|
|
||||||
|
fileTemplate.GetFilename(GetLibraryBook(null), directory, "ext", Replacements)
|
||||||
.PathWithoutPrefix
|
.PathWithoutPrefix
|
||||||
.Should().Be(expected);
|
.Should().Be(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@ -266,10 +275,112 @@ namespace TemplatesTests
|
|||||||
public void IfSeries_with_series(string directory, string expected, PlatformID platformID)
|
public void IfSeries_with_series(string directory, string expected, PlatformID platformID)
|
||||||
{
|
{
|
||||||
if (Environment.OSVersion.Platform == platformID)
|
if (Environment.OSVersion.Platform == platformID)
|
||||||
Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext", Replacements)
|
{
|
||||||
.GetFilePath(".ext")
|
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series->-<series>-<id>-<-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||||
.PathWithoutPrefix
|
|
||||||
.Should().Be(expected);
|
fileTemplate
|
||||||
|
.GetFilename(GetLibraryBook(), directory, "ext", Replacements)
|
||||||
|
.PathWithoutPrefix
|
||||||
|
.Should().Be(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
namespace Templates_Other
|
||||||
|
{
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class GetFilePath
|
||||||
|
{
|
||||||
|
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(@"C:\foo\bar", @"C:\foo\bar\Folder\my꞉ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\[ID123456].txt", PlatformID.Win32NT)]
|
||||||
|
[DataRow(@"/foo/bar", @"/foo/bar/Folder/my: book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/[ID123456].txt", PlatformID.Unix)]
|
||||||
|
public void equiv_GetValidFilename(string dirFullPath, string expected, PlatformID platformID)
|
||||||
|
{
|
||||||
|
if (Environment.OSVersion.Platform != platformID)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.Append('0', 300);
|
||||||
|
var longText = sb.ToString();
|
||||||
|
|
||||||
|
NEW_GetValidFilename_FileNamingTemplate(dirFullPath, "my: book " + longText, "txt", "ID123456").Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TemplateTag : ITemplateTag
|
||||||
|
{
|
||||||
|
public string TagName { get; init; }
|
||||||
|
public string DefaultValue { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public string Display { get; }
|
||||||
|
}
|
||||||
|
private static string NEW_GetValidFilename_FileNamingTemplate(string dirFullPath, string filename, string extension, string metadataSuffix)
|
||||||
|
{
|
||||||
|
char slash = Path.DirectorySeparatorChar;
|
||||||
|
|
||||||
|
var template = $"{slash}Folder{slash}<title>{slash}[<id>]{slash}";
|
||||||
|
|
||||||
|
extension = FileUtility.GetStandardizedExtension(extension);
|
||||||
|
|
||||||
|
var lbDto = GetLibraryBook();
|
||||||
|
lbDto.Title = filename;
|
||||||
|
lbDto.AudibleProductId = metadataSuffix;
|
||||||
|
|
||||||
|
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var fileNamingTemplate).Should().BeTrue();
|
||||||
|
|
||||||
|
return fileNamingTemplate.GetFilename(lbDto, dirFullPath, extension, Replacements).PathWithoutPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(@"C:\foo\bar\my file.txt", @"C:\foo\bar\my file - 002 - title.txt", PlatformID.Win32NT)]
|
||||||
|
[DataRow(@"/foo/bar/my file.txt", @"/foo/bar/my file - 002 - title.txt", PlatformID.Unix)]
|
||||||
|
public void equiv_GetMultipartFileName(string inStr, string outStr, PlatformID platformID)
|
||||||
|
{
|
||||||
|
if (Environment.OSVersion.Platform == platformID)
|
||||||
|
NEW_GetMultipartFileName_FileNamingTemplate(inStr, 2, 100, "title").Should().Be(outStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NEW_GetMultipartFileName_FileNamingTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
|
||||||
|
{
|
||||||
|
// 1-9 => 1-9
|
||||||
|
// 10-99 => 01-99
|
||||||
|
// 100-999 => 001-999
|
||||||
|
|
||||||
|
var estension = Path.GetExtension(originalPath);
|
||||||
|
var dir = Path.GetDirectoryName(originalPath);
|
||||||
|
var template = Path.GetFileNameWithoutExtension(originalPath) + " - <ch# 0> - <title>" + estension;
|
||||||
|
|
||||||
|
var lbDto = GetLibraryBook();
|
||||||
|
lbDto.Title = suffix;
|
||||||
|
|
||||||
|
Templates.TryGetTemplate<Templates.ChapterFileTemplate>(template, out var chapterFileTemplate).Should().BeTrue();
|
||||||
|
|
||||||
|
return chapterFileTemplate
|
||||||
|
.GetFilename(lbDto, new AaxDecrypter.MultiConvertFileProperties { Title = suffix, PartsTotal = partsTotal, PartsPosition = partsPosition }, dir, estension, Replacements)
|
||||||
|
.PathWithoutPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(@"\foo\<title>.txt", @"\foo\sl∕as∕he∕s.txt", PlatformID.Win32NT)]
|
||||||
|
[DataRow(@"/foo/<title>.txt", @"/foo/s\l∕a\s∕h\e∕s.txt", PlatformID.Unix)]
|
||||||
|
public void remove_slashes(string inStr, string outStr, PlatformID platformID)
|
||||||
|
{
|
||||||
|
if (Environment.OSVersion.Platform == platformID)
|
||||||
|
{
|
||||||
|
var lbDto = GetLibraryBook();
|
||||||
|
lbDto.Title = @"s\l/a\s/h\e/s";
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(inStr);
|
||||||
|
var fileName = Path.GetFileName(inStr);
|
||||||
|
|
||||||
|
Templates.TryGetTemplate<Templates.FileTemplate>(fileName, out var fileNamingTemplate).Should().BeTrue();
|
||||||
|
|
||||||
|
fileNamingTemplate.GetFilename(lbDto, directory, "txt", Replacements).PathWithoutPrefix.Should().Be(outStr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,7 +391,7 @@ namespace Templates_Folder_Tests
|
|||||||
public class GetErrors
|
public class GetErrors
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
|
public void null_is_invalid() => Tests(null, PlatformID.Win32NT | PlatformID.Unix, new[] { NamingTemplate.ERROR_NULL_IS_INVALID });
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void empty_is_valid() => valid_tests("");
|
public void empty_is_valid() => valid_tests("");
|
||||||
@ -296,15 +407,19 @@ namespace Templates_Folder_Tests
|
|||||||
[DataRow(@"foo\bar")]
|
[DataRow(@"foo\bar")]
|
||||||
[DataRow(@"<id>")]
|
[DataRow(@"<id>")]
|
||||||
[DataRow(@"<id>\<title>")]
|
[DataRow(@"<id>\<title>")]
|
||||||
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
|
public void valid_tests(string template) => Tests(template, PlatformID.Win32NT | PlatformID.Unix, Array.Empty<string>());
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(@"C:\", Templates.ERROR_FULL_PATH_IS_INVALID)]
|
[DataRow(@"C:\", PlatformID.Win32NT, Templates.ERROR_FULL_PATH_IS_INVALID)]
|
||||||
public void Tests(string template, params string[] expected)
|
public void Tests(string template, PlatformID platformID, params string[] expected)
|
||||||
{
|
{
|
||||||
var result = Templates.Folder.GetErrors(template);
|
if ((platformID & Environment.OSVersion.Platform) == Environment.OSVersion.Platform)
|
||||||
result.Count().Should().Be(expected.Length);
|
{
|
||||||
result.Should().BeEquivalentTo(expected);
|
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var folderTemplate);
|
||||||
|
var result = folderTemplate.Errors;
|
||||||
|
result.Should().HaveCount(expected.Length);
|
||||||
|
result.Should().BeEquivalentTo(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,50 +427,57 @@ namespace Templates_Folder_Tests
|
|||||||
public class IsValid
|
public class IsValid
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void null_is_invalid() => Tests(null, false);
|
public void null_is_invalid() => Templates.TryGetTemplate<Templates.FolderTemplate>(null, out _).Should().BeFalse();
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void empty_is_valid() => Tests("", true);
|
public void empty_is_valid() => Tests("", true, PlatformID.Win32NT | PlatformID.Unix);
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void whitespace_is_valid() => Tests(" ", true);
|
public void whitespace_is_valid() => Tests(" ", true, PlatformID.Win32NT | PlatformID.Unix);
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(@"C:\", false)]
|
[DataRow(@"C:\", false, PlatformID.Win32NT)]
|
||||||
[DataRow(@"foo", true)]
|
[DataRow(@"foo", true, PlatformID.Win32NT | PlatformID.Unix)]
|
||||||
[DataRow(@"\foo", true)]
|
[DataRow(@"\foo", true, PlatformID.Win32NT | PlatformID.Unix)]
|
||||||
[DataRow(@"foo\", true)]
|
[DataRow(@"foo\", true, PlatformID.Win32NT | PlatformID.Unix)]
|
||||||
[DataRow(@"\foo\", true)]
|
[DataRow(@"\foo\", true, PlatformID.Win32NT | PlatformID.Unix)]
|
||||||
[DataRow(@"foo\bar", true)]
|
[DataRow(@"foo\bar", true, PlatformID.Win32NT | PlatformID.Unix)]
|
||||||
[DataRow(@"<id>", true)]
|
[DataRow(@"<id>", true, PlatformID.Win32NT | PlatformID.Unix)]
|
||||||
[DataRow(@"<id>\<title>", true)]
|
[DataRow(@"<id>\<title>", true, PlatformID.Win32NT | PlatformID.Unix)]
|
||||||
public void Tests(string template, bool expected) => Templates.Folder.IsValid(template).Should().Be(expected);
|
public void Tests(string template, bool expected, PlatformID platformID)
|
||||||
|
{
|
||||||
|
if ((platformID & Environment.OSVersion.Platform) == Environment.OSVersion.Platform)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var folderTemplate).Should().BeTrue();
|
||||||
|
folderTemplate.IsValid.Should().Be(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class GetWarnings
|
public class GetWarnings
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
|
public void null_is_invalid() => Tests(null, new[] { NamingTemplate.ERROR_NULL_IS_INVALID });
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void empty_has_warnings() => Tests("", Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS);
|
public void empty_has_warnings() => Tests("", NamingTemplate.WARNING_EMPTY, NamingTemplate.WARNING_NO_TAGS);
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void whitespace_has_warnings() => Tests(" ", Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS);
|
public void whitespace_has_warnings() => Tests(" ", NamingTemplate.WARNING_WHITE_SPACE, NamingTemplate.WARNING_NO_TAGS);
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(@"<id>\foo\bar")]
|
[DataRow(@"<id>\foo\bar")]
|
||||||
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
|
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(@"no tags", Templates.WARNING_NO_TAGS)]
|
[DataRow(@"no tags", NamingTemplate.WARNING_NO_TAGS)]
|
||||||
[DataRow("<ch#> <id>", Templates.WARNING_HAS_CHAPTER_TAGS)]
|
[DataRow("<ch#> chapter tag", NamingTemplate.WARNING_NO_TAGS)]
|
||||||
[DataRow("<ch#> chapter tag", Templates.WARNING_NO_TAGS, Templates.WARNING_HAS_CHAPTER_TAGS)]
|
|
||||||
public void Tests(string template, params string[] expected)
|
public void Tests(string template, params string[] expected)
|
||||||
{
|
{
|
||||||
var result = Templates.Folder.GetWarnings(template);
|
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var folderTemplate);
|
||||||
result.Count().Should().Be(expected.Length);
|
var result = folderTemplate.Warnings;
|
||||||
|
result.Should().HaveCount(expected.Length);
|
||||||
result.Should().BeEquivalentTo(expected);
|
result.Should().BeEquivalentTo(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -375,16 +497,23 @@ namespace Templates_Folder_Tests
|
|||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(@"no tags", true)]
|
[DataRow(@"no tags", true)]
|
||||||
[DataRow(@"<id>\foo\bar", false)]
|
[DataRow(@"<id>\foo\bar", false)]
|
||||||
[DataRow("<ch#> <id>", true)]
|
|
||||||
[DataRow("<ch#> chapter tag", true)]
|
[DataRow("<ch#> chapter tag", true)]
|
||||||
public void Tests(string template, bool expected) => Templates.Folder.HasWarnings(template).Should().Be(expected);
|
public void Tests(string template, bool expected)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var folderTemplate);
|
||||||
|
folderTemplate.HasWarnings.Should().Be(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class TagCount
|
public class TagCount
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void null_throws() => Assert.ThrowsException<NullReferenceException>(() => Templates.Folder.TagCount(null));
|
public void null_invalid()
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.FolderTemplate>(null, out var template).Should().BeFalse();
|
||||||
|
template.IsValid.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void empty() => Tests("", 0);
|
public void empty() => Tests("", 0);
|
||||||
@ -402,7 +531,11 @@ namespace Templates_Folder_Tests
|
|||||||
[DataRow("<not a real tag>", 0)]
|
[DataRow("<not a real tag>", 0)]
|
||||||
[DataRow("<ch#> non-folder tag", 0)]
|
[DataRow("<ch#> non-folder tag", 0)]
|
||||||
[DataRow("<ID> case specific", 0)]
|
[DataRow("<ID> case specific", 0)]
|
||||||
public void Tests(string template, int expected) => Templates.Folder.TagCount(template).Should().Be(expected);
|
public void Tests(string template, int expected)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var folderTemplate).Should().BeTrue();
|
||||||
|
folderTemplate.TagsInUse.Count().Should().Be(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -412,7 +545,7 @@ namespace Templates_File_Tests
|
|||||||
public class GetErrors
|
public class GetErrors
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void null_is_invalid() => Tests(null, Environment.OSVersion.Platform, new[] { Templates.ERROR_NULL_IS_INVALID });
|
public void null_is_invalid() => Tests(null, Environment.OSVersion.Platform, new[] { NamingTemplate.ERROR_NULL_IS_INVALID });
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void empty_is_valid() => valid_tests("");
|
public void empty_is_valid() => valid_tests("");
|
||||||
@ -425,19 +558,13 @@ namespace Templates_File_Tests
|
|||||||
[DataRow(@"<id>")]
|
[DataRow(@"<id>")]
|
||||||
public void valid_tests(string template) => Tests(template, Environment.OSVersion.Platform, Array.Empty<string>());
|
public void valid_tests(string template) => Tests(template, Environment.OSVersion.Platform, Array.Empty<string>());
|
||||||
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[DataRow(@"C:\", PlatformID.Win32NT, Templates.ERROR_INVALID_FILE_NAME_CHAR)]
|
|
||||||
[DataRow(@"/", PlatformID.Unix, Templates.ERROR_INVALID_FILE_NAME_CHAR)]
|
|
||||||
[DataRow(@"\foo", PlatformID.Win32NT, Templates.ERROR_INVALID_FILE_NAME_CHAR)]
|
|
||||||
[DataRow(@"/foo", PlatformID.Win32NT, Templates.ERROR_INVALID_FILE_NAME_CHAR)]
|
|
||||||
[DataRow(@"/foo", PlatformID.Unix, Templates.ERROR_INVALID_FILE_NAME_CHAR)]
|
|
||||||
public void Tests(string template, PlatformID platformID, params string[] expected)
|
public void Tests(string template, PlatformID platformID, params string[] expected)
|
||||||
{
|
{
|
||||||
if (Environment.OSVersion.Platform == platformID)
|
if (Environment.OSVersion.Platform == platformID)
|
||||||
{
|
{
|
||||||
var result = Templates.File.GetErrors(template);
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate);
|
||||||
result.Count().Should().Be(expected.Length);
|
var result = fileTemplate.Errors;
|
||||||
|
result.Should().HaveCount(expected.Length);
|
||||||
result.Should().BeEquivalentTo(expected);
|
result.Should().BeEquivalentTo(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -447,28 +574,26 @@ namespace Templates_File_Tests
|
|||||||
public class IsValid
|
public class IsValid
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void null_is_invalid() => Tests(null, false, Environment.OSVersion.Platform);
|
public void null_is_invalid() => Templates.TryGetTemplate<Templates.FileTemplate>(null, out _).Should().BeFalse();
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void empty_is_valid() => Tests("", true, Environment.OSVersion.Platform);
|
public void empty_is_valid() => Tests("", true);
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void whitespace_is_valid() => Tests(" ", true, Environment.OSVersion.Platform);
|
public void whitespace_is_valid() => Tests(" ", true);
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(@"C:\", false, PlatformID.Win32NT)]
|
[DataRow(@"foo", true)]
|
||||||
[DataRow(@"/", false, PlatformID.Unix)]
|
[DataRow(@"\foo", true)]
|
||||||
[DataRow(@"foo", true, PlatformID.Win32NT)]
|
[DataRow(@"foo\", true)]
|
||||||
[DataRow(@"foo", true, PlatformID.Unix)]
|
[DataRow(@"\foo\", true)]
|
||||||
[DataRow(@"\foo", false, PlatformID.Win32NT)]
|
[DataRow(@"foo\bar", true)]
|
||||||
[DataRow(@"\foo", true, PlatformID.Unix)]
|
[DataRow(@"<id>", true)]
|
||||||
[DataRow(@"/foo", false, PlatformID.Win32NT)]
|
[DataRow(@"<id>\<title>", true)]
|
||||||
[DataRow(@"<id>", true, PlatformID.Win32NT)]
|
public void Tests(string template, bool expected)
|
||||||
[DataRow(@"<id>", true, PlatformID.Unix)]
|
|
||||||
public void Tests(string template, bool expected, PlatformID platformID)
|
|
||||||
{
|
{
|
||||||
if (Environment.OSVersion.Platform == platformID)
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var folderTemplate).Should().BeTrue();
|
||||||
Templates.File.IsValid(template).Should().Be(expected);
|
folderTemplate.IsValid.Should().Be(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -499,13 +624,13 @@ namespace Templates_ChapterFile_Tests
|
|||||||
public class GetWarnings
|
public class GetWarnings
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void null_is_invalid() => Tests(null, null, new[] { Templates.ERROR_NULL_IS_INVALID });
|
public void null_is_invalid() => Tests(null, null, new[] { NamingTemplate.ERROR_NULL_IS_INVALID, Templates.WARNING_NO_CHAPTER_NUMBER_TAG });
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void empty_has_warnings() => Tests("", null, Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
|
public void empty_has_warnings() => Tests("", null, NamingTemplate.WARNING_EMPTY, NamingTemplate.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void whitespace_has_warnings() => Tests(" ", null, Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
|
public void whitespace_has_warnings() => Tests(" ", null, NamingTemplate.WARNING_WHITE_SPACE, NamingTemplate.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow("<ch#>")]
|
[DataRow("<ch#>")]
|
||||||
@ -513,18 +638,20 @@ namespace Templates_ChapterFile_Tests
|
|||||||
public void valid_tests(string template) => Tests(template, null, Array.Empty<string>());
|
public void valid_tests(string template) => Tests(template, null, Array.Empty<string>());
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(@"no tags", null, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
[DataRow(@"no tags", null, NamingTemplate.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
||||||
[DataRow(@"<id>\foo\bar", true, Templates.ERROR_INVALID_FILE_NAME_CHAR, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
[DataRow(@"<id>\foo\bar", true, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
||||||
[DataRow(@"<id>/foo/bar", false, Templates.ERROR_INVALID_FILE_NAME_CHAR, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
[DataRow(@"<id>/foo/bar", false, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
||||||
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", null, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", null, NamingTemplate.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
|
||||||
public void Tests(string template, bool? windows, params string[] expected)
|
public void Tests(string template, bool? windows, params string[] expected)
|
||||||
{
|
{
|
||||||
if(windows is null
|
if (windows is null
|
||||||
|| (windows is true && Environment.OSVersion.Platform is PlatformID.Win32NT)
|
|| (windows is true && Environment.OSVersion.Platform is PlatformID.Win32NT)
|
||||||
|| (windows is false && Environment.OSVersion.Platform is PlatformID.Unix))
|
|| (windows is false && Environment.OSVersion.Platform is PlatformID.Unix))
|
||||||
{
|
{
|
||||||
var result = Templates.ChapterFile.GetWarnings(template);
|
|
||||||
result.Count().Should().Be(expected.Length);
|
Templates.TryGetTemplate<Templates.ChapterFileTemplate>(template, out var chapterFileTemplate);
|
||||||
|
var result = chapterFileTemplate.Warnings;
|
||||||
|
result.Should().HaveCount(expected.Length);
|
||||||
result.Should().BeEquivalentTo(expected);
|
result.Should().BeEquivalentTo(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -548,14 +675,18 @@ namespace Templates_ChapterFile_Tests
|
|||||||
[DataRow("<ch#> <id>", false)]
|
[DataRow("<ch#> <id>", false)]
|
||||||
[DataRow("<ch#> -- chapter tag", false)]
|
[DataRow("<ch#> -- chapter tag", false)]
|
||||||
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", true)]
|
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", true)]
|
||||||
public void Tests(string template, bool expected) => Templates.ChapterFile.HasWarnings(template).Should().Be(expected);
|
public void Tests(string template, bool expected)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.ChapterFileTemplate>(template, out var chapterFileTemplate);
|
||||||
|
chapterFileTemplate.HasWarnings.Should().Be(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class TagCount
|
public class TagCount
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void null_is_not_recommended() => Assert.ThrowsException<NullReferenceException>(() => Tests(null, -1));
|
public void null_is_not_recommended() => Templates.TryGetTemplate<Templates.ChapterFileTemplate>(null, out _).Should().BeFalse();
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void empty_is_not_recommended() => Tests("", 0);
|
public void empty_is_not_recommended() => Tests("", 0);
|
||||||
@ -573,11 +704,15 @@ namespace Templates_ChapterFile_Tests
|
|||||||
[DataRow("<not a real tag>", 0)]
|
[DataRow("<not a real tag>", 0)]
|
||||||
[DataRow("<ch#> non-folder tag", 1)]
|
[DataRow("<ch#> non-folder tag", 1)]
|
||||||
[DataRow("<ID> case specific", 0)]
|
[DataRow("<ID> case specific", 0)]
|
||||||
public void Tests(string template, int expected) => Templates.ChapterFile.TagCount(template).Should().Be(expected);
|
public void Tests(string template, int expected)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.ChapterFileTemplate>(template, out var chapterFileTemplate).Should().BeTrue();
|
||||||
|
chapterFileTemplate.TagsInUse.Count().Should().Be(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class GetPortionFilename
|
public class GetFilename
|
||||||
{
|
{
|
||||||
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
|
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
|
||||||
|
|
||||||
@ -589,8 +724,13 @@ namespace Templates_ChapterFile_Tests
|
|||||||
public void Tests(string template, string dir, string ext, int pos, int total, string chapter, string expected, PlatformID platformID)
|
public void Tests(string template, string dir, string ext, int pos, int total, string chapter, string expected, PlatformID platformID)
|
||||||
{
|
{
|
||||||
if (Environment.OSVersion.Platform == platformID)
|
if (Environment.OSVersion.Platform == platformID)
|
||||||
Templates.ChapterFile.GetPortionFilename(GetLibraryBook(), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, Default)
|
{
|
||||||
.Should().Be(expected);
|
Templates.TryGetTemplate<Templates.ChapterFileTemplate>(template, out var chapterTemplate).Should().BeTrue();
|
||||||
|
chapterTemplate
|
||||||
|
.GetFilename(GetLibraryBook(), new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, ext, Default)
|
||||||
|
.PathWithoutPrefix
|
||||||
|
.Should().Be(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user