Increase tag template options for contributor and series types

- Add template tag support for multiple series
- Add series ID and contributor ID to template tags
- <first author> and <first narrator> are now name types with name formatter support
- Properly import contributor IDs into database
- Updated docs
This commit is contained in:
Michael Bucari-Tovo 2025-03-24 15:56:32 -06:00
parent 0a9e489f48
commit 7d806e0f3e
31 changed files with 425 additions and 255 deletions

View File

@ -17,6 +17,9 @@ 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)
- [Series Formatters](#series-formatters)
- [Series List Formatters](#series-list-formatters)
- [Name Formatters](#name-formatters)
- [Name List Formatters](#name-list-formatters) - [Name List Formatters](#name-list-formatters)
- [Number Formatters](#number-formatters) - [Number Formatters](#number-formatters)
- [Date Formatters](#date-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| |Tag|Description|Type|
|-|-|-| |-|-|-|
|\<id\> **†**|Audible book ID (ASIN)|Text| |\<id\> **†**|Audible book ID (ASIN)|Text|
|\<title\>|Full title with subtitle|Text| |\<title\>|Full title with subtitle|[Text](#text-formatters)|
|\<title short\>|Title. Stop at first colon|Text| |\<title short\>|Title. Stop at first colon|[Text](#text-formatters)|
|\<audible title\>|Audible's title (does not include subtitle)|Text| |\<audible title\>|Audible's title (does not include subtitle)|[Text](#text-formatters)|
|\<audible subtitle\>|Audible's subtitle|Text| |\<audible subtitle\>|Audible's subtitle|[Text](#text-formatters)|
|\<author\>|Author(s)|Name List| |\<author\>|Author(s)|[Name List](#name-list-formatters)|
|\<first author\>|First author|Text| |\<first author\>|First author|[Name](#name-formatters)|
|\<narrator\>|Narrator(s)|Name List| |\<narrator\>|Narrator(s)|[Name List](#name-list-formatters)|
|\<first narrator\>|First narrator|Text| |\<first narrator\>|First narrator|[Name](#name-formatters)|
|\<series\>|Name of series|Text| |\<series\>|All series to which the book belongs (if any)|[Series List](#series-list-formatters)|
|\<series#\>|Number order in series|Number| |\<first series\>|First series|[Series](#series-formatters)|
|\<bitrate\>|File's original bitrate (Kbps)|Number| |\<series#\>|Number order in series (alias for \<first series[{#}]\>|[Number](#number-formatters)|
|\<samplerate\>|File's original audio sample rate|Number| |\<bitrate\>|File's original bitrate (Kbps)|[Number](#number-formatters)|
|\<channels\>|Number of audio channels|Number| |\<samplerate\>|File's original audio sample rate|[Number](#number-formatters)|
|\<account\>|Audible account of this book|Text| |\<channels\>|Number of audio channels|[Number](#number-formatters)|
|\<account nickname\>|Audible account nickname of this book|Text| |\<account\>|Audible account of this book|[Text](#text-formatters)|
|\<locale\>|Region/country|Text| |\<account nickname\>|Audible account nickname of this book|[Text](#text-formatters)|
|\<year\>|Year published|Number| |\<locale\>|Region/country|[Text](#text-formatters)|
|\<language\>|Book's language|Text| |\<year\>|Year published|[Number](#number-formatters)|
|\<language\>|Book's language|[Text](#text-formatters)|
|\<language short\> **†**|Book's language abbreviated. Eg: ENG|Text| |\<language short\> **†**|Book's language abbreviated. Eg: ENG|Text|
|\<file date\>|File creation date/time.|DateTime| |\<file date\>|File creation date/time.|[DateTime](#date-formatters)|
|\<pub date\>|Audiobook publication date|DateTime| |\<pub date\>|Audiobook publication date|[DateTime](#date-formatters)|
|\<date added\>|Date the book added to your Audible account|DateTime| |\<date added\>|Date the book added to your Audible account|[DateTime](#date-formatters)|
|\<ch count\> **‡**|Number of chapters|Number| |\<ch count\> **‡**|Number of chapters|[Number](#number-formatters)|
|\<ch title\> **‡**|Chapter title|Text| |\<ch title\> **‡**|Chapter title|[Text](#text-formatters)|
|\<ch#\> **‡**|Chapter number|Number| |\<ch#\> **‡**|Chapter number|[Number](#number-formatters)|
|\<ch# 0\> **‡**|Chapter number with leading zeros|Number| |\<ch# 0\> **‡**|Chapter number with leading zeros|[Number](#number-formatters)|
**†** Does not support custom formatting **†** 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| |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|
## 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 ## Name List Formatters
|Formatter|Description|Example Usage|Example Result| |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| |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| |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| |max(#)|Only use the first # of names<br><br>Default is all names|`<author[max(1)]>`|Arthur Conan Doyle|

View File

@ -418,7 +418,6 @@ namespace AppScaffolding
public List<string> Filters { get; set; } = new(); public List<string> Filters { get; set; } = new();
} }
public static void migrate_to_v12_0_1(Configuration config) public static void migrate_to_v12_0_1(Configuration config)
{ {
#nullable enable #nullable enable

View File

@ -43,5 +43,7 @@ namespace DataLayer
} }
public override string ToString() => Name; public override string ToString() => Name;
public void SetAudibleContributorId(string audibleContributorId)
=> AudibleContributorId = audibleContributorId;
} }
} }

View File

@ -61,19 +61,19 @@ namespace DtoImporterService
private int upsertPeople(List<Person> people) private int upsertPeople(List<Person> people)
{ {
var hash = people var qtyNew = 0;
// new people only foreach (var person in people)
.Where(p => !Cache.ContainsKey(p.Name))
// remove duplicates by Name. first in wins
.ToDictionarySafe(p => p.Name);
foreach (var kvp in hash)
{ {
var person = kvp.Value; if (!Cache.TryGetValue(person.Name, out var contributor))
addContributor(person.Name, person.Asin); {
contributor = createContributor(person.Name, person.Asin);
qtyNew++;
}
updateContributor(person, contributor);
} }
return hash.Count; return qtyNew;
} }
// only use after loading contributors => local // only use after loading contributors => local
@ -86,16 +86,22 @@ namespace DtoImporterService
.ToHashSet(); .ToHashSet();
foreach (var pub in hash) foreach (var pub in hash)
addContributor(pub); createContributor(pub);
return hash.Count; 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 try
{ {
var newContrib = new Contributor(name); var newContrib = new Contributor(name, id);
var entityEntry = DbContext.Contributors.Add(newContrib); var entityEntry = DbContext.Contributors.Add(newContrib);
var entity = entityEntry.Entity; var entity = entityEntry.Entity;

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using DataLayer; using DataLayer;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
namespace FileLiberator namespace FileLiberator
{ {

View File

@ -158,7 +158,7 @@ namespace FileLiberator
if (success && config.SaveMetadataToFile) 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); 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)); item.SourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(contentLic.ContentMetadata.ChapterInfo));

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using System; using System;
using System.IO; using System.IO;
using ApplicationServices; using ApplicationServices;
using LibationFileManager.Templates;
namespace FileLiberator namespace FileLiberator
{ {
@ -26,8 +27,8 @@ namespace FileLiberator
public string Publisher => LibraryBook.Book.Publisher; public string Publisher => LibraryBook.Book.Publisher;
public string Language => LibraryBook.Book.Language; public string Language => LibraryBook.Book.Language;
public string AudibleProductId => LibraryBookDto.AudibleProductId; public string AudibleProductId => LibraryBookDto.AudibleProductId;
public string SeriesName => LibraryBookDto.SeriesName; public string SeriesName => LibraryBookDto.FirstSeries?.Name;
public float? SeriesNumber => LibraryBookDto.SeriesNumber; public float? SeriesNumber => LibraryBookDto.FirstSeries?.Number;
public NAudio.Lame.LameConfig LameConfig { get; init; } public NAudio.Lame.LameConfig LameConfig { get; init; }
public string UserAgent => AudibleApi.Resources.Download_User_Agent; public string UserAgent => AudibleApi.Resources.Download_User_Agent;
public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio; public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio;

View File

@ -5,8 +5,9 @@ using System.Threading.Tasks;
using AudibleUtilities; using AudibleUtilities;
using DataLayer; using DataLayer;
using Dinah.Core; using Dinah.Core;
using LibationFileManager; using LibationFileManager.Templates;
#nullable enable
namespace FileLiberator namespace FileLiberator
{ {
public static class UtilityExtensions public static class UtilityExtensions
@ -47,12 +48,10 @@ namespace FileLiberator
YearPublished = libraryBook.Book.DatePublished?.Year, YearPublished = libraryBook.Book.DatePublished?.Year,
DatePublished = libraryBook.Book.DatePublished, 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(), Series = getSeries(libraryBook.Book.SeriesLink),
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Index,
IsPodcastParent = libraryBook.Book.IsEpisodeParent(), IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(), IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
@ -62,5 +61,21 @@ namespace FileLiberator
Language = libraryBook.Book.Language 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();
}
} }
} }

View File

@ -2,6 +2,7 @@ using Avalonia.Controls;
using LibationAvalonia.Dialogs; using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Settings; using LibationAvalonia.ViewModels.Settings;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationAvalonia.Controls.Settings namespace LibationAvalonia.Controls.Settings

View File

@ -2,6 +2,7 @@ using Avalonia.Controls;
using LibationAvalonia.Dialogs; using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Settings; using LibationAvalonia.ViewModels.Settings;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationAvalonia.Controls.Settings namespace LibationAvalonia.Controls.Settings

View File

@ -5,6 +5,7 @@ using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using Dinah.Core; using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using ReactiveUI; using ReactiveUI;
using System; using System;
using System.IO; using System.IO;

View File

@ -12,6 +12,7 @@ using LibationAvalonia.Controls;
using LibationAvalonia.Dialogs; using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels; using LibationAvalonia.ViewModels;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using LibationUiBase.GridView; using LibationUiBase.GridView;
using ReactiveUI; using ReactiveUI;
using System; using System;
@ -350,7 +351,7 @@ namespace LibationAvalonia.Views
#region Edit Templates (Single book only) #region Edit Templates (Single book only)
async Task editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate) 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 template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate);
var form = new EditTemplateDialog(template); var form = new EditTemplateDialog(template);

View File

@ -306,41 +306,41 @@ namespace LibationFileManager
[Description("How to format the folders in which files will be saved")] [Description("How to format the folders in which files will be saved")]
public string FolderTemplate public string FolderTemplate
{ {
get => getTemplate<Templates.FolderTemplate>(); get => getTemplate<Templates.Templates.FolderTemplate>();
set => setTemplate<Templates.FolderTemplate>(value); set => setTemplate<Templates.Templates.FolderTemplate>(value);
} }
[Description("How to format the saved pdf and audio files")] [Description("How to format the saved pdf and audio files")]
public string FileTemplate public string FileTemplate
{ {
get => getTemplate<Templates.FileTemplate>(); get => getTemplate<Templates.Templates.FileTemplate>();
set => setTemplate<Templates.FileTemplate>(value); set => setTemplate<Templates.Templates.FileTemplate>(value);
} }
[Description("How to format the saved audio files when split by chapters")] [Description("How to format the saved audio files when split by chapters")]
public string ChapterFileTemplate public string ChapterFileTemplate
{ {
get => getTemplate<Templates.ChapterFileTemplate>(); get => getTemplate<Templates.Templates.ChapterFileTemplate>();
set => setTemplate<Templates.ChapterFileTemplate>(value); set => setTemplate<Templates.Templates.ChapterFileTemplate>(value);
} }
[Description("How to format the file's Title stored in metadata")] [Description("How to format the file's Title stored in metadata")]
public string ChapterTitleTemplate public string ChapterTitleTemplate
{ {
get => getTemplate<Templates.ChapterTitleTemplate>(); get => getTemplate<Templates.Templates.ChapterTitleTemplate>();
set => setTemplate<Templates.ChapterTitleTemplate>(value); set => setTemplate<Templates.Templates.ChapterTitleTemplate>(value);
} }
private string getTemplate<T>([CallerMemberName] string propertyName = "") 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 = "") 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 #endregion
} }

View File

@ -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; }
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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; }
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -1,11 +1,10 @@
using AaxDecrypter; using AaxDecrypter;
using FileManager; using FileManager;
using System.Collections.Generic;
using System; using System;
using System.IO; using System.IO;
#nullable enable #nullable enable
namespace LibationFileManager namespace LibationFileManager.Templates
{ {
public interface ITemplateEditor public interface ITemplateEditor
{ {
@ -61,16 +60,15 @@ namespace LibationFileManager
AccountNickname = "my account", AccountNickname = "my account",
DateAdded = new DateTime(2022, 6, 9, 0, 0, 0), DateAdded = new DateTime(2022, 6, 9, 0, 0, 0),
DatePublished = new DateTime(2017, 2, 27, 0, 0, 0), DatePublished = new DateTime(2017, 2, 27, 0, 0, 0),
AudibleProductId = "123456789", AudibleProductId = "B06WLMWF2S",
Title = "A Study in Scarlet", Title = "A Study in Scarlet",
TitleWithSubtitle = "A Study in Scarlet: A Sherlock Holmes Novel", TitleWithSubtitle = "A Study in Scarlet: A Sherlock Holmes Novel",
Subtitle = "A Sherlock Holmes Novel", Subtitle = "A Sherlock Holmes Novel",
Locale = "us", Locale = "us",
YearPublished = 2017, YearPublished = 2017,
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" }, Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")],
Narrators = new List<string> { "Stephen Fry" }, Narrators = [new("Stephen Fry", null)],
SeriesName = "Sherlock Holmes", Series = [new("Sherlock Holmes", 1, "B08376S3R2"), new("Some Other Series", 1, "B000000000")],
SeriesNumber = 1,
BitRate = 128, BitRate = 128,
SampleRate = 44100, SampleRate = 44100,
Channels = 2, Channels = 2,
@ -131,7 +129,7 @@ namespace LibationFileManager
if (!templateEditor.IsFolder && !templateEditor.IsFilePath) if (!templateEditor.IsFolder && !templateEditor.IsFilePath)
throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates"); throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates");
if (templateEditor.IsFolder) if (templateEditor.IsFolder)
templateEditor.File = Templates.File; templateEditor.File = Templates.File;
else else

View File

@ -1,7 +1,7 @@
using FileManager.NamingTemplate; using FileManager.NamingTemplate;
#nullable enable #nullable enable
namespace LibationFileManager namespace LibationFileManager.Templates
{ {
public sealed class TemplateTags : ITemplateTag 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 FirstAuthor { get; } = new TemplateTags("first author", "First author");
public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)"); public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)");
public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator"); public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator");
public static TemplateTags Series { get; } = new TemplateTags("series", "Name of series"); public static TemplateTags Series { get; } = new TemplateTags("series", "All series to which the book belongs (if any)");
// can't also have a leading zeros version. Too many weird edge cases. Eg: "1-4" public static TemplateTags FirstSeries { get; } = new TemplateTags("first series", "First series");
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in 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 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 SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate");
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels"); public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels");
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book"); public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname 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 YearPublished { get; } = new("year", "Year published");
public static TemplateTags Language { get; } = new("language", "Book's language"); public static TemplateTags Language { get; } = new("language", "Book's language");
public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG"); public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG");

View File

@ -10,7 +10,7 @@ using FileManager.NamingTemplate;
using NameParser; using NameParser;
#nullable enable #nullable enable
namespace LibationFileManager namespace LibationFileManager.Templates
{ {
public interface ITemplate public interface ITemplate
{ {
@ -58,19 +58,19 @@ namespace LibationFileManager
{ {
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.FolderTemplate))] [PropertyChangeFilter(nameof(Configuration.FolderTemplate))]
(_,e) => _folder = GetTemplate<FolderTemplate>(e.NewValue as string); (_, e) => _folder = GetTemplate<FolderTemplate>(e.NewValue as string);
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.FileTemplate))] [PropertyChangeFilter(nameof(Configuration.FileTemplate))]
(_, e) => _file = GetTemplate<FileTemplate>(e.NewValue as string); (_, e) => _file = GetTemplate<FileTemplate>(e.NewValue as string);
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))] [PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))]
(_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>(e.NewValue as string); (_, e) => _chapterFile = GetTemplate<ChapterFileTemplate>(e.NewValue as string);
Configuration.Instance.PropertyChanged += Configuration.Instance.PropertyChanged +=
[PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))] [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.Suffixes.Add("ret");
HumanName.Titles.Add("professor"); HumanName.Titles.Add("professor");
@ -121,7 +121,7 @@ namespace LibationFileManager
ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension)); ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension));
replacements ??= Configuration.Instance.ReplacementCharacters; 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) 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. //If file already exists, GetValidFilename will append " (n)" to the filename.
//This could cause the filename length to exceed MaxFilenameLength, so reduce //This could cause the filename length to exceed MaxFilenameLength, so reduce
//allowable filename length by 5 chars, allowing for up to 99 duplicates. //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); (i < pathParts.Count - 1 || string.IsNullOrEmpty(fileExtension) ? 0 : fileExtension.Length + 5);
while (part.Sum(LongPath.GetFilesystemStringLength) > maxFilenameLength) 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()); 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> /// <summary>
@ -186,7 +186,7 @@ namespace LibationFileManager
foreach (var part in templateParts) foreach (var part in templateParts)
{ {
int slashIndex, lastIndex = 0; 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]); dir.Add(part[lastIndex..slashIndex]);
RemoveSpaces(dir); RemoveSpaces(dir);
@ -229,7 +229,7 @@ namespace LibationFileManager
{ {
original = parts[i]; original = parts[i];
parts[i] = original.Replace(" ", " "); parts[i] = original.Replace(" ", " ");
}while(original.Length != parts[i].Length); } while (original.Length != parts[i].Length);
} }
//Remove instances of double spaces at part boundaries //Remove instances of double spaces at part boundaries
@ -262,11 +262,12 @@ namespace LibationFileManager
{ TemplateTags.AudibleTitle, lb => lb.Title }, { TemplateTags.AudibleTitle, lb => lb.Title },
{ TemplateTags.AudibleSubtitle, lb => lb.Subtitle }, { TemplateTags.AudibleSubtitle, lb => lb.Subtitle },
{ TemplateTags.Author, lb => lb.Authors, NameListFormat.Formatter }, { 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.Narrator, lb => lb.Narrators, NameListFormat.Formatter },
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator }, { TemplateTags.FirstNarrator, lb => lb.FirstNarrator, FormattableFormatter },
{ TemplateTags.Series, lb => lb.SeriesName }, { TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter },
{ TemplateTags.SeriesNumber, lb => lb.IsPodcastParent ? null : lb.SeriesNumber }, { TemplateTags.FirstSeries, lb => lb.FirstSeries, FormattableFormatter },
{ TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Number },
{ TemplateTags.Language, lb => lb.Language }, { TemplateTags.Language, lb => lb.Language },
//Don't allow formatting of LanguageShort //Don't allow formatting of LanguageShort
{ TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort }, { TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort },
@ -280,7 +281,7 @@ namespace LibationFileManager
{ TemplateTags.DatePublished, lb => lb.DatePublished }, { TemplateTags.DatePublished, lb => lb.DatePublished },
{ TemplateTags.DateAdded, lb => lb.DateAdded }, { TemplateTags.DateAdded, lb => lb.DateAdded },
{ TemplateTags.FileDate, lb => lb.FileDate }, { TemplateTags.FileDate, lb => lb.FileDate },
}; };
private static readonly List<TagCollection> chapterPropertyTags = new() private static readonly List<TagCollection> chapterPropertyTags = new()
{ {
@ -290,7 +291,8 @@ namespace LibationFileManager
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) }, { TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
{ TemplateTags.AudibleTitle, lb => lb.Title }, { TemplateTags.AudibleTitle, lb => lb.Title },
{ TemplateTags.AudibleSubtitle, lb => lb.Subtitle }, { 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) new PropertyTagCollection<MultiConvertFileProperties>(caseSensative: true, StringFormatter, IntegerFormatter, DateTimeFormatter)
{ {
@ -332,6 +334,9 @@ namespace LibationFileManager
return language[..3].ToUpper(); 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) private static string StringFormatter(ITemplateTag templateTag, string value, string formatString)
{ {
if (value is null) return ""; if (value is null) return "";
@ -368,7 +373,7 @@ namespace LibationFileManager
public class FolderTemplate : Templates, ITemplate 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 Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? "";
public static string DefaultTemplate { get; } = "<title short> [<id>]"; public static string DefaultTemplate { get; } = "<title short> [<id>]";
public static IEnumerable<TagCollection> TagCollections public static IEnumerable<TagCollection> TagCollections

View File

@ -2,6 +2,7 @@
using DataLayer; using DataLayer;
using FileLiberator; using FileLiberator;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -25,7 +26,7 @@ public class GridContextMenu
public string FolderTemplateText => "Folder Template"; public string FolderTemplateText => "Folder Template";
public string FileTemplateText => "File Template"; public string FileTemplateText => "File Template";
public string MultipartTemplateText => "Multipart 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 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)); public bool LiberateEpisodesEnabled => GridEntries.OfType<ISeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload));

View File

@ -4,6 +4,7 @@ using System.IO;
using System.Windows.Forms; using System.Windows.Forms;
using Dinah.Core; using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -2,6 +2,7 @@
using LibationFileManager; using LibationFileManager;
using System.Linq; using System.Linq;
using LibationUiBase; using LibationUiBase;
using LibationFileManager.Templates;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using Dinah.Core; using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {

View File

@ -3,6 +3,7 @@ using AudibleUtilities;
using DataLayer; using DataLayer;
using FileLiberator; using FileLiberator;
using LibationFileManager; using LibationFileManager;
using LibationFileManager.Templates;
using LibationUiBase.GridView; using LibationUiBase.GridView;
using LibationWinForms.Dialogs; using LibationWinForms.Dialogs;
using LibationWinForms.SeriesView; using LibationWinForms.SeriesView;
@ -258,7 +259,7 @@ namespace LibationWinForms.GridView
#region Edit Templates (Single book only) #region Edit Templates (Single book only)
void editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate) 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 template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate);
var form = new EditTemplateDialog(template); var form = new EditTemplateDialog(template);
@ -280,8 +281,8 @@ namespace LibationWinForms.GridView
var editTemplatesMenuItem = new ToolStripMenuItem { Text = ctx.EditTemplatesText }; var editTemplatesMenuItem = new ToolStripMenuItem { Text = ctx.EditTemplatesText };
editTemplatesMenuItem.DropDownItems.AddRange(new[] { folderTemplateMenuItem, fileTemplateMenuItem, multiFileTemplateMenuItem }); editTemplatesMenuItem.DropDownItems.AddRange(new[] { folderTemplateMenuItem, fileTemplateMenuItem, multiFileTemplateMenuItem });
ctxMenu.Items.Add(new ToolStripSeparator());
ctxMenu.Items.Add(editTemplatesMenuItem); ctxMenu.Items.Add(editTemplatesMenuItem);
ctxMenu.Items.Add(new ToolStripSeparator());
} }
#endregion #endregion

View File

@ -6,7 +6,7 @@ using Dinah.Core;
using FileManager; using FileManager;
using FileManager.NamingTemplate; using FileManager.NamingTemplate;
using FluentAssertions; using FluentAssertions;
using LibationFileManager; using LibationFileManager.Templates;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using static TemplatesTests.Shared; using static TemplatesTests.Shared;
@ -23,7 +23,10 @@ namespace TemplatesTests
public static class Shared 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() => new()
{ {
Account = "myaccount@example.co", Account = "myaccount@example.co",
@ -35,10 +38,9 @@ namespace TemplatesTests
Title = "A Study in Scarlet: A Sherlock Holmes Novel", Title = "A Study in Scarlet: A Sherlock Holmes Novel",
Locale = "us", Locale = "us",
YearPublished = 2017, YearPublished = 2017,
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" }, Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")],
Narrators = new List<string> { "Stephen Fry" }, Narrators = [new("Stephen Fry", "B000APAGVS"), new("Some Narrator", "B000000000")],
SeriesName = seriesName ?? "", Series = series,
SeriesNumber = 1,
BitRate = 128, BitRate = 128,
SampleRate = 44100, SampleRate = 44100,
Channels = 2, Channels = 2,
@ -253,7 +255,6 @@ namespace TemplatesTests
} }
} }
[TestMethod] [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")] [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) 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) public void NameFormat_unusual(string author, string expected)
{ {
var bookDto = GetLibraryBook(); 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(); Templates.TryGetTemplate<Templates.FileTemplate>("<author[format(Title={T}, First={F}, Middle={M} Last={L}, Suffix={S})]>", out var fileTemplate).Should().BeTrue();
fileTemplate fileTemplate
.GetFilename(bookDto, "", "", Replacements) .GetFilename(bookDto, "", "", Replacements)
@ -329,6 +330,11 @@ namespace TemplatesTests
[DataRow("<author[max(2)]>", "Jill Conner Browne, Charles E. Gannon")] [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[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})]>", "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({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(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")] [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. //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. //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("<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) public void NameFormat_formatters(string template, string expected)
{ {
var bookDto = GetLibraryBook(); var bookDto = GetLibraryBook();
bookDto.Authors = new List<string> bookDto.Authors =
{ [
"Jill Conner Browne", new("Jill Conner Browne", "B1"),
"Charles E. Gannon", new("Charles E. Gannon", "B2"),
"Christopher John Fetherolf", new("Christopher John Fetherolf", "B3"),
"Lucy Maud Montgomery", new("Lucy Maud Montgomery", "B4"),
"Jon Bon Jovi", new("Jon Bon Jovi", "B5"),
"Paul Van Doren" new("Paul Van Doren", "B6")
}; ];
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue(); Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
fileTemplate 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] [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)]