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:
parent
0a9e489f48
commit
7d806e0f3e
@ -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|
|
||||
|-|-|-|
|
||||
|\<id\> **†**|Audible book ID (ASIN)|Text|
|
||||
|\<title\>|Full title with subtitle|Text|
|
||||
|\<title short\>|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|
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -43,5 +43,7 @@ namespace DataLayer
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
public void SetAudibleContributorId(string audibleContributorId)
|
||||
=> AudibleContributorId = audibleContributorId;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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++;
|
||||
}
|
||||
|
||||
return hash.Count;
|
||||
updateContributor(person, contributor);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationFileManager.Templates;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
44
Source/LibationFileManager/Templates/ContributorDto.cs
Normal file
44
Source/LibationFileManager/Templates/ContributorDto.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
53
Source/LibationFileManager/Templates/IListFormat[TList].cs
Normal file
53
Source/LibationFileManager/Templates/IListFormat[TList].cs
Normal 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();
|
||||
}
|
||||
43
Source/LibationFileManager/Templates/LibraryBookDto.cs
Normal file
43
Source/LibationFileManager/Templates/LibraryBookDto.cs
Normal 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; }
|
||||
}
|
||||
33
Source/LibationFileManager/Templates/NameListFormat.cs
Normal file
33
Source/LibationFileManager/Templates/NameListFormat.cs
Normal 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();
|
||||
}
|
||||
27
Source/LibationFileManager/Templates/SeriesDto.cs
Normal file
27
Source/LibationFileManager/Templates/SeriesDto.cs
Normal 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();
|
||||
}
|
||||
17
Source/LibationFileManager/Templates/SeriesListFormat.cs
Normal file
17
Source/LibationFileManager/Templates/SeriesListFormat.cs
Normal 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();
|
||||
}
|
||||
@ -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,
|
||||
@ -1,7 +1,7 @@
|
||||
using FileManager.NamingTemplate;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
namespace LibationFileManager.Templates
|
||||
{
|
||||
public sealed class TemplateTags : ITemplateTag
|
||||
{
|
||||
@ -33,9 +33,9 @@ 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");
|
||||
@ -10,7 +10,7 @@ using FileManager.NamingTemplate;
|
||||
using NameParser;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
namespace LibationFileManager.Templates
|
||||
{
|
||||
public interface ITemplate
|
||||
{
|
||||
@ -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 },
|
||||
@ -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 "";
|
||||
@ -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));
|
||||
|
||||
@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using LibationFileManager.Templates;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using LibationFileManager;
|
||||
using System.Linq;
|
||||
using LibationUiBase;
|
||||
using LibationFileManager.Templates;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using LibationFileManager.Templates;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using LibationFileManager;
|
||||
using LibationFileManager.Templates;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user