mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-03-30 12:53:45 -04:00
Introduce culture parameter for formatting.
This commit is contained in:
@@ -9,12 +9,12 @@ public static partial class CommonFormatters
|
||||
{
|
||||
public const string DefaultDateFormat = "yyyy-MM-dd";
|
||||
|
||||
public delegate string PropertyFormatter<in T>(ITemplateTag templateTag, T value, string formatString);
|
||||
public delegate string PropertyFormatter<in T>(ITemplateTag templateTag, T? value, string formatString, CultureInfo? culture);
|
||||
|
||||
public static string StringFormatter(ITemplateTag _, string? value, string formatString)
|
||||
public static string StringFormatter(ITemplateTag _, string? value, string formatString, CultureInfo? culture)
|
||||
{
|
||||
if (value is null) return "";
|
||||
var culture = CultureInfo.CurrentCulture;
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
|
||||
return formatString switch
|
||||
{
|
||||
@@ -24,15 +24,15 @@ public static partial class CommonFormatters
|
||||
};
|
||||
}
|
||||
|
||||
public static string FormattableFormatter(ITemplateTag _, IFormattable? value, string formatString)
|
||||
=> value?.ToString(formatString, null) ?? "";
|
||||
public static string FormattableFormatter(ITemplateTag _, IFormattable? value, string formatString, CultureInfo? culture)
|
||||
=> value?.ToString(formatString, culture) ?? "";
|
||||
|
||||
public static string IntegerFormatter(ITemplateTag templateTag, int value, string formatString)
|
||||
=> FloatFormatter(templateTag, value, formatString);
|
||||
public static string IntegerFormatter(ITemplateTag templateTag, int value, string formatString, CultureInfo? culture)
|
||||
=> FloatFormatter(templateTag, value, formatString, culture);
|
||||
|
||||
public static string FloatFormatter(ITemplateTag _, float value, string formatString)
|
||||
public static string FloatFormatter(ITemplateTag _, float value, string formatString, CultureInfo? culture)
|
||||
{
|
||||
var culture = CultureInfo.CurrentCulture;
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
if (!int.TryParse(formatString, out var numDigits) || numDigits <= 0) return value.ToString(formatString, culture);
|
||||
//Zero-pad the integer part
|
||||
var strValue = value.ToString(culture);
|
||||
@@ -42,22 +42,22 @@ public static partial class CommonFormatters
|
||||
return new string('0', zeroPad) + strValue;
|
||||
}
|
||||
|
||||
public static string DateTimeFormatter(ITemplateTag _, DateTime value, string formatString)
|
||||
public static string DateTimeFormatter(ITemplateTag _, DateTime value, string formatString, CultureInfo? culture)
|
||||
{
|
||||
var culture = CultureInfo.CurrentCulture;
|
||||
culture ??= CultureInfo.InvariantCulture;
|
||||
if (string.IsNullOrEmpty(formatString))
|
||||
formatString = CommonFormatters.DefaultDateFormat;
|
||||
formatString = DefaultDateFormat;
|
||||
return value.ToString(formatString, culture);
|
||||
}
|
||||
|
||||
public static string LanguageShortFormatter(string? language)
|
||||
public static string LanguageShortFormatter(ITemplateTag templateTag, string? language, string formatString, CultureInfo? culture)
|
||||
{
|
||||
if (language is null)
|
||||
return "";
|
||||
|
||||
language = language.Trim();
|
||||
if (language.Length <= 3)
|
||||
return language.ToUpper();
|
||||
return language[..3].ToUpper();
|
||||
if (language.Length > 3) language = language[..3];
|
||||
|
||||
return StringFormatter(templateTag, language, formatString, culture);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -21,7 +22,7 @@ internal interface IClosingPropertyTag : IPropertyTag
|
||||
bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag);
|
||||
}
|
||||
|
||||
public delegate bool Conditional<T>(ITemplateTag templateTag, T value, string condition);
|
||||
public delegate bool Conditional<T>(ITemplateTag templateTag, T value, string condition, CultureInfo? culture);
|
||||
|
||||
public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive)
|
||||
{
|
||||
@@ -74,7 +75,8 @@ public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCo
|
||||
conditional.Method,
|
||||
Expression.Constant(templateTag),
|
||||
parameter,
|
||||
Expression.Constant(condition));
|
||||
Expression.Constant(condition),
|
||||
CultureParameter);
|
||||
}
|
||||
|
||||
public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
@@ -28,19 +29,36 @@ public class NamingTemplate
|
||||
/// <summary>
|
||||
/// Invoke the <see cref="NamingTemplate"/>
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
|
||||
public TemplatePart Evaluate(params object?[] propertyClasses)
|
||||
public TemplatePart Evaluate(params object?[] propertyClasses) //CultureInfo? culture,
|
||||
{
|
||||
if (_templateToString is null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
|
||||
// First parameter is "this", so ignore it.
|
||||
var delegateArgTypes = _templateToString.Method.GetParameters().Skip(1);
|
||||
var parameters = _templateToString.Method.GetParameters();
|
||||
int skip = _templateToString.Target == null ? 0 : 1;
|
||||
var delegateArgTypes = parameters.Skip(skip).ToList();
|
||||
|
||||
object?[] args = delegateArgTypes.Join(propertyClasses, o => o.ParameterType, i => i?.GetType(), (_, i) => i).ToArray();
|
||||
object?[] args = new object?[delegateArgTypes.Count];
|
||||
// args = delegateArgTypes.Join(propertyClasses, dat => dat.ParameterType, pc => pc?.GetType(), (_, i) => i,
|
||||
// EqualityComparer<Type?>.Create((datType, pcType) => datType!.IsAssignableFrom(pcType))).ToArray();
|
||||
for (int i = 0; i < delegateArgTypes.Count; i++)
|
||||
{
|
||||
var p = delegateArgTypes[i];
|
||||
if (typeof(CultureInfo).IsAssignableFrom(p.ParameterType) && false)
|
||||
{
|
||||
args[i] = null;//culture;
|
||||
}
|
||||
else
|
||||
{
|
||||
args[i] = propertyClasses.FirstOrDefault(pc => pc != null && p.ParameterType.IsInstanceOfType(pc));
|
||||
}
|
||||
}
|
||||
|
||||
if (args.Length != delegateArgTypes.Count())
|
||||
if (args.Length != delegateArgTypes.Count)
|
||||
throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}");
|
||||
|
||||
return (_templateToString.DynamicInvoke(args) as TemplatePart)!.FirstPart;
|
||||
@@ -58,7 +76,7 @@ public class NamingTemplate
|
||||
BinaryNode intermediate = namingTemplate.IntermediateParse(template);
|
||||
Expression evalTree = GetExpressionTree(intermediate);
|
||||
|
||||
namingTemplate._templateToString = Expression.Lambda(evalTree, tagCollections.Select(tc => tc.Parameter)).Compile();
|
||||
namingTemplate._templateToString = Expression.Lambda(evalTree, tagCollections.Select(tc => tc.Parameter).Append(TagCollection.CultureParameter)).Compile();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -20,10 +21,12 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
var parameters = formatter.Method.GetParameters();
|
||||
|
||||
if (formatter.Method.ReturnType != typeof(string)
|
||||
|| parameters.Length != 3
|
||||
|| parameters.Length != 4
|
||||
|| parameters[0].ParameterType != typeof(ITemplateTag)
|
||||
|| parameters[2].ParameterType != typeof(string))
|
||||
throw new ArgumentException($"{nameof(defaultFormatters)} must have a signature of [{nameof(String)} PropertyFormatter<T>({nameof(ITemplateTag)}, T, {nameof(String)})]");
|
||||
|| parameters[2].ParameterType != typeof(string)
|
||||
|| !typeof(CultureInfo).IsAssignableFrom(parameters[3].ParameterType))
|
||||
throw new ArgumentException(
|
||||
$"{nameof(defaultFormatters)} must have a signature of [{nameof(String)} PropertyFormatter<T>({nameof(ITemplateTag)}, T, {nameof(String)}, {nameof(CultureInfo)})]");
|
||||
|
||||
this._defaultFormatters[parameters[1].ParameterType] = formatter;
|
||||
}
|
||||
@@ -118,15 +121,15 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
/// <param name="object">The property class from which the tag's value is read</param>
|
||||
/// <param name="value"><paramref name="tagName"/>'s string value if it is in this collection, otherwise null</param>
|
||||
/// <returns>True if the <paramref name="tagName"/> is in this collection, otherwise false</returns>
|
||||
public bool TryGetValue(string tagName, TClass @object, [NotNullWhen(true)] out string? value)
|
||||
public bool TryGetValue(string tagName, TClass @object, CultureInfo? culture, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
if (!StartsWith($"<{tagName}>", out _, out _, out var valueExpression))
|
||||
return false;
|
||||
|
||||
var func = Expression.Lambda<Func<TClass, string>>(valueExpression, Parameter).Compile();
|
||||
value = func(@object);
|
||||
var func = Expression.Lambda<Func<TClass, CultureInfo?, string>>(valueExpression, Parameter, CultureParameter).Compile();
|
||||
value = func(@object, culture);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -145,7 +148,8 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
formatter.Method,
|
||||
Expression.Constant(templateTag),
|
||||
expVal,
|
||||
Expression.Constant(format));
|
||||
Expression.Constant(format),
|
||||
CultureParameter);
|
||||
}
|
||||
|
||||
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func<TPropertyValue, string> toString)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -16,6 +17,8 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
|
||||
|
||||
/// <summary>The <see cref="ParameterExpression"/> of the <see cref="TagCollection"/>'s TClass type.</summary>
|
||||
internal ParameterExpression Parameter { get; }
|
||||
|
||||
internal static readonly ParameterExpression CultureParameter = Expression.Parameter(typeof(CultureInfo), "culture");
|
||||
protected RegexOptions Options { get; } = RegexOptions.Compiled;
|
||||
private List<IPropertyTag> PropertyTags { get; } = [];
|
||||
|
||||
@@ -34,7 +37,8 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
|
||||
/// <param name="propertyTag"></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, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? propertyValue)
|
||||
internal bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag,
|
||||
[NotNullWhen(true)] out Expression? propertyValue)
|
||||
{
|
||||
foreach (var p in PropertyTags)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ public class ContributorDto(string name, string? audibleContributorId) : IFormat
|
||||
public override string ToString()
|
||||
=> ToString("{T} {F} {M} {L} {S}", null);
|
||||
|
||||
public string ToString(string? format, IFormatProvider? _)
|
||||
public string ToString(string? format, IFormatProvider? provider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
return ToString();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -7,46 +8,63 @@ namespace LibationFileManager.Templates;
|
||||
|
||||
internal partial interface IListFormat<TList> where TList : IListFormat<TList>
|
||||
{
|
||||
static string Join<T>(string formatString, IEnumerable<T> items)
|
||||
where T : IFormattable
|
||||
static IEnumerable<T> FilteredList<T>(string formatString, IEnumerable<T> items)
|
||||
{
|
||||
var itemFormatter = Formatter(formatString);
|
||||
var separatorString = Separator(formatString) ?? ", ";
|
||||
var maxValues = Max(formatString) ?? items.Count();
|
||||
return Max(formatString, items);
|
||||
|
||||
var formattedValues = string.Join(separatorString, items.Take(maxValues).Select(n => n.ToString(itemFormatter, null)));
|
||||
|
||||
while (formattedValues.Contains(" "))
|
||||
formattedValues = formattedValues.Replace(" ", " ");
|
||||
|
||||
return formattedValues;
|
||||
|
||||
static string? Formatter(string formatString)
|
||||
{
|
||||
var formatMatch = TList.FormatRegex().Match(formatString);
|
||||
return formatMatch.Success ? formatMatch.Groups[1].Value : null;
|
||||
}
|
||||
|
||||
static int? Max(string formatString)
|
||||
static IEnumerable<T> Max(string formatString, IEnumerable<T> items)
|
||||
{
|
||||
var maxMatch = MaxRegex().Match(formatString);
|
||||
return maxMatch.Success && int.TryParse(maxMatch.Groups[1].Value, out var max) ? int.Max(1, max) : null;
|
||||
}
|
||||
|
||||
static string? Separator(string formatString)
|
||||
{
|
||||
var separatorMatch = SeparatorRegex().Match(formatString);
|
||||
return separatorMatch.Success ? separatorMatch.Groups[1].Value : ", ";
|
||||
return maxMatch.Success && int.TryParse(maxMatch.Groups[1].ValueSpan, out var max)
|
||||
? items.Take(max)
|
||||
: items;
|
||||
}
|
||||
}
|
||||
|
||||
static IEnumerable<string> FormattedList<T>(string formatString, IEnumerable<T> items, CultureInfo? culture) where T : IFormattable
|
||||
{
|
||||
var format = FormatElement(formatString, TList.FormatRegex);
|
||||
var separator = FormatElement(formatString, SeparatorRegex);
|
||||
var formattedItems = FilteredList(formatString, items).Select(ItemFormatter);
|
||||
|
||||
// ReSharper disable PossibleMultipleEnumeration
|
||||
return separator is null
|
||||
? formattedItems
|
||||
: formattedItems.Any()
|
||||
? [Join(separator, formattedItems)]
|
||||
: [];
|
||||
// ReSharper restore PossibleMultipleEnumeration
|
||||
|
||||
string ItemFormatter(T n) => n.ToString(format, culture);
|
||||
|
||||
static string? FormatElement(string formatString, Func<Regex> regex)
|
||||
{
|
||||
var match = regex().Match(formatString);
|
||||
return match.Success ? match.Groups[1].Value : null;
|
||||
}
|
||||
}
|
||||
|
||||
static string Join<T>(string formatString, IEnumerable<T> items, CultureInfo? culture) where T : IFormattable
|
||||
{
|
||||
return Join(", ", FormattedList(formatString, items, culture));
|
||||
}
|
||||
|
||||
private static string Join(string separator, IEnumerable<string> strings)
|
||||
{
|
||||
return CollapseSpacesRegex().Replace(string.Join(separator, strings), " ");
|
||||
}
|
||||
|
||||
// Collapses runs of 2+ spaces into a single space (does NOT touch tabs/newlines).
|
||||
[GeneratedRegex(@" {2,}")]
|
||||
private static partial Regex CollapseSpacesRegex();
|
||||
|
||||
static abstract Regex FormatRegex();
|
||||
|
||||
/// <summary> Max must have a 1 or 2-digit number </summary>
|
||||
[GeneratedRegex(@"[Mm]ax\(\s*([1-9]\d?)\s*\)")]
|
||||
private static partial Regex MaxRegex();
|
||||
|
||||
/// <summary> Separator can be anything </summary>
|
||||
[GeneratedRegex(@"[Ss]eparator\((.*?)\)")]
|
||||
private static partial Regex SeparatorRegex();
|
||||
|
||||
/// <summary> Max must have a 1 or 2-digit number </summary>
|
||||
[GeneratedRegex(@"[Mm]ax\(\s*(\d{1,2})\s*\)")]
|
||||
private static partial Regex MaxRegex();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using FileManager.NamingTemplate;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -7,10 +8,10 @@ namespace LibationFileManager.Templates;
|
||||
|
||||
internal partial class NameListFormat : IListFormat<NameListFormat>
|
||||
{
|
||||
public static string Formatter(ITemplateTag _, IEnumerable<ContributorDto>? names, string formatString)
|
||||
public static string Formatter(ITemplateTag _, IEnumerable<ContributorDto>? names, string formatString, CultureInfo? culture)
|
||||
=> names is null
|
||||
? string.Empty
|
||||
: IListFormat<NameListFormat>.Join(formatString, Sort(names, formatString));
|
||||
: IListFormat<NameListFormat>.Join(formatString, Sort(names, formatString), culture);
|
||||
|
||||
private static IEnumerable<ContributorDto> Sort(IEnumerable<ContributorDto> names, string formatString)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ public partial record SeriesDto(string? Name, string? Number, string AudibleSeri
|
||||
public SeriesOrder Order { get; } = SeriesOrder.Parse(Number);
|
||||
|
||||
public override string? ToString() => Name?.Trim();
|
||||
public string ToString(string? format, IFormatProvider? _)
|
||||
public string ToString(string? format, IFormatProvider? provider)
|
||||
=> string.IsNullOrWhiteSpace(format) ? ToString() ?? string.Empty
|
||||
: FormatRegex().Replace(format, MatchEvaluator)
|
||||
.Replace("{N}", Name)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
using FileManager.NamingTemplate;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LibationFileManager.Templates;
|
||||
|
||||
internal partial class SeriesListFormat : IListFormat<SeriesListFormat>
|
||||
{
|
||||
public static string Formatter(ITemplateTag _, IEnumerable<SeriesDto>? series, string formatString)
|
||||
public static string Formatter(ITemplateTag _, IEnumerable<SeriesDto>? series, string formatString, CultureInfo? culture)
|
||||
=> series is null
|
||||
? string.Empty
|
||||
: IListFormat<SeriesListFormat>.Join(formatString, series);
|
||||
: IListFormat<SeriesListFormat>.Join(formatString, series, culture);
|
||||
|
||||
/// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary>
|
||||
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{#(?:\:.*?)?}|{N}|{ID})+.*?)\)")]
|
||||
|
||||
@@ -103,23 +103,33 @@ public abstract class Templates
|
||||
#region to file name
|
||||
|
||||
public string GetName(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps)
|
||||
=> GetName(libraryBookDto, multiChapProps, null);
|
||||
|
||||
public string GetName(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, CultureInfo? culture)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps));
|
||||
return string.Concat(NamingTemplate.Evaluate(libraryBookDto, multiChapProps, new CombinedDto(libraryBookDto, multiChapProps)).Select(p => p.Value));
|
||||
return string.Concat(NamingTemplate.Evaluate(culture, libraryBookDto, multiChapProps, new CombinedDto(libraryBookDto, multiChapProps)).Select(p => p.Value));
|
||||
}
|
||||
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false)
|
||||
=> GetFilename(libraryBookDto, baseDir, fileExtension, culture: null, replacements: replacements, returnFirstExisting: returnFirstExisting);
|
||||
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, CultureInfo? culture, 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, replacements, returnFirstExisting, libraryBookDto);
|
||||
return GetFilename(baseDir, fileExtension, replacements, returnFirstExisting, libraryBookDto, culture);
|
||||
}
|
||||
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false)
|
||||
=> GetFilename(libraryBookDto, multiChapProps, baseDir, fileExtension, culture: null, replacements: replacements, returnFirstExisting: returnFirstExisting);
|
||||
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, CultureInfo? culture,
|
||||
ReplacementCharacters? replacements = null, bool returnFirstExisting = false)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps));
|
||||
@@ -127,17 +137,18 @@ public abstract class Templates
|
||||
ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension));
|
||||
|
||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||
return GetFilename(baseDir, fileExtension, replacements, returnFirstExisting, libraryBookDto, multiChapProps);
|
||||
return GetFilename(baseDir, fileExtension, replacements, returnFirstExisting, libraryBookDto, culture, 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, ReplacementCharacters replacements, bool returnFirstExisting, LibraryBookDto lbDto, MultiConvertFileProperties? multiDto = null)
|
||||
private LongPath GetFilename(string baseDir, string fileExtension, ReplacementCharacters replacements, bool returnFirstExisting, LibraryBookDto lbDto, CultureInfo? culture,
|
||||
MultiConvertFileProperties? multiDto = null)
|
||||
{
|
||||
fileExtension = FileUtility.GetStandardizedExtension(fileExtension);
|
||||
|
||||
var parts = NamingTemplate.Evaluate(lbDto, multiDto, new CombinedDto(lbDto, multiDto)).ToList();
|
||||
var parts = NamingTemplate.Evaluate(culture, lbDto, multiDto, new CombinedDto(lbDto, multiDto)).ToList();
|
||||
var pathParts = GetPathParts(GetTemplatePartsStrings(parts, replacements));
|
||||
|
||||
//Remove 1 character from the end of the longest filename part until
|
||||
@@ -331,14 +342,14 @@ public abstract class Templates
|
||||
private static readonly List<TagCollection> allPropertyTags =
|
||||
chapterPropertyTags.Append(filePropertyTags).Append(audioFilePropertyTags).ToList();
|
||||
|
||||
private static bool HasValue(ITemplateTag _, CombinedDto dtos, string property)
|
||||
private static bool HasValue(ITemplateTag _, CombinedDto dtos, string property, CultureInfo? culture)
|
||||
{
|
||||
Func<string?, bool> check = s => !string.IsNullOrWhiteSpace(s);
|
||||
Func<string?, CultureInfo?, bool> check = (s, _) => !string.IsNullOrWhiteSpace(s);
|
||||
|
||||
foreach (var c in allPropertyTags.OfType<PropertyTagCollection<LibraryBookDto>>())
|
||||
{
|
||||
if (c.TryGetValue(property, dtos.LibraryBook, out var value))
|
||||
return check(value);
|
||||
if (c.TryGetValue(property, dtos.LibraryBook, culture, out var value))
|
||||
return check(value, culture ?? CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
if (dtos.MultiConvert is null)
|
||||
@@ -346,8 +357,8 @@ public abstract class Templates
|
||||
|
||||
foreach (var c in allPropertyTags.OfType<PropertyTagCollection<MultiConvertFileProperties>>())
|
||||
{
|
||||
if (c.TryGetValue(property, dtos.MultiConvert, out var value))
|
||||
return check(value);
|
||||
if (c.TryGetValue(property, dtos.MultiConvert, culture, out var value))
|
||||
return check(value, culture ?? CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -98,14 +98,14 @@ public class GetPortionFilename
|
||||
{ new TemplateTag { TagName = "has3" }, HasValue }
|
||||
};
|
||||
|
||||
private static bool HasValue(ITemplateTag templateTag, PropertyClass1 referenceType, string condition)
|
||||
=> props1.TryGetValue(condition, referenceType, out var value) && !string.IsNullOrEmpty(value);
|
||||
private static bool HasValue(ITemplateTag templateTag, PropertyClass1 referenceType, string condition, CultureInfo? culture)
|
||||
=> props1.TryGetValue(condition, referenceType, culture, out var value) && !string.IsNullOrEmpty(value);
|
||||
|
||||
private static bool HasValue(ITemplateTag templateTag, PropertyClass2 referenceType, string condition)
|
||||
=> props2.TryGetValue(condition, referenceType, out var value) && !string.IsNullOrEmpty(value);
|
||||
private static bool HasValue(ITemplateTag templateTag, PropertyClass2 referenceType, string condition, CultureInfo? culture)
|
||||
=> props2.TryGetValue(condition, referenceType, culture, out var value) && !string.IsNullOrEmpty(value);
|
||||
|
||||
private static bool HasValue(ITemplateTag templateTag, PropertyClass3 referenceType, string condition)
|
||||
=> props3.TryGetValue(condition, referenceType, out var value) && !string.IsNullOrEmpty(value);
|
||||
private static bool HasValue(ITemplateTag templateTag, PropertyClass3 referenceType, string condition, CultureInfo? culture)
|
||||
=> props3.TryGetValue(condition, referenceType, culture, out var value) && !string.IsNullOrEmpty(value);
|
||||
|
||||
private readonly PropertyClass1 _propertyClass1 = new()
|
||||
{
|
||||
@@ -156,7 +156,7 @@ public class GetPortionFilename
|
||||
template.Warnings.Should().HaveCount(numTags > 0 ? 0 : 1);
|
||||
template.Errors.Should().HaveCount(0);
|
||||
|
||||
var templateText = string.Concat(template.Evaluate(_propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value));
|
||||
var templateText = string.Concat(template.Evaluate(null, _propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value));
|
||||
|
||||
templateText.Should().Be(outStr);
|
||||
}
|
||||
@@ -186,7 +186,7 @@ public class GetPortionFilename
|
||||
template.Warnings.Should().HaveCount(1);
|
||||
template.Errors.Should().HaveCount(0);
|
||||
|
||||
var templateText = string.Concat(template.Evaluate(_propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value));
|
||||
var templateText = string.Concat(template.Evaluate(null, _propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value));
|
||||
|
||||
templateText.Should().Be(outStr);
|
||||
}
|
||||
@@ -210,7 +210,7 @@ public class GetPortionFilename
|
||||
template.Warnings.Should().HaveCount(2);
|
||||
template.Errors.Should().HaveCount(0);
|
||||
|
||||
var templateText = string.Concat(template.Evaluate(_propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value));
|
||||
var templateText = string.Concat(template.Evaluate(null, _propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value));
|
||||
|
||||
templateText.Should().Be(outStr);
|
||||
}
|
||||
@@ -234,7 +234,7 @@ public class GetPortionFilename
|
||||
template.Warnings.Should().BeEquivalentTo(warnings);
|
||||
}
|
||||
|
||||
static string GetVal(ITemplateTag templateTag, ReferenceType referenceType, string format)
|
||||
static string GetVal(ITemplateTag templateTag, ReferenceType referenceType, string format, CultureInfo? culture)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
@@ -267,20 +267,20 @@ public class GetPortionFilename
|
||||
template.Warnings.Should().HaveCount(0);
|
||||
template.Errors.Should().HaveCount(0);
|
||||
|
||||
var templateText = string.Concat(template.Evaluate(_propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value));
|
||||
var templateText = string.Concat(template.Evaluate(null, _propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value));
|
||||
|
||||
templateText.Should().Be(outStr);
|
||||
|
||||
string FormatInt(ITemplateTag templateTag, int value, string format)
|
||||
string FormatInt(ITemplateTag templateTag, int value, string format, CultureInfo? culture)
|
||||
{
|
||||
if (int.TryParse(format, out var numDecs))
|
||||
return value.ToString($"D{numDecs}");
|
||||
return value.ToString();
|
||||
return value.ToString($"D{numDecs}", culture);
|
||||
return value.ToString(culture);
|
||||
}
|
||||
|
||||
string FormatString(ITemplateTag templateTag, string? value, string format)
|
||||
string FormatString(ITemplateTag templateTag, string? value, string format, CultureInfo? culture)
|
||||
{
|
||||
return CommonFormatters.StringFormatter(templateTag, value, format);
|
||||
return CommonFormatters.StringFormatter(templateTag, value, format, culture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace TemplatesTests
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements)
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -131,7 +131,7 @@ namespace TemplatesTests
|
||||
[DataRow("4<bitrate> - <bitrate> 4", "", "", "1 8 - 1 8")]
|
||||
[DataRow("<channels><channels><samplerate><channels><channels>", "", "", "100")]
|
||||
[DataRow(" <channels> <channels> <samplerate> <channels> <channels>", "", "", "100")]
|
||||
[DataRow(" <channels> - <channels> <samplerate> <channels> - <channels>", "", "", "- 100 -")]
|
||||
[DataRow(" <channels> - <channels> <samplerate> <channels> - <channels>", "", "", "- 100 -")]
|
||||
|
||||
public void Tests_removeSpaces(string template, string dirFullPath, string extension, string expected)
|
||||
{
|
||||
@@ -152,7 +152,7 @@ namespace TemplatesTests
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, replacements)
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, culture: null, replacements: replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -168,7 +168,7 @@ namespace TemplatesTests
|
||||
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);
|
||||
fileTemplate.GetFilename(GetLibraryBook(), "", "", culture: null, replacements: Replacements).PathWithoutPrefix.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -193,7 +193,7 @@ namespace TemplatesTests
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements)
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -222,7 +222,7 @@ namespace TemplatesTests
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements)
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -241,7 +241,7 @@ namespace TemplatesTests
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements)
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -255,15 +255,13 @@ namespace TemplatesTests
|
||||
[DataRow("<id> - <date added[MM/dd/yy HH:mm]>", @"/foo/bar", ".m4b", @"/foo/bar/asin - 06∕09∕22 00:00.m4b", PlatformID.Unix)]
|
||||
public void DateFormat_illegal(string template, string dirFullPath, string extension, string expected, PlatformID platformId)
|
||||
{
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
||||
|
||||
if (Environment.OSVersion.Platform == platformId)
|
||||
{
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate.HasWarnings.Should().BeFalse();
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements)
|
||||
.GetFilename(GetLibraryBook(), dirFullPath, extension, culture: CultureInfo.InvariantCulture, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -285,7 +283,7 @@ namespace TemplatesTests
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate
|
||||
.GetFilename(lbDto, dirFullPath, extension, Replacements)
|
||||
.GetFilename(lbDto, dirFullPath, extension, culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -326,7 +324,7 @@ namespace TemplatesTests
|
||||
bookDto.Authors = [new(author, null)];
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>("<author[format(Title={T}, First={F}, Middle={M} Last={L}, Suffix={S})]>", out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate
|
||||
.GetFilename(bookDto, "", "", Replacements)
|
||||
.GetFilename(bookDto, "", "", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -375,7 +373,7 @@ namespace TemplatesTests
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate
|
||||
.GetFilename(bookDto, "", "", Replacements)
|
||||
.GetFilename(bookDto, "", "", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -395,7 +393,7 @@ namespace TemplatesTests
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate
|
||||
.GetFilename(bookDto, multiDto, "", "", Replacements)
|
||||
.GetFilename(bookDto, multiDto, "", "", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -445,7 +443,7 @@ namespace TemplatesTests
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate
|
||||
.GetFilename(bookDto, multiDto, "", "", Replacements)
|
||||
.GetFilename(bookDto, multiDto, "", "", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -466,8 +464,6 @@ namespace TemplatesTests
|
||||
[DataRow("<first series[{N}, {#:00.0}]>", "Series A, 01.0")]
|
||||
public void SeriesFormat_formatters(string template, string expected)
|
||||
{
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
||||
|
||||
var bookDto = GetLibraryBook();
|
||||
bookDto.Series =
|
||||
[
|
||||
@@ -479,7 +475,7 @@ namespace TemplatesTests
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate
|
||||
.GetFilename(bookDto, "", "", Replacements)
|
||||
.GetFilename(bookDto, "", "", culture: CultureInfo.InvariantCulture, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -499,14 +495,12 @@ namespace TemplatesTests
|
||||
[DataRow("<series#>", " 1 6 ", "1 6")]
|
||||
public void SeriesOrder_formatters(string template, string seriesOrder, string expected)
|
||||
{
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
||||
|
||||
var bookDto = GetLibraryBook();
|
||||
bookDto.Series = [new("Series A", seriesOrder, "B1")];
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate
|
||||
.GetFilename(bookDto, "", "", Replacements)
|
||||
.GetFilename(bookDto, "", "", culture: CultureInfo.InvariantCulture, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -521,7 +515,7 @@ namespace TemplatesTests
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series-><-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), directory, "ext", Replacements)
|
||||
.GetFilename(GetLibraryBook(), directory, "ext", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -536,7 +530,7 @@ namespace TemplatesTests
|
||||
{
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series->-<series>-<id>-<-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate.GetFilename(GetLibraryBook(null), directory, "ext", Replacements)
|
||||
fileTemplate.GetFilename(GetLibraryBook(null), directory, "ext", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -552,11 +546,28 @@ namespace TemplatesTests
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>("foo<if series->-<series>-<id>-<-if series>bar", out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate
|
||||
.GetFilename(GetLibraryBook(), directory, "ext", Replacements)
|
||||
.GetFilename(GetLibraryBook(), directory, "ext", culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<audibletitle [u]>", "I", "en-US", "i")]
|
||||
[DataRow("<audibletitle [l]>", "ı", "tr-TR", "I")]
|
||||
[DataRow("<audibletitle [u]>", "İ", "tr-TR", "i")]
|
||||
public void Tag_culture_test(string template, string expected, string cultureName, string title)
|
||||
{
|
||||
var bookDto = Shared.GetLibraryBook();
|
||||
bookDto.Title = title;
|
||||
var culture = new System.Globalization.CultureInfo(cultureName);
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate
|
||||
.GetName(bookDto, new MultiConvertFileProperties { OutputFileName = string.Empty }, culture)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,7 +625,7 @@ namespace Templates_Other
|
||||
|
||||
Templates.TryGetTemplate<Templates.FolderTemplate>(template, out var fileNamingTemplate).Should().BeTrue();
|
||||
|
||||
return fileNamingTemplate.GetFilename(lbDto, dirFullPath, extension, Replacements).PathWithoutPrefix;
|
||||
return fileNamingTemplate.GetFilename(lbDto, dirFullPath, extension, culture: null, replacements: Replacements).PathWithoutPrefix;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -642,7 +653,8 @@ namespace Templates_Other
|
||||
Templates.TryGetTemplate<Templates.ChapterFileTemplate>(template, out var chapterFileTemplate).Should().BeTrue();
|
||||
|
||||
return chapterFileTemplate
|
||||
.GetFilename(lbDto, new MultiConvertFileProperties { Title = suffix, PartsTotal = partsTotal, PartsPosition = partsPosition, OutputFileName = string.Empty }, dir, estension, Replacements)
|
||||
.GetFilename(lbDto, new MultiConvertFileProperties { Title = suffix, PartsTotal = partsTotal, PartsPosition = partsPosition, OutputFileName = string.Empty }, dir, estension,
|
||||
culture: null, replacements: Replacements)
|
||||
.PathWithoutPrefix;
|
||||
}
|
||||
|
||||
@@ -661,7 +673,7 @@ namespace Templates_Other
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(fileName, out var fileNamingTemplate).Should().BeTrue();
|
||||
|
||||
fileNamingTemplate.GetFilename(lbDto, directory, "txt", Replacements).PathWithoutPrefix.Should().Be(outStr);
|
||||
fileNamingTemplate.GetFilename(lbDto, directory, "txt", culture: null, replacements: Replacements).PathWithoutPrefix.Should().Be(outStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1011,7 +1023,7 @@ namespace Templates_ChapterFile_Tests
|
||||
{
|
||||
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)
|
||||
.GetFilename(GetLibraryBook(), new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, ext, culture: null, replacements: Default)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user