Add human name parsing and formatting to naming templates
This commit is contained in:
parent
bba9c2ba7b
commit
3a4ab80892
@ -10,6 +10,7 @@ These templates apply to both GUI and CLI.
|
|||||||
- [Conditional Tags](#conditional-tags)
|
- [Conditional Tags](#conditional-tags)
|
||||||
- [Tag Formatters](#tag-formatters)
|
- [Tag Formatters](#tag-formatters)
|
||||||
- [Text Formatters](#text-formatters)
|
- [Text Formatters](#text-formatters)
|
||||||
|
- [Name List Formatters](#name-list-formatters)
|
||||||
- [Integer Formatters](#integer-formatters)
|
- [Integer Formatters](#integer-formatters)
|
||||||
- [Date Formatters](#date-formatters)
|
- [Date Formatters](#date-formatters)
|
||||||
|
|
||||||
@ -26,9 +27,9 @@ These tags will be replaced in the template with the audiobook's values.
|
|||||||
|\<id\> **†**|Audible book ID (ASIN)|Text|
|
|\<id\> **†**|Audible book ID (ASIN)|Text|
|
||||||
|\<title\>|Full title|Text|
|
|\<title\>|Full title|Text|
|
||||||
|\<title short\>|Title. Stop at first colon|Text|
|
|\<title short\>|Title. Stop at first colon|Text|
|
||||||
|\<author\>|Author(s)|Text|
|
|\<author\>|Author(s)|Name List|
|
||||||
|\<first author\>|First author|Text|
|
|\<first author\>|First author|Text|
|
||||||
|\<narrator\>|Narrator(s)|Text|
|
|\<narrator\>|Narrator(s)|Name List|
|
||||||
|\<first narrator\>|First narrator|Text|
|
|\<first narrator\>|First narrator|Text|
|
||||||
|\<series\>|Name of series|Text|
|
|\<series\>|Name of series|Text|
|
||||||
|\<series#\>|Number order in series|Text|
|
|\<series#\>|Number order in series|Text|
|
||||||
@ -73,7 +74,7 @@ As an example, this folder template will place all Liberated podcasts into a "Po
|
|||||||
|
|
||||||
|
|
||||||
# Tag Formatters
|
# Tag Formatters
|
||||||
**Text**, **Integer**, and **DateTime** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type.
|
**Text**, **Name List**, **Integer**, and **DateTime** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type.
|
||||||
|
|
||||||
## Text Formatters
|
## Text Formatters
|
||||||
|Formatter|Description|Example Usage|Example Result|
|
|Formatter|Description|Example Usage|Example Result|
|
||||||
@ -81,12 +82,18 @@ As an example, this folder template will place all Liberated podcasts into a "Po
|
|||||||
|L|Converts text to lowercase|\<title[L]\>|a study in scarlet꞉ a sherlock holmes novel|
|
|L|Converts text to lowercase|\<title[L]\>|a study in scarlet꞉ a sherlock holmes novel|
|
||||||
|U|Converts text to uppercase|\<title short[U]\>|A STUDY IN SCARLET|
|
|U|Converts text to uppercase|\<title short[U]\>|A STUDY IN SCARLET|
|
||||||
|
|
||||||
|
## Name List Formatters
|
||||||
|
|Formatter|Description|Example Usage|Example Result|
|
||||||
|
|-|-|-|-|
|
||||||
|
|separator()|Speficy the text used to join multiple people's names.<br><br>Default is ", "|`<author[separator(; )]>`|Arthur Conan Doyle; Stephen Fry|
|
||||||
|
|format(\{T \| F \| M \| L \| S\})|Formats the human name using the name part tags.<br>\{T\} = Title (e.g. "Dr.")<br>\{F\} = First name<br>\{M\} = Middle name<br>\{L\} = Last Name<br>\{S\} = Suffix (e.g. "PhD")<br><br>Default is \{P\} \{F\} \{M\} \{L\} \{S\} |`<author[format({L}, {F}) separator(; )]>`|Doyle, Arthur; Fry, Stephen|
|
||||||
|
|sort(F \| M \| L)|Sorts the names by first, middle, or last name<br><br>Default is unsorted|`<author[sort(M)]>`|Stephen Fry, Arthur Conan Doyle|
|
||||||
|
|max(#)|Only use the first # of names<br><br>Default is all names|`<author[max(1)]>`|Arthur Conan Doyle|
|
||||||
|
|
||||||
## Integer Formatters
|
## Integer Formatters
|
||||||
|Formatter|Description|Example Usage|Example Result|
|
|Formatter|Description|Example Usage|Example Result|
|
||||||
|-|-|-|-|
|
|-|-|-|-|
|
||||||
|# (a number)|Zero-pads the number|\<bitrate[4]\><br>\<series#[3]\><br>\<samplerate[6]\>|0128<br>001<br>044100|
|
|# (a number)|Zero-pads the number|\<bitrate\[4\]\><br>\<series#\[3\]\><br>\<samplerate\[6\]\>|0128<br>001<br>044100|
|
||||||
|
|
||||||
**Text**, **Integer**, and **DateTime** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type.
|
|
||||||
|
|
||||||
## Date Formatters
|
## Date Formatters
|
||||||
Form more standard formatters, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings).
|
Form more standard formatters, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings).
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
Icon="/Assets/libation.ico">
|
Icon="/Assets/libation.ico">
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,Auto,Auto">
|
<Grid RowDefinitions="Auto,Auto,Auto">
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Margin="10,10,10,0"
|
Margin="10,10,10,0"
|
||||||
|
|||||||
@ -46,7 +46,7 @@ namespace LibationAvalonia.Dialogs
|
|||||||
{
|
{
|
||||||
tokenSource.Cancel();
|
tokenSource.Cancel();
|
||||||
//If this dialog is closed before it's completed, Closing is fired
|
//If this dialog is closed before it's completed, Closing is fired
|
||||||
//once for the form clising and again for the MessageBox closing.
|
//once for the form closing and again for the MessageBox closing.
|
||||||
Closing -= LocateAudiobooksDialog_Closing;
|
Closing -= LocateAudiobooksDialog_Closing;
|
||||||
this.SaveSizeAndLocation(Configuration.Instance);
|
this.SaveSizeAndLocation(Configuration.Instance);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding RemoveMenuItemsEnabled}" Click="removeSomeAccountsToolStripMenuItem_Click" Header="_Remove Books from Some Accounts" />
|
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding RemoveMenuItemsEnabled}" Click="removeSomeAccountsToolStripMenuItem_Click" Header="_Remove Books from Some Accounts" />
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Click="locateAudiobooksToolStripMenuItem_Click" Header="Locate Audiobooks" />
|
<MenuItem Click="locateAudiobooksToolStripMenuItem_Click" Header="L_ocate Audiobooks" />
|
||||||
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
||||||
|
<PackageReference Include="NameParserSharp" Version="1.5.0" />
|
||||||
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
|
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,12 @@
|
|||||||
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 AaxDecrypter;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using FileManager;
|
using FileManager;
|
||||||
using FileManager.NamingTemplate;
|
using FileManager.NamingTemplate;
|
||||||
|
using NameParser;
|
||||||
|
|
||||||
namespace LibationFileManager
|
namespace LibationFileManager
|
||||||
{
|
{
|
||||||
@ -68,6 +70,8 @@ namespace LibationFileManager
|
|||||||
Configuration.Instance.PropertyChanged +=
|
Configuration.Instance.PropertyChanged +=
|
||||||
[PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))]
|
[PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))]
|
||||||
(_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>((string)e.NewValue);
|
(_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>((string)e.NewValue);
|
||||||
|
|
||||||
|
HumanName.Suffixes.Add("ret");
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -198,9 +202,9 @@ namespace LibationFileManager
|
|||||||
{ TemplateTags.Id, lb => lb.AudibleProductId, v => v },
|
{ TemplateTags.Id, lb => lb.AudibleProductId, v => v },
|
||||||
{ TemplateTags.Title, lb => lb.Title },
|
{ TemplateTags.Title, lb => lb.Title },
|
||||||
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
|
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
|
||||||
{ TemplateTags.Author, lb => lb.AuthorNames },
|
{ TemplateTags.Author, lb => lb.Authors, NameFormatter },
|
||||||
{ TemplateTags.FirstAuthor, lb => lb.FirstAuthor },
|
{ TemplateTags.FirstAuthor, lb => lb.FirstAuthor },
|
||||||
{ TemplateTags.Narrator, lb => lb.NarratorNames },
|
{ TemplateTags.Narrator, lb => lb.Narrators, NameFormatter },
|
||||||
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator },
|
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator },
|
||||||
{ TemplateTags.Series, lb => lb.SeriesName },
|
{ TemplateTags.Series, lb => lb.SeriesName },
|
||||||
{ TemplateTags.SeriesNumber, lb => lb.SeriesNumber },
|
{ TemplateTags.SeriesNumber, lb => lb.SeriesNumber },
|
||||||
@ -247,6 +251,70 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
#region Tag Formatters
|
#region Tag Formatters
|
||||||
|
|
||||||
|
private static readonly string[] suffixes = { "introductions", "introduction", "adaptation", "translator", "contributor", "illustrator", "director", "foreword", "editor", "preface", "adaptor", "afterword", "interviewer", "introductions", "essay", "editor/introduction" };
|
||||||
|
|
||||||
|
private static string removeSuffix(string namesString)
|
||||||
|
{
|
||||||
|
foreach (var suffix in suffixes)
|
||||||
|
namesString = namesString.Replace($" - {suffix}", "");
|
||||||
|
|
||||||
|
return namesString.Replace('’', '\'').Replace(" - Ret.", ", Ret.").Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Format must have at least one of the string {T}, {F}, {M}, {L}, or {S}
|
||||||
|
private static readonly Regex FormatRegex = new(@"[Ff]ormat\((.*?(?:{[TFMLS]})+.*?)\)", RegexOptions.Compiled);
|
||||||
|
//Sort must have exactly one of the characters F, M, or L
|
||||||
|
private static readonly Regex SortRegex = new(@"[Ss]ort\(\s*?([FML])\s*?\)", RegexOptions.Compiled);
|
||||||
|
//Max must have a 1 or 2-digit number
|
||||||
|
private static readonly Regex MaxRegex = new(@"[Mm]ax\(\s*?(\d{1,2})\s*?\)", RegexOptions.Compiled);
|
||||||
|
//Separator can be anything
|
||||||
|
private static readonly Regex SeparatorRegex = new(@"[Ss]eparator\((.*?)\)", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static string NameFormatter(ITemplateTag templateTag, IEnumerable<string> value, string formatString)
|
||||||
|
{
|
||||||
|
var names = value.Select(n => new HumanName(removeSuffix(n), Prefer.FirstOverPrefix));
|
||||||
|
|
||||||
|
var formatMatch = FormatRegex.Match(formatString);
|
||||||
|
string nameFormatString
|
||||||
|
= formatMatch.Success
|
||||||
|
? formatMatch.Groups[1].Value
|
||||||
|
.Replace("{T}", "{0}")
|
||||||
|
.Replace("{F}", "{1}")
|
||||||
|
.Replace("{M}", "{2}")
|
||||||
|
.Replace("{L}", "{3}")
|
||||||
|
.Replace("{S}", "{4}")
|
||||||
|
: "{0} {1} {2} {3} {4}"; // T F M L S
|
||||||
|
|
||||||
|
var maxMatch = MaxRegex.Match(formatString);
|
||||||
|
int maxNames = maxMatch.Success && int.TryParse(maxMatch.Groups[1].Value, out var max) ? int.Max(1, max) : int.MaxValue;
|
||||||
|
|
||||||
|
var separatorMatch = SeparatorRegex.Match(formatString);
|
||||||
|
var separatorString = separatorMatch.Success ? separatorMatch.Groups[1].Value : ", ";
|
||||||
|
|
||||||
|
var sortMatch = SortRegex.Match(formatString);
|
||||||
|
var sortedNames
|
||||||
|
= sortMatch.Success
|
||||||
|
? (
|
||||||
|
sortMatch.Groups[1].Value.ToUpper() == "F" ? names.OrderBy(n => n.First)
|
||||||
|
: sortMatch.Groups[1].Value.ToUpper() == "M" ? names.OrderBy(n => n.Middle)
|
||||||
|
: sortMatch.Groups[1].Value.ToUpper() == "L" ? names.OrderBy(n => n.Last)
|
||||||
|
: names
|
||||||
|
)
|
||||||
|
: names;
|
||||||
|
|
||||||
|
var formattedNames = string.Join(
|
||||||
|
separatorString,
|
||||||
|
sortedNames
|
||||||
|
.Take(int.Min(sortedNames.Count(), maxNames))
|
||||||
|
.Select(n => string.Format(nameFormatString, n.Title, n.First, n.Middle, n.Last, n.Suffix).Trim())
|
||||||
|
);
|
||||||
|
|
||||||
|
while (formattedNames.Contains(" "))
|
||||||
|
formattedNames = formattedNames.Replace(" ", " ");
|
||||||
|
|
||||||
|
return formattedNames;
|
||||||
|
}
|
||||||
|
|
||||||
private static string getTitleShort(string title)
|
private static string getTitleShort(string title)
|
||||||
=> title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title;
|
=> title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title;
|
||||||
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(LibationFileManager) + ".Tests")]
|
|
||||||
2
Source/LibationWinForms/Form1.Designer.cs
generated
2
Source/LibationWinForms/Form1.Designer.cs
generated
@ -569,7 +569,7 @@
|
|||||||
//
|
//
|
||||||
this.locateAudiobooksToolStripMenuItem.Name = "locateAudiobooksToolStripMenuItem";
|
this.locateAudiobooksToolStripMenuItem.Name = "locateAudiobooksToolStripMenuItem";
|
||||||
this.locateAudiobooksToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
|
this.locateAudiobooksToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
|
||||||
this.locateAudiobooksToolStripMenuItem.Text = "Locate Audiobooks";
|
this.locateAudiobooksToolStripMenuItem.Text = "L&ocate Audiobooks";
|
||||||
this.locateAudiobooksToolStripMenuItem.Click += new System.EventHandler(this.locateAudiobooksToolStripMenuItem_Click);
|
this.locateAudiobooksToolStripMenuItem.Click += new System.EventHandler(this.locateAudiobooksToolStripMenuItem_Click);
|
||||||
//
|
//
|
||||||
// toolStripSeparator3
|
// toolStripSeparator3
|
||||||
|
|||||||
@ -238,6 +238,29 @@ namespace TemplatesTests
|
|||||||
.Should().Be(expected);
|
.Should().Be(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow("<author>", "Arthur Conan Doyle, Stephen Fry")]
|
||||||
|
[DataRow("<author[]>", "Arthur Conan Doyle, Stephen Fry")]
|
||||||
|
[DataRow("<author[sort(F)]>", "Arthur Conan Doyle, Stephen Fry")]
|
||||||
|
[DataRow("<author[sort(L)]>", "Arthur Conan Doyle, Stephen Fry")]
|
||||||
|
[DataRow("<author[sort(M)]>", "Stephen Fry, Arthur Conan Doyle")]
|
||||||
|
[DataRow("<author[sort(m)]>", "Arthur Conan Doyle, Stephen Fry")]
|
||||||
|
[DataRow("<author [ max( 1 ) ]>", "Arthur Conan Doyle")]
|
||||||
|
[DataRow("<author[max(2)]>", "Arthur Conan Doyle, Stephen Fry")]
|
||||||
|
[DataRow("<author[max(3)]>", "Arthur Conan Doyle, Stephen Fry")]
|
||||||
|
[DataRow("<author[format({L}, {F})]>", "Doyle, Arthur, Fry, Stephen")]
|
||||||
|
[DataRow("<author[format({f}, {l})]>", "Arthur Conan Doyle, Stephen Fry")]
|
||||||
|
[DataRow("<author[format(First={F}, Last={L})]>", "First=Arthur, Last=Doyle, First=Stephen, Last=Fry")]
|
||||||
|
[DataRow("<author[format({L}, {F}) separator( - )]>", "Doyle, Arthur - Fry, Stephen")]
|
||||||
|
public void NameFormat(string template, string expected)
|
||||||
|
{
|
||||||
|
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||||
|
fileTemplate
|
||||||
|
.GetFilename(GetLibraryBook(), "", "", Replacements)
|
||||||
|
.PathWithoutPrefix
|
||||||
|
.Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)]
|
[DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)]
|
||||||
[DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]
|
[DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user