diff --git a/Documentation/NamingTemplates.md b/Documentation/NamingTemplates.md index ce1cc17f..8f5d3e2e 100644 --- a/Documentation/NamingTemplates.md +++ b/Documentation/NamingTemplates.md @@ -17,6 +17,9 @@ These templates apply to both GUI and CLI. - [Conditional Tags](#conditional-tags) - [Tag Formatters](#tag-formatters) - [Text Formatters](#text-formatters) + - [Series Formatters](#series-formatters) + - [Series List Formatters](#series-list-formatters) + - [Name Formatters](#name-formatters) - [Name List Formatters](#name-list-formatters) - [Number Formatters](#number-formatters) - [Date Formatters](#date-formatters) @@ -32,32 +35,33 @@ These tags will be replaced in the template with the audiobook's values. |Tag|Description|Type| |-|-|-| |\ **†**|Audible book ID (ASIN)|Text| -|\|Full title with subtitle|Text| -|\|Title. Stop at first colon|Text| -|\<audible title\>|Audible's title (does not include subtitle)|Text| -|\<audible subtitle\>|Audible's subtitle|Text| -|\<author\>|Author(s)|Name List| -|\<first author\>|First author|Text| -|\<narrator\>|Narrator(s)|Name List| -|\<first narrator\>|First narrator|Text| -|\<series\>|Name of series|Text| -|\<series#\>|Number order in series|Number| -|\<bitrate\>|File's original bitrate (Kbps)|Number| -|\<samplerate\>|File's original audio sample rate|Number| -|\<channels\>|Number of audio channels|Number| -|\<account\>|Audible account of this book|Text| -|\<account nickname\>|Audible account nickname of this book|Text| -|\<locale\>|Region/country|Text| -|\<year\>|Year published|Number| -|\<language\>|Book's language|Text| +|\<title\>|Full title with subtitle|[Text](#text-formatters)| +|\<title short\>|Title. Stop at first colon|[Text](#text-formatters)| +|\<audible title\>|Audible's title (does not include subtitle)|[Text](#text-formatters)| +|\<audible subtitle\>|Audible's subtitle|[Text](#text-formatters)| +|\<author\>|Author(s)|[Name List](#name-list-formatters)| +|\<first author\>|First author|[Name](#name-formatters)| +|\<narrator\>|Narrator(s)|[Name List](#name-list-formatters)| +|\<first narrator\>|First narrator|[Name](#name-formatters)| +|\<series\>|All series to which the book belongs (if any)|[Series List](#series-list-formatters)| +|\<first series\>|First series|[Series](#series-formatters)| +|\<series#\>|Number order in series (alias for \<first series[{#}]\>|[Number](#number-formatters)| +|\<bitrate\>|File's original bitrate (Kbps)|[Number](#number-formatters)| +|\<samplerate\>|File's original audio sample rate|[Number](#number-formatters)| +|\<channels\>|Number of audio channels|[Number](#number-formatters)| +|\<account\>|Audible account of this book|[Text](#text-formatters)| +|\<account nickname\>|Audible account nickname of this book|[Text](#text-formatters)| +|\<locale\>|Region/country|[Text](#text-formatters)| +|\<year\>|Year published|[Number](#number-formatters)| +|\<language\>|Book's language|[Text](#text-formatters)| |\<language short\> **†**|Book's language abbreviated. Eg: ENG|Text| -|\<file date\>|File creation date/time.|DateTime| -|\<pub date\>|Audiobook publication date|DateTime| -|\<date added\>|Date the book added to your Audible account|DateTime| -|\<ch count\> **‡**|Number of chapters|Number| -|\<ch title\> **‡**|Chapter title|Text| -|\<ch#\> **‡**|Chapter number|Number| -|\<ch# 0\> **‡**|Chapter number with leading zeros|Number| +|\<file date\>|File creation date/time.|[DateTime](#date-formatters)| +|\<pub date\>|Audiobook publication date|[DateTime](#date-formatters)| +|\<date added\>|Date the book added to your Audible account|[DateTime](#date-formatters)| +|\<ch count\> **‡**|Number of chapters|[Number](#number-formatters)| +|\<ch title\> **‡**|Chapter title|[Text](#text-formatters)| +|\<ch#\> **‡**|Chapter number|[Number](#number-formatters)| +|\<ch# 0\> **‡**|Chapter number with leading zeros|[Number](#number-formatters)| **†** Does not support custom formatting @@ -95,11 +99,28 @@ 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| |U|Converts text to uppercase|\<title short[U]\>|A STUDY IN SCARLET| +## Series Formatters +|Formatter|Description|Example Usage|Example Result| +|-|-|-|-| +|\{N \| # \| ID\}|Formats the series using<br>the series part tags.<br>\{N\} = Series Name<br>\{#\} = Number order in series<br>\{ID\} = Audible Series ID<br><br>Default is \{N\}|`<first series>`<hr>`<first series[{N}]>`<hr>`<first series[{N}, {#}, {ID}]>`|Sherlock Holmes<hr>Sherlock Holmes<hr>Sherlock Holmes, 1, B08376S3R2| + +## Series List Formatters +|Formatter|Description|Example Usage|Example Result| +|-|-|-|-| +|separator()|Speficy the text used to join<br>multiple series names.<br><br>Default is ", "|`<series[separator(; )]>`|Sherlock Holmes; Some Other Series| +|format(\{N \| # \| ID\})|Formats the series properties<br>using the name series tags.<br>See [Series Formatter Usage](#series-formatters) above.|`<series[format({N}, {#})`<br>`separator(; )]>`<hr>`<author[format({L}, {ID}) separator(; )]>`|Sherlock Holmes, 1; Some Other Series, 1<hr>herlock Holmes, B08376S3R2; Some Other Series, B000000000| +|max(#)|Only use the first # of series<br><br>Default is all series|`<series[max(1)]>`|Sherlock Holmes| + +## Name Formatters +|Formatter|Description|Example Usage|Example Result| +|-|-|-|-| +|\{T \| F \| M \| L \| S \| ID\}|Formats the human name using<br>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>\{ID\} = Audible Contributor ID<br><br>Default is \{P\} \{F\} \{M\} \{L\} \{S\}|`<first narrator[{L}, {F}]>`<hr>`<first author[{L}, {F} _{ID}_]>`|Fry, Stephen<hr>Doyle, Arthur \_B000AQ43GQ\_;<br>Fry, Stephen \_B000APAGVS\_| + ## Name List Formatters |Formatter|Description|Example Usage|Example Result| |-|-|-|-| |separator()|Speficy the text used to join<br>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<br>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})`<br>`separator(; )]>`|Doyle, Arthur; Fry, Stephen| +|format(\{T \| F \| M \| L \| S \| ID\})|Formats the human name using<br>the name part tags.<br>See [Name Formatter Usage](#name-formatters) above.|`<author[format({L}, {F})`<br>`separator(; )]>`<hr>`<author[format({L}, {F}`<br>`_{ID}_) separator(; )]>`|Doyle, Arthur; Fry, Stephen<hr>Doyle, Arthur \_B000AQ43GQ\_;<br>Fry, Stephen \_B000APAGVS\_| |sort(F \| M \| L)|Sorts the names by first, middle,<br>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| diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 57b86ec8..19c154b0 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -418,7 +418,6 @@ namespace AppScaffolding public List<string> Filters { get; set; } = new(); } - public static void migrate_to_v12_0_1(Configuration config) { #nullable enable diff --git a/Source/DataLayer/EfClasses/Contributor.cs b/Source/DataLayer/EfClasses/Contributor.cs index 9c88d96b..4c42d363 100644 --- a/Source/DataLayer/EfClasses/Contributor.cs +++ b/Source/DataLayer/EfClasses/Contributor.cs @@ -43,5 +43,7 @@ namespace DataLayer } public override string ToString() => Name; + public void SetAudibleContributorId(string audibleContributorId) + => AudibleContributorId = audibleContributorId; } } diff --git a/Source/DtoImporterService/ContributorImporter.cs b/Source/DtoImporterService/ContributorImporter.cs index 2f6d255f..99a1304f 100644 --- a/Source/DtoImporterService/ContributorImporter.cs +++ b/Source/DtoImporterService/ContributorImporter.cs @@ -61,19 +61,19 @@ namespace DtoImporterService private int upsertPeople(List<Person> people) { - var hash = people - // new people only - .Where(p => !Cache.ContainsKey(p.Name)) - // remove duplicates by Name. first in wins - .ToDictionarySafe(p => p.Name); - - foreach (var kvp in hash) + var qtyNew = 0; + foreach (var person in people) { - var person = kvp.Value; - addContributor(person.Name, person.Asin); + if (!Cache.TryGetValue(person.Name, out var contributor)) + { + contributor = createContributor(person.Name, person.Asin); + qtyNew++; + } + + updateContributor(person, contributor); } - return hash.Count; + return qtyNew; } // only use after loading contributors => local @@ -86,16 +86,22 @@ namespace DtoImporterService .ToHashSet(); foreach (var pub in hash) - addContributor(pub); + createContributor(pub); return hash.Count; } - private Contributor addContributor(string name, string id = null) + private void updateContributor(Person person, Contributor contributor) + { + if (person.Asin != contributor.AudibleContributorId) + contributor.SetAudibleContributorId(person.Asin); + } + + private Contributor createContributor(string name, string id = null) { try { - var newContrib = new Contributor(name); + var newContrib = new Contributor(name, id); var entityEntry = DbContext.Contributors.Add(newContrib); var entity = entityEntry.Entity; diff --git a/Source/FileLiberator/AudioFileStorageExt.cs b/Source/FileLiberator/AudioFileStorageExt.cs index b2eda7cf..ff47fd59 100644 --- a/Source/FileLiberator/AudioFileStorageExt.cs +++ b/Source/FileLiberator/AudioFileStorageExt.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using DataLayer; using LibationFileManager; +using LibationFileManager.Templates; namespace FileLiberator { diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 47ea4bb6..d5d6a3f6 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -158,7 +158,7 @@ namespace FileLiberator if (success && config.SaveMetadataToFile) { - var metadataFile = Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json"); + var metadataFile = LibationFileManager.Templates.Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json"); var item = await api.GetCatalogProductAsync(libraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS); item.SourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(contentLic.ContentMetadata.ChapterInfo)); diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs index 76b80967..8d9dbb12 100644 --- a/Source/FileLiberator/DownloadOptions.cs +++ b/Source/FileLiberator/DownloadOptions.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using System; using System.IO; using ApplicationServices; +using LibationFileManager.Templates; namespace FileLiberator { @@ -26,8 +27,8 @@ namespace FileLiberator public string Publisher => LibraryBook.Book.Publisher; public string Language => LibraryBook.Book.Language; public string AudibleProductId => LibraryBookDto.AudibleProductId; - public string SeriesName => LibraryBookDto.SeriesName; - public float? SeriesNumber => LibraryBookDto.SeriesNumber; + public string SeriesName => LibraryBookDto.FirstSeries?.Name; + public float? SeriesNumber => LibraryBookDto.FirstSeries?.Number; public NAudio.Lame.LameConfig LameConfig { get; init; } public string UserAgent => AudibleApi.Resources.Download_User_Agent; public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio; diff --git a/Source/FileLiberator/UtilityExtensions.cs b/Source/FileLiberator/UtilityExtensions.cs index 575188ad..6a920990 100644 --- a/Source/FileLiberator/UtilityExtensions.cs +++ b/Source/FileLiberator/UtilityExtensions.cs @@ -5,8 +5,9 @@ using System.Threading.Tasks; using AudibleUtilities; using DataLayer; using Dinah.Core; -using LibationFileManager; +using LibationFileManager.Templates; +#nullable enable namespace FileLiberator { public static class UtilityExtensions @@ -47,12 +48,10 @@ namespace FileLiberator YearPublished = libraryBook.Book.DatePublished?.Year, DatePublished = libraryBook.Book.DatePublished, - Authors = libraryBook.Book.Authors.Select(c => c.Name).ToList(), + Authors = libraryBook.Book.Authors.Select(c => new ContributorDto(c.Name, c.AudibleContributorId)).ToList(), + Narrators = libraryBook.Book.Narrators.Select(c => new ContributorDto(c.Name, c.AudibleContributorId)).ToList(), - Narrators = libraryBook.Book.Narrators.Select(c => c.Name).ToList(), - - SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name, - SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Index, + Series = getSeries(libraryBook.Book.SeriesLink), IsPodcastParent = libraryBook.Book.IsEpisodeParent(), IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(), @@ -62,5 +61,21 @@ namespace FileLiberator Language = libraryBook.Book.Language }; } + + private static List<SeriesDto>? getSeries(IEnumerable<SeriesBook> seriesBooks) + { + if (!seriesBooks.Any()) + return null; + + //I don't remember why or if there was a good reason not to have series numbers for + //podcast parents, but preserving the behavior for backwards compatibility. + return seriesBooks + .Select(sb + => new SeriesDto( + sb.Series.Name, + sb.Book.IsEpisodeParent() ? null : sb.Index, + sb.Series.AudibleSeriesId) + ).ToList(); + } } } diff --git a/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs b/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs index 59559b04..6d1482cd 100644 --- a/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs +++ b/Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using LibationAvalonia.Dialogs; using LibationAvalonia.ViewModels.Settings; using LibationFileManager; +using LibationFileManager.Templates; using System.Threading.Tasks; namespace LibationAvalonia.Controls.Settings diff --git a/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs b/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs index 99c8efdd..0a55b0e4 100644 --- a/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs +++ b/Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using LibationAvalonia.Dialogs; using LibationAvalonia.ViewModels.Settings; using LibationFileManager; +using LibationFileManager.Templates; using System.Threading.Tasks; namespace LibationAvalonia.Controls.Settings diff --git a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs index 3a7010b4..b7224ae3 100644 --- a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Styling; using Dinah.Core; using LibationFileManager; +using LibationFileManager.Templates; using ReactiveUI; using System; using System.IO; diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs index 707399a0..d94adb16 100644 --- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs +++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs @@ -12,6 +12,7 @@ using LibationAvalonia.Controls; using LibationAvalonia.Dialogs; using LibationAvalonia.ViewModels; using LibationFileManager; +using LibationFileManager.Templates; using LibationUiBase.GridView; using ReactiveUI; using System; @@ -350,7 +351,7 @@ namespace LibationAvalonia.Views #region Edit Templates (Single book only) async Task editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate) - where T : Templates, LibationFileManager.ITemplate, new() + where T : Templates, LibationFileManager.Templates.ITemplate, new() { var template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate); var form = new EditTemplateDialog(template); diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs index 33a61e76..f9192a02 100644 --- a/Source/LibationFileManager/Configuration.PersistentSettings.cs +++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs @@ -306,41 +306,41 @@ namespace LibationFileManager [Description("How to format the folders in which files will be saved")] public string FolderTemplate { - get => getTemplate<Templates.FolderTemplate>(); - set => setTemplate<Templates.FolderTemplate>(value); + get => getTemplate<Templates.Templates.FolderTemplate>(); + set => setTemplate<Templates.Templates.FolderTemplate>(value); } [Description("How to format the saved pdf and audio files")] public string FileTemplate { - get => getTemplate<Templates.FileTemplate>(); - set => setTemplate<Templates.FileTemplate>(value); + get => getTemplate<Templates.Templates.FileTemplate>(); + set => setTemplate<Templates.Templates.FileTemplate>(value); } [Description("How to format the saved audio files when split by chapters")] public string ChapterFileTemplate { - get => getTemplate<Templates.ChapterFileTemplate>(); - set => setTemplate<Templates.ChapterFileTemplate>(value); + get => getTemplate<Templates.Templates.ChapterFileTemplate>(); + set => setTemplate<Templates.Templates.ChapterFileTemplate>(value); } [Description("How to format the file's Title stored in metadata")] public string ChapterTitleTemplate { - get => getTemplate<Templates.ChapterTitleTemplate>(); - set => setTemplate<Templates.ChapterTitleTemplate>(value); + get => getTemplate<Templates.Templates.ChapterTitleTemplate>(); + set => setTemplate<Templates.Templates.ChapterTitleTemplate>(value); } private string getTemplate<T>([CallerMemberName] string propertyName = "") - where T : Templates, ITemplate, new() + where T : Templates.Templates, Templates.ITemplate, new() { - return Templates.GetTemplate<T>(GetString(defaultValue: T.DefaultTemplate, propertyName)).TemplateText; + return Templates.Templates.GetTemplate<T>(GetString(defaultValue: T.DefaultTemplate, propertyName)).TemplateText; } private void setTemplate<T>(string newValue, [CallerMemberName] string propertyName = "") - where T : Templates, ITemplate, new() + where T : Templates.Templates, Templates.ITemplate, new() { - SetString(Templates.GetTemplate<T>(newValue).TemplateText, propertyName); + SetString(Templates.Templates.GetTemplate<T>(newValue).TemplateText, propertyName); } #endregion } diff --git a/Source/LibationFileManager/LibraryBookDto.cs b/Source/LibationFileManager/LibraryBookDto.cs deleted file mode 100644 index 9059d364..00000000 --- a/Source/LibationFileManager/LibraryBookDto.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -#nullable enable -namespace LibationFileManager -{ - public class BookDto - { - public string? AudibleProductId { get; set; } - public string? Title { get; set; } - public string? Subtitle { get; set; } - public string? TitleWithSubtitle { get; set; } - public string? Locale { get; set; } - public int? YearPublished { get; set; } - - public IEnumerable<string>? Authors { get; set; } - public string? AuthorNames => Authors is null ? null : string.Join(", ", Authors); - public string? FirstAuthor => Authors?.FirstOrDefault(); - - public IEnumerable<string>? Narrators { get; set; } - public string? NarratorNames => Narrators is null? null: string.Join(", ", Narrators); - public string? FirstNarrator => Narrators?.FirstOrDefault(); - - public string? SeriesName { get; set; } - public float? SeriesNumber { get; set; } - public bool IsSeries => !string.IsNullOrEmpty(SeriesName); - public bool IsPodcastParent { get; set; } - public bool IsPodcast { get; set; } - - public int BitRate { get; set; } - public int SampleRate { get; set; } - public int Channels { get; set; } - public DateTime FileDate { get; set; } = DateTime.Now; - public DateTime? DatePublished { get; set; } - public string? Language { get; set; } - } - - public class LibraryBookDto : BookDto - { - public DateTime? DateAdded { get; set; } - public string? Account { get; set; } - public string? AccountNickname { get; set; } - } -} diff --git a/Source/LibationFileManager/NameListFormat.cs b/Source/LibationFileManager/NameListFormat.cs deleted file mode 100644 index 1e906d58..00000000 --- a/Source/LibationFileManager/NameListFormat.cs +++ /dev/null @@ -1,98 +0,0 @@ -using FileManager.NamingTemplate; -using NameParser; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; - -#nullable enable -namespace LibationFileManager -{ - internal partial class NameListFormat - { - public static string Formatter(ITemplateTag _, IEnumerable<string>? names, string formatString) - { - if (names is null) return ""; - - var humanNames = names.Select(n => new HumanName(RemoveSuffix(n), Prefer.FirstOverPrefix)); - - var sortedNames = Sort(humanNames, formatString); - var nameFormatString = Format(formatString, defaultValue: "{T} {F} {M} {L} {S}"); - var separatorString = Separator(formatString, defaultValue: ", "); - var maxNames = Max(formatString, defaultValue: humanNames.Count()); - - var formattedNames = string.Join(separatorString, sortedNames.Take(maxNames).Select(n => FormatName(n, nameFormatString))); - - while (formattedNames.Contains(" ")) - formattedNames = formattedNames.Replace(" ", " "); - - return formattedNames; - } - - private static string RemoveSuffix(string namesString) - { - namesString = namesString.Replace('’', '\'').Replace(" - Ret.", ", Ret."); - int dashIndex = namesString.IndexOf(" - "); - return (dashIndex > 0 ? namesString[..dashIndex] : namesString).Trim(); - } - - private static IEnumerable<HumanName> Sort(IEnumerable<HumanName> humanNames, string formatString) - { - var sortMatch = SortRegex().Match(formatString); - return - sortMatch.Success - ? sortMatch.Groups[1].Value == "F" ? humanNames.OrderBy(n => n.First) - : sortMatch.Groups[1].Value == "M" ? humanNames.OrderBy(n => n.Middle) - : sortMatch.Groups[1].Value == "L" ? humanNames.OrderBy(n => n.Last) - : humanNames - : humanNames; - } - - private static string Format(string formatString, string defaultValue) - { - var formatMatch = FormatRegex().Match(formatString); - return formatMatch.Success ? formatMatch.Groups[1].Value : defaultValue; - } - - private static string Separator(string formatString, string defaultValue) - { - var separatorMatch = SeparatorRegex().Match(formatString); - return separatorMatch.Success ? separatorMatch.Groups[1].Value : defaultValue; - } - - private static int Max(string formatString, int defaultValue) - { - var maxMatch = MaxRegex().Match(formatString); - return maxMatch.Success && int.TryParse(maxMatch.Groups[1].Value, out var max) ? int.Max(1, max) : defaultValue; - } - - private static string FormatName(HumanName humanName, string nameFormatString) - { - //Single-word names parse as first names. Use it as last name. - var lastName = string.IsNullOrWhiteSpace(humanName.Last) ? humanName.First : humanName.Last; - - nameFormatString - = nameFormatString - .Replace("{T}", "{0}") - .Replace("{F}", "{1}") - .Replace("{M}", "{2}") - .Replace("{L}", "{3}") - .Replace("{S}", "{4}"); - - return string.Format(nameFormatString, humanName.Title, humanName.First, humanName.Middle, lastName, humanName.Suffix).Trim(); - } - - /// <summary> Sort must have exactly one of the characters F, M, or L </summary> - [GeneratedRegex(@"[Ss]ort\(\s*?([FML])\s*?\)")] - private static partial Regex SortRegex(); - /// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, or {S} </summary> - [GeneratedRegex(@"[Ff]ormat\((.*?(?:{[TFMLS]})+.*?)\)")] - private static partial Regex FormatRegex(); - /// <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(); - } -} diff --git a/Source/LibationFileManager/Templates/ContributorDto.cs b/Source/LibationFileManager/Templates/ContributorDto.cs new file mode 100644 index 00000000..e52d26d8 --- /dev/null +++ b/Source/LibationFileManager/Templates/ContributorDto.cs @@ -0,0 +1,44 @@ +using NameParser; +using System; + +#nullable enable +namespace LibationFileManager.Templates; + +public class ContributorDto : IFormattable +{ + public HumanName HumanName { get; } + public string? AudibleContributorId { get; } + public ContributorDto(string name, string? audibleContributorId) + { + HumanName = new HumanName(RemoveSuffix(name), Prefer.FirstOverPrefix); + AudibleContributorId = audibleContributorId; + } + + public override string ToString() + => ToString("{T} {F} {M} {L} {S}", null); + + public string ToString(string? format, IFormatProvider? _) + { + if (string.IsNullOrWhiteSpace(format)) + return ToString(); + + //Single-word names parse as first names. Use it as last name. + var lastName = string.IsNullOrWhiteSpace(HumanName.Last) ? HumanName.First : HumanName.Last; + + return format + .Replace("{T}", HumanName.Title) + .Replace("{F}", HumanName.First) + .Replace("{M}", HumanName.Middle) + .Replace("{L}", lastName) + .Replace("{S}", HumanName.Suffix) + .Replace("{ID}", AudibleContributorId) + .Trim(); + } + + private static string RemoveSuffix(string namesString) + { + namesString = namesString.Replace('’', '\'').Replace(" - Ret.", ", Ret."); + int dashIndex = namesString.IndexOf(" - "); + return (dashIndex > 0 ? namesString[..dashIndex] : namesString).Trim(); + } +} diff --git a/Source/LibationFileManager/Templates/IListFormat[TList].cs b/Source/LibationFileManager/Templates/IListFormat[TList].cs new file mode 100644 index 00000000..e1911a16 --- /dev/null +++ b/Source/LibationFileManager/Templates/IListFormat[TList].cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +#nullable enable +namespace LibationFileManager.Templates; + +internal partial interface IListFormat<TList> where TList : IListFormat<TList> +{ + static string Join<T>(string formatString, IEnumerable<T> items) + where T : IFormattable + { + var itemFormatter = Formatter(formatString); + var separatorString = Separator(formatString) ?? ", "; + var maxValues = Max(formatString) ?? items.Count(); + + 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) + { + 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 : ", "; + } + } + + static abstract Regex FormatRegex(); + + /// <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(); +} diff --git a/Source/LibationFileManager/Templates/LibraryBookDto.cs b/Source/LibationFileManager/Templates/LibraryBookDto.cs new file mode 100644 index 00000000..be26c989 --- /dev/null +++ b/Source/LibationFileManager/Templates/LibraryBookDto.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +#nullable enable +namespace LibationFileManager.Templates; + +public class BookDto +{ + public string? AudibleProductId { get; set; } + public string? Title { get; set; } + public string? Subtitle { get; set; } + public string? TitleWithSubtitle { get; set; } + public string? Locale { get; set; } + public int? YearPublished { get; set; } + + public IEnumerable<ContributorDto>? Authors { get; set; } + public ContributorDto? FirstAuthor => Authors?.FirstOrDefault(); + + public IEnumerable<ContributorDto>? Narrators { get; set; } + public ContributorDto? FirstNarrator => Narrators?.FirstOrDefault(); + + public IEnumerable<SeriesDto>? Series { get; set; } + public SeriesDto? FirstSeries => Series?.FirstOrDefault(); + + public bool IsSeries => Series is not null; + public bool IsPodcastParent { get; set; } + public bool IsPodcast { get; set; } + + public int BitRate { get; set; } + public int SampleRate { get; set; } + public int Channels { get; set; } + public DateTime FileDate { get; set; } = DateTime.Now; + public DateTime? DatePublished { get; set; } + public string? Language { get; set; } +} + +public class LibraryBookDto : BookDto +{ + public DateTime? DateAdded { get; set; } + public string? Account { get; set; } + public string? AccountNickname { get; set; } +} diff --git a/Source/LibationFileManager/Templates/NameListFormat.cs b/Source/LibationFileManager/Templates/NameListFormat.cs new file mode 100644 index 00000000..efe3c35d --- /dev/null +++ b/Source/LibationFileManager/Templates/NameListFormat.cs @@ -0,0 +1,33 @@ +using FileManager.NamingTemplate; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +#nullable enable +namespace LibationFileManager.Templates; + +internal partial class NameListFormat : IListFormat<NameListFormat> +{ + public static string Formatter(ITemplateTag _, IEnumerable<ContributorDto>? names, string formatString) + => names is null ? string.Empty + : IListFormat<NameListFormat>.Join(formatString, Sort(names, formatString)); + + private static IEnumerable<ContributorDto> Sort(IEnumerable<ContributorDto> names, string formatString) + { + var sortMatch = SortRegex().Match(formatString); + return + sortMatch.Success + ? sortMatch.Groups[1].Value == "F" ? names.OrderBy(n => n.HumanName.First) + : sortMatch.Groups[1].Value == "M" ? names.OrderBy(n => n.HumanName.Middle) + : sortMatch.Groups[1].Value == "L" ? names.OrderBy(n => n.HumanName.Last) + : names + : names; + } + + /// <summary> Sort must have exactly one of the characters F, M, or L </summary> + [GeneratedRegex(@"[Ss]ort\(\s*?([FML])\s*?\)")] + private static partial Regex SortRegex(); + /// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, {S}, or {ID} </summary> + [GeneratedRegex(@"[Ff]ormat\((.*?(?:{[TFMLS]}|{ID})+.*?)\)")] + public static partial Regex FormatRegex(); +} diff --git a/Source/LibationFileManager/Templates/SeriesDto.cs b/Source/LibationFileManager/Templates/SeriesDto.cs new file mode 100644 index 00000000..834c8db4 --- /dev/null +++ b/Source/LibationFileManager/Templates/SeriesDto.cs @@ -0,0 +1,27 @@ +using System; + +#nullable enable +namespace LibationFileManager.Templates; + +public record SeriesDto : IFormattable +{ + public string Name { get; } + + public float? Number { get; } + public string AudibleSeriesId { get; } + public SeriesDto(string name, float? number, string audibleSeriesId) + { + Name = name; + Number = number; + AudibleSeriesId = audibleSeriesId; + } + + public override string ToString() => Name.Trim(); + public string ToString(string? format, IFormatProvider? _) + => string.IsNullOrWhiteSpace(format) ? ToString() + : format + .Replace("{N}", Name) + .Replace("{#}", Number?.ToString()) + .Replace("{ID}", AudibleSeriesId) + .Trim(); +} diff --git a/Source/LibationFileManager/Templates/SeriesListFormat.cs b/Source/LibationFileManager/Templates/SeriesListFormat.cs new file mode 100644 index 00000000..1127eaa5 --- /dev/null +++ b/Source/LibationFileManager/Templates/SeriesListFormat.cs @@ -0,0 +1,17 @@ +using FileManager.NamingTemplate; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +#nullable enable +namespace LibationFileManager.Templates; + +internal partial class SeriesListFormat : IListFormat<SeriesListFormat> +{ + public static string Formatter(ITemplateTag _, IEnumerable<SeriesDto>? series, string formatString) + => series is null ? string.Empty + : IListFormat<SeriesListFormat>.Join(formatString, series); + + /// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary> + [GeneratedRegex(@"[Ff]ormat\((.*?(?:{[N#]}|{ID})+.*?)\)")] + public static partial Regex FormatRegex(); +} diff --git a/Source/LibationFileManager/TemplateEditor[T].cs b/Source/LibationFileManager/Templates/TemplateEditor[T].cs similarity index 93% rename from Source/LibationFileManager/TemplateEditor[T].cs rename to Source/LibationFileManager/Templates/TemplateEditor[T].cs index c30b6e59..e718b7f4 100644 --- a/Source/LibationFileManager/TemplateEditor[T].cs +++ b/Source/LibationFileManager/Templates/TemplateEditor[T].cs @@ -1,11 +1,10 @@ using AaxDecrypter; using FileManager; -using System.Collections.Generic; using System; using System.IO; #nullable enable -namespace LibationFileManager +namespace LibationFileManager.Templates { public interface ITemplateEditor { @@ -61,16 +60,15 @@ namespace LibationFileManager AccountNickname = "my account", DateAdded = new DateTime(2022, 6, 9, 0, 0, 0), DatePublished = new DateTime(2017, 2, 27, 0, 0, 0), - AudibleProductId = "123456789", + AudibleProductId = "B06WLMWF2S", Title = "A Study in Scarlet", TitleWithSubtitle = "A Study in Scarlet: A Sherlock Holmes Novel", Subtitle = "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, + Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")], + Narrators = [new("Stephen Fry", null)], + Series = [new("Sherlock Holmes", 1, "B08376S3R2"), new("Some Other Series", 1, "B000000000")], BitRate = 128, SampleRate = 44100, Channels = 2, @@ -131,7 +129,7 @@ namespace LibationFileManager if (!templateEditor.IsFolder && !templateEditor.IsFilePath) throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates"); - + if (templateEditor.IsFolder) templateEditor.File = Templates.File; else diff --git a/Source/LibationFileManager/TemplateTags.cs b/Source/LibationFileManager/Templates/TemplateTags.cs similarity index 92% rename from Source/LibationFileManager/TemplateTags.cs rename to Source/LibationFileManager/Templates/TemplateTags.cs index c4a74aba..d5bead6d 100644 --- a/Source/LibationFileManager/TemplateTags.cs +++ b/Source/LibationFileManager/Templates/TemplateTags.cs @@ -1,7 +1,7 @@ using FileManager.NamingTemplate; #nullable enable -namespace LibationFileManager +namespace LibationFileManager.Templates { public sealed class TemplateTags : ITemplateTag { @@ -33,15 +33,15 @@ namespace LibationFileManager public static TemplateTags FirstAuthor { get; } = new TemplateTags("first author", "First author"); public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)"); public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator"); - public static TemplateTags Series { get; } = new TemplateTags("series", "Name of series"); - // can't also have a leading zeros version. Too many weird edge cases. Eg: "1-4" - public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series"); + public static TemplateTags Series { get; } = new TemplateTags("series", "All series to which the book belongs (if any)"); + public static TemplateTags FirstSeries { get; } = new TemplateTags("first series", "First series"); + public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series (alias for <first series[{#}]>"); public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "File's orig. bitrate"); 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 Account { get; } = new TemplateTags("account", "Audible account of this book"); public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname 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 Language { get; } = new("language", "Book's language"); public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG"); diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates/Templates.cs similarity index 91% rename from Source/LibationFileManager/Templates.cs rename to Source/LibationFileManager/Templates/Templates.cs index edcbbedf..c054459d 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates/Templates.cs @@ -10,7 +10,7 @@ using FileManager.NamingTemplate; using NameParser; #nullable enable -namespace LibationFileManager +namespace LibationFileManager.Templates { public interface ITemplate { @@ -58,19 +58,19 @@ namespace LibationFileManager { Configuration.Instance.PropertyChanged += [PropertyChangeFilter(nameof(Configuration.FolderTemplate))] - (_,e) => _folder = GetTemplate<FolderTemplate>(e.NewValue as string); + (_, e) => _folder = GetTemplate<FolderTemplate>(e.NewValue as string); Configuration.Instance.PropertyChanged += [PropertyChangeFilter(nameof(Configuration.FileTemplate))] - (_, e) => _file = GetTemplate<FileTemplate>(e.NewValue as string); + (_, e) => _file = GetTemplate<FileTemplate>(e.NewValue as string); Configuration.Instance.PropertyChanged += [PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))] - (_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>(e.NewValue as string); + (_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>(e.NewValue as string); Configuration.Instance.PropertyChanged += [PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))] - (_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>(e.NewValue as string); + (_, e) => _chapterTitle = GetTemplate<ChapterTitleTemplate>(e.NewValue as string); HumanName.Suffixes.Add("ret"); HumanName.Titles.Add("professor"); @@ -121,7 +121,7 @@ namespace LibationFileManager ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension)); replacements ??= Configuration.Instance.ReplacementCharacters; - return GetFilename(baseDir, fileExtension,replacements, returnFirstExisting, libraryBookDto); + return GetFilename(baseDir, fileExtension, replacements, returnFirstExisting, libraryBookDto); } public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false) @@ -154,7 +154,7 @@ namespace LibationFileManager //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. - var maxFilenameLength = LongPath.MaxFilenameLength - + var maxFilenameLength = LongPath.MaxFilenameLength - (i < pathParts.Count - 1 || string.IsNullOrEmpty(fileExtension) ? 0 : fileExtension.Length + 5); while (part.Sum(LongPath.GetFilesystemStringLength) > maxFilenameLength) @@ -170,7 +170,7 @@ namespace LibationFileManager var fullPath = Path.Combine(pathParts.Select(fileParts => string.Concat(fileParts)).Prepend(baseDir).ToArray()); - return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting); + return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting); } /// <summary> @@ -186,7 +186,7 @@ namespace LibationFileManager foreach (var part in templateParts) { int slashIndex, lastIndex = 0; - while((slashIndex = part.IndexOf(Path.DirectorySeparatorChar, lastIndex)) > -1) + while ((slashIndex = part.IndexOf(Path.DirectorySeparatorChar, lastIndex)) > -1) { dir.Add(part[lastIndex..slashIndex]); RemoveSpaces(dir); @@ -229,7 +229,7 @@ namespace LibationFileManager { original = parts[i]; parts[i] = original.Replace(" ", " "); - }while(original.Length != parts[i].Length); + } while (original.Length != parts[i].Length); } //Remove instances of double spaces at part boundaries @@ -262,11 +262,12 @@ namespace LibationFileManager { TemplateTags.AudibleTitle, lb => lb.Title }, { TemplateTags.AudibleSubtitle, lb => lb.Subtitle }, { TemplateTags.Author, lb => lb.Authors, NameListFormat.Formatter }, - { TemplateTags.FirstAuthor, lb => lb.FirstAuthor }, + { TemplateTags.FirstAuthor, lb => lb.FirstAuthor, FormattableFormatter }, { TemplateTags.Narrator, lb => lb.Narrators, NameListFormat.Formatter }, - { TemplateTags.FirstNarrator, lb => lb.FirstNarrator }, - { TemplateTags.Series, lb => lb.SeriesName }, - { TemplateTags.SeriesNumber, lb => lb.IsPodcastParent ? null : lb.SeriesNumber }, + { TemplateTags.FirstNarrator, lb => lb.FirstNarrator, FormattableFormatter }, + { TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter }, + { TemplateTags.FirstSeries, lb => lb.FirstSeries, FormattableFormatter }, + { TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Number }, { TemplateTags.Language, lb => lb.Language }, //Don't allow formatting of LanguageShort { TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort }, @@ -280,7 +281,7 @@ namespace LibationFileManager { TemplateTags.DatePublished, lb => lb.DatePublished }, { TemplateTags.DateAdded, lb => lb.DateAdded }, { TemplateTags.FileDate, lb => lb.FileDate }, - }; + }; private static readonly List<TagCollection> chapterPropertyTags = new() { @@ -290,7 +291,8 @@ namespace LibationFileManager { TemplateTags.TitleShort, lb => getTitleShort(lb.Title) }, { TemplateTags.AudibleTitle, lb => lb.Title }, { TemplateTags.AudibleSubtitle, lb => lb.Subtitle }, - { TemplateTags.Series, lb => lb.SeriesName }, + { TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter }, + { TemplateTags.FirstSeries, lb => lb.FirstSeries, FormattableFormatter }, }, new PropertyTagCollection<MultiConvertFileProperties>(caseSensative: true, StringFormatter, IntegerFormatter, DateTimeFormatter) { @@ -332,6 +334,9 @@ namespace LibationFileManager return language[..3].ToUpper(); } + private static string FormattableFormatter(ITemplateTag templateTag, IFormattable? value, string formatString) + => value?.ToString(formatString, null) ?? ""; + private static string StringFormatter(ITemplateTag templateTag, string value, string formatString) { if (value is null) return ""; @@ -368,7 +373,7 @@ namespace LibationFileManager public class FolderTemplate : Templates, ITemplate { - public static string Name { get; }= "Folder Template"; + public static string Name { get; } = "Folder Template"; public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? ""; public static string DefaultTemplate { get; } = "<title short> [<id>]"; public static IEnumerable<TagCollection> TagCollections diff --git a/Source/LibationUiBase/GridView/GridContextMenu.cs b/Source/LibationUiBase/GridView/GridContextMenu.cs index c35f0403..1925ca3d 100644 --- a/Source/LibationUiBase/GridView/GridContextMenu.cs +++ b/Source/LibationUiBase/GridView/GridContextMenu.cs @@ -2,6 +2,7 @@ using DataLayer; using FileLiberator; using LibationFileManager; +using LibationFileManager.Templates; using System; using System.Linq; using System.Threading.Tasks; @@ -25,7 +26,7 @@ public class GridContextMenu public string FolderTemplateText => "Folder Template"; public string FileTemplateText => "File Template"; public string MultipartTemplateText => "Multipart File Template"; - public string ViewBookmarksText => "View _Bookmarks/Clips"; + public string ViewBookmarksText => $"View {Accelerator}Bookmarks/Clips"; public string ViewSeriesText => GridEntries[0].Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series"; public bool LiberateEpisodesEnabled => GridEntries.OfType<ISeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)); diff --git a/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs b/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs index 93cec08c..e768a175 100644 --- a/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs +++ b/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs @@ -4,6 +4,7 @@ using System.IO; using System.Windows.Forms; using Dinah.Core; using LibationFileManager; +using LibationFileManager.Templates; namespace LibationWinForms.Dialogs { diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs index 478187ea..7dcd6d92 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs @@ -2,6 +2,7 @@ using LibationFileManager; using System.Linq; using LibationUiBase; +using LibationFileManager.Templates; namespace LibationWinForms.Dialogs { diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs index 49894a7c..9c88ce35 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.DownloadDecrypt.cs @@ -2,6 +2,7 @@ using System.Linq; using Dinah.Core; using LibationFileManager; +using LibationFileManager.Templates; namespace LibationWinForms.Dialogs { diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.cs index 81ba4d0c..555e651f 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Windows.Forms; using LibationFileManager; +using LibationFileManager.Templates; namespace LibationWinForms.Dialogs { diff --git a/Source/LibationWinForms/GridView/ProductsDisplay.cs b/Source/LibationWinForms/GridView/ProductsDisplay.cs index ee8c0176..37f16e24 100644 --- a/Source/LibationWinForms/GridView/ProductsDisplay.cs +++ b/Source/LibationWinForms/GridView/ProductsDisplay.cs @@ -3,6 +3,7 @@ using AudibleUtilities; using DataLayer; using FileLiberator; using LibationFileManager; +using LibationFileManager.Templates; using LibationUiBase.GridView; using LibationWinForms.Dialogs; using LibationWinForms.SeriesView; @@ -258,7 +259,7 @@ namespace LibationWinForms.GridView #region Edit Templates (Single book only) void editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate) - where T : Templates, LibationFileManager.ITemplate, new() + where T : Templates, ITemplate, new() { var template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate); var form = new EditTemplateDialog(template); @@ -280,8 +281,8 @@ namespace LibationWinForms.GridView var editTemplatesMenuItem = new ToolStripMenuItem { Text = ctx.EditTemplatesText }; editTemplatesMenuItem.DropDownItems.AddRange(new[] { folderTemplateMenuItem, fileTemplateMenuItem, multiFileTemplateMenuItem }); - ctxMenu.Items.Add(new ToolStripSeparator()); ctxMenu.Items.Add(editTemplatesMenuItem); + ctxMenu.Items.Add(new ToolStripSeparator()); } #endregion diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs index 8ca42e23..dc68bad6 100644 --- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -6,7 +6,7 @@ using Dinah.Core; using FileManager; using FileManager.NamingTemplate; using FluentAssertions; -using LibationFileManager; +using LibationFileManager.Templates; using Microsoft.VisualStudio.TestTools.UnitTesting; using static TemplatesTests.Shared; @@ -23,7 +23,10 @@ namespace TemplatesTests public static class Shared { - public static LibraryBookDto GetLibraryBook(string seriesName = "Sherlock Holmes") + public static LibraryBookDto GetLibraryBook() + => GetLibraryBook([new SeriesDto("Sherlock Holmes", 1, "B08376S3R2")]); + + public static LibraryBookDto GetLibraryBook(IEnumerable<SeriesDto> series) => new() { Account = "myaccount@example.co", @@ -35,10 +38,9 @@ namespace TemplatesTests 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, + Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")], + Narrators = [new("Stephen Fry", "B000APAGVS"), new("Some Narrator", "B000000000")], + Series = series, BitRate = 128, SampleRate = 44100, Channels = 2, @@ -253,7 +255,6 @@ namespace TemplatesTests } } - [TestMethod] [DataRow("<filedate[yy-MM-dd]> <date added[yy-MM-dd]> <pubdate[yy-MM]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28.m4b")] public void DateFormat_null(string template, string dirFullPath, string extension, string expected) @@ -308,7 +309,7 @@ namespace TemplatesTests public void NameFormat_unusual(string author, string expected) { var bookDto = GetLibraryBook(); - bookDto.Authors = new List<string> { author }; + 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) @@ -329,6 +330,11 @@ namespace TemplatesTests [DataRow("<author[max(2)]>", "Jill Conner Browne, Charles E. Gannon")] [DataRow("<author[max(3)]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf")] [DataRow("<author[format({L}, {F})]>", "Browne, Jill, Gannon, Charles, Fetherolf, Christopher, Montgomery, Lucy, Bon Jovi, Jon, Van Doren, Paul")] + [DataRow("<author[format({L}, {F} {ID})]>", "Browne, Jill B1, Gannon, Charles B2, Fetherolf, Christopher B3, Montgomery, Lucy B4, Bon Jovi, Jon B5, Van Doren, Paul B6")] + [DataRow("<author[format({ID})]>", "B1, B2, B3, B4, B5, B6")] + [DataRow("<author[format({Id})]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")] + [DataRow("<author[format({iD})]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")] + [DataRow("<author[format({id})]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")] [DataRow("<author[format({f}, {l})]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")] [DataRow("<author[format(First={F}, Last={L})]>", "First=Jill, Last=Browne, First=Charles, Last=Gannon, First=Christopher, Last=Fetherolf, First=Lucy, Last=Montgomery, First=Jon, Last=Bon Jovi, First=Paul, Last=Van Doren")] [DataRow("<author[format({L}, {F}) separator( - ) max(3)]>", "Browne, Jill - Gannon, Charles - Fetherolf, Christopher")] @@ -337,18 +343,21 @@ namespace TemplatesTests //Jon Bon Jovi and Paul Van Doren don't have middle names, so they are sorted to the top. //Since only the middle names of the first 2 names are to be displayed, the name string is empty. [DataRow("<author[sort(M) max(2) separator(; ) format({M})]>", ";")] + [DataRow("<first author>", "Jill Conner Browne")] + [DataRow("<first author[]>", "Jill Conner Browne")] + [DataRow("<first author[{L}, {F}]>", "Browne, Jill")] public void NameFormat_formatters(string template, string expected) { var bookDto = GetLibraryBook(); - bookDto.Authors = new List<string> - { - "Jill Conner Browne", - "Charles E. Gannon", - "Christopher John Fetherolf", - "Lucy Maud Montgomery", - "Jon Bon Jovi", - "Paul Van Doren" - }; + bookDto.Authors = + [ + new("Jill Conner Browne", "B1"), + new("Charles E. Gannon", "B2"), + new("Christopher John Fetherolf", "B3"), + new("Lucy Maud Montgomery", "B4"), + new("Jon Bon Jovi", "B5"), + new("Paul Van Doren", "B6") + ]; Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue(); fileTemplate @@ -358,6 +367,35 @@ namespace TemplatesTests } + [TestMethod] + [DataRow("<series>", "Series A, Series B, Series C")] + [DataRow("<series[]>", "Series A, Series B, Series C")] + [DataRow("<series[max(1)]>", "Series A")] + [DataRow("<series[max(2)]>", "Series A, Series B")] + [DataRow("<series[max(3)]>", "Series A, Series B, Series C")] + [DataRow("<series[format({N}, {#}, {ID}) separator(; )]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3")] + [DataRow("<series[format({N}, {#}, {ID}) separator(; ) max(3)]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3")] + [DataRow("<series[format({N}, {#}, {ID}) separator(; ) max(2)]>", "Series A, 1, B1; Series B, 6, B2")] + [DataRow("<first series>", "Series A")] + [DataRow("<first series[]>", "Series A")] + [DataRow("<first series[{N}, {#}, {ID}]>", "Series A, 1, B1")] + public void SeriesFormat_formatters(string template, string expected) + { + var bookDto = GetLibraryBook(); + bookDto.Series = + [ + new("Series A", 1, "B1"), + new("Series B", 6, "B2"), + new("Series C", 2, "B3") + ]; + + Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue(); + fileTemplate + .GetFilename(bookDto, "", "", Replacements) + .PathWithoutPrefix + .Should().Be(expected); + } + [TestMethod] [DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)] [DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]