mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-03-30 12:53:45 -04:00
New tags <minutes>, <tag> and <if abridged->...<-if abridged>
This commit is contained in:
@@ -66,17 +66,20 @@ public static class UtilityExtensions
|
||||
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(),
|
||||
|
||||
IsAbridged = libraryBook.Book.IsAbridged,
|
||||
Series = GetSeries(libraryBook.Book.SeriesLink),
|
||||
IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
|
||||
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
|
||||
|
||||
LengthInMinutes = libraryBook.Book.LengthInMinutes,
|
||||
Language = libraryBook.Book.Language,
|
||||
Codec = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.CodecString,
|
||||
BitRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.BitRate,
|
||||
SampleRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.SampleRate,
|
||||
Channels = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.ChannelCount,
|
||||
LibationVersion = libraryBook.Book.UserDefinedItem.LastDownloadedVersion.ToVersionString(),
|
||||
FileVersion = libraryBook.Book.UserDefinedItem.LastDownloadedFileVersion
|
||||
FileVersion = libraryBook.Book.UserDefinedItem.LastDownloadedFileVersion,
|
||||
Tags = libraryBook.Book.UserDefinedItem.TagsEnumerated.Select(s => new StringDto(s)).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public static partial class CommonFormatters
|
||||
return (templateTag, value, culture) => formatter(templateTag, value, null, culture);
|
||||
}
|
||||
|
||||
public static string? StringFinalizer(ITemplateTag templateTag, string? value, CultureInfo? culture) => value ?? "";
|
||||
public static string? StringFinalizer(ITemplateTag templateTag, string? value, CultureInfo? culture) => value;
|
||||
|
||||
public static TPropertyValue? IdlePreFormatter<TPropertyValue>(ITemplateTag templateTag, TPropertyValue? value, string? formatString, CultureInfo? culture) => value;
|
||||
|
||||
@@ -110,6 +110,41 @@ public static partial class CommonFormatters
|
||||
return new string('0', zeroPad) + strValue;
|
||||
}
|
||||
|
||||
public static string MinutesFormatter(ITemplateTag templateTag, int value, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
if (string.IsNullOrEmpty(formatString))
|
||||
return value.ToString(culture);
|
||||
|
||||
var timeSpan = TimeSpan.FromMinutes(value);
|
||||
var result = formatString;
|
||||
|
||||
// replace all placeholders with formatted values
|
||||
result = RegexMinutesD().Replace(result, m =>
|
||||
{
|
||||
var val = (int)timeSpan.TotalDays;
|
||||
timeSpan = timeSpan.Subtract(TimeSpan.FromDays(val));
|
||||
|
||||
return FloatFormatter(templateTag, val, m.Groups["format"].Value, culture);
|
||||
});
|
||||
result = RegexMinutesH().Replace(result, m =>
|
||||
{
|
||||
var val = (int)timeSpan.TotalHours;
|
||||
timeSpan = timeSpan.Subtract(TimeSpan.FromHours(val));
|
||||
|
||||
return FloatFormatter(templateTag, val, m.Groups["format"].Value, culture);
|
||||
});
|
||||
result = RegexMinutesM().Replace(result, m =>
|
||||
{
|
||||
var val = (int)timeSpan.TotalMinutes;
|
||||
timeSpan = timeSpan.Subtract(TimeSpan.FromMinutes(val));
|
||||
|
||||
return FloatFormatter(templateTag, val, m.Groups["format"].Value, culture);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string DateTimeFormatter(ITemplateTag _, DateTime value, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
culture ??= CultureInfo.InvariantCulture;
|
||||
@@ -122,4 +157,14 @@ public static partial class CommonFormatters
|
||||
{
|
||||
return StringFormatter(templateTag, language?.Trim(), "3u", culture);
|
||||
}
|
||||
|
||||
// Regex to find patterns like {D:3}, {h:4}, {m}
|
||||
[GeneratedRegex(@"\{D(?::(?<format>.*?))?\}", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RegexMinutesD();
|
||||
|
||||
[GeneratedRegex(@"\{H(?::(?<format>.*?))?\}", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RegexMinutesH();
|
||||
|
||||
[GeneratedRegex(@"\{M(?::(?<format>.*?))?\}", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RegexMinutesM();
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using FileManager.NamingTemplate;
|
||||
|
||||
namespace LibationFileManager.Templates;
|
||||
namespace LibationFileManager.Templates;
|
||||
|
||||
public class ContributorDto(string name, string? audibleContributorId) : IFormattable
|
||||
{
|
||||
@@ -23,16 +23,12 @@ public class ContributorDto(string name, string? audibleContributorId) : IFormat
|
||||
{ "ID", dto => dto.AudibleContributorId },
|
||||
};
|
||||
|
||||
public override string ToString()
|
||||
=> ToString("{T} {F} {M} {L} {S}", null);
|
||||
public override string ToString() => ToString("{T} {F} {M} {L} {S}", null);
|
||||
|
||||
public string ToString(string? format, IFormatProvider? provider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
return ToString();
|
||||
|
||||
return CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements);
|
||||
}
|
||||
=> string.IsNullOrWhiteSpace(format)
|
||||
? ToString()
|
||||
: CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements);
|
||||
|
||||
private static string RemoveSuffix(string namesString)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using FileManager.NamingTemplate;
|
||||
|
||||
namespace LibationFileManager.Templates;
|
||||
|
||||
|
||||
@@ -22,10 +22,12 @@ public class BookDto
|
||||
public IEnumerable<SeriesDto>? Series { get; set; }
|
||||
public SeriesDto? FirstSeries => Series?.FirstOrDefault();
|
||||
|
||||
public bool IsAbridged { get; set; }
|
||||
public bool IsSeries => Series is not null;
|
||||
public bool IsPodcastParent { get; set; }
|
||||
public bool IsPodcast { get; set; }
|
||||
|
||||
public int LengthInMinutes { get; set; }
|
||||
public int? BitRate { get; set; }
|
||||
public int? SampleRate { get; set; }
|
||||
public int? Channels { get; set; }
|
||||
@@ -42,4 +44,5 @@ public class LibraryBookDto : BookDto
|
||||
public DateTime? DateAdded { get; set; }
|
||||
public string? Account { get; set; }
|
||||
public string? AccountNickname { get; set; }
|
||||
public IEnumerable<StringDto>? Tags { get; set; }
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ internal partial class NameListFormat : IListFormat<NameListFormat>
|
||||
[GeneratedRegex($@"\G(?<token>{Token})(?<descending>(?-i:(?<=\G\P{{Lu}}+)))?\s*", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex SortTokenizer();
|
||||
|
||||
/// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, {S}, or {ID} </summary>
|
||||
/// <summary> Format must have at least one of the strings {T}, {F}, {M}, {L}, {S}, or {ID} </summary>
|
||||
[GeneratedRegex($@"[Ff]ormat\((?<format>.*?\{{{Token}(?::.*?)?\}}.*?)\)")]
|
||||
public static partial Regex FormatRegex();
|
||||
}
|
||||
|
||||
@@ -18,10 +18,7 @@ public record SeriesDto(string? Name, string? Number, string AudibleSeriesId) :
|
||||
public override string? ToString() => Name?.Trim();
|
||||
|
||||
public string ToString(string? format, IFormatProvider? provider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
return ToString() ?? string.Empty;
|
||||
|
||||
return CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements);
|
||||
}
|
||||
=> string.IsNullOrWhiteSpace(format)
|
||||
? ToString() ?? string.Empty
|
||||
: CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ internal partial class SeriesListFormat : IListFormat<SeriesListFormat>
|
||||
[GeneratedRegex($@"\G(?<token>{Token})(?<descending>(?-i:(?<=\G\P{{Lu}}+)))?\s*", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex SortTokenizer();
|
||||
|
||||
/// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary>
|
||||
/// <summary> Format must have at least one of the strings {N}, {#}, {ID} </summary>
|
||||
[GeneratedRegex($@"[Ff]ormat\((?<format>.*?\{{{Token}(?::.*?)?\}}.*?)\)")]
|
||||
public static partial Regex FormatRegex();
|
||||
}
|
||||
|
||||
20
Source/LibationFileManager/Templates/StringDto.cs
Normal file
20
Source/LibationFileManager/Templates/StringDto.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FileManager.NamingTemplate;
|
||||
|
||||
namespace LibationFileManager.Templates;
|
||||
|
||||
public record StringDto(string Value) : IFormattable
|
||||
{
|
||||
public static readonly Dictionary<string, Func<StringDto, object?>> FormatReplacements = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "S", dto => dto.Value }
|
||||
};
|
||||
|
||||
public override string ToString() => Value;
|
||||
|
||||
public string ToString(string? format, IFormatProvider? provider)
|
||||
=> string.IsNullOrWhiteSpace(format)
|
||||
? ToString()
|
||||
: CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements);
|
||||
}
|
||||
58
Source/LibationFileManager/Templates/StringListFormat.cs
Normal file
58
Source/LibationFileManager/Templates/StringListFormat.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using FileManager.NamingTemplate;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LibationFileManager.Templates;
|
||||
|
||||
internal partial class StringListFormat : IListFormat<StringListFormat>
|
||||
{
|
||||
public static IEnumerable<string> Formatter(ITemplateTag _, IEnumerable<StringDto>? entries, string? formatString, CultureInfo? culture)
|
||||
=> entries is null
|
||||
? []
|
||||
: IListFormat<StringListFormat>.FormattedList(formatString, Sort(entries, formatString, StringDto.FormatReplacements), culture);
|
||||
|
||||
public static string? Finalizer(ITemplateTag _, IEnumerable<string>? entries, CultureInfo? culture)
|
||||
=> IListFormat<StringListFormat>.Join(entries, culture);
|
||||
|
||||
private static IEnumerable<T> Sort<T>(IEnumerable<T> entries, string? formatString, Dictionary<string, Func<T, object?>> formatReplacements)
|
||||
{
|
||||
var pattern = formatString is null ? null : SortRegex().Match(formatString).ResolveValue("pattern");
|
||||
if (pattern is null) return entries;
|
||||
|
||||
IOrderedEnumerable<T>? ordered = null;
|
||||
foreach (Match m in SortTokenizer().Matches(pattern!))
|
||||
{
|
||||
// Dictionary is case-insensitive, no ToUpper needed
|
||||
if (!formatReplacements.TryGetValue(m.Groups["token"].Value, out var selector))
|
||||
continue;
|
||||
|
||||
ordered = m.Groups["descending"].Success
|
||||
? ordered is null
|
||||
// ReSharper disable once PossibleMultipleEnumeration
|
||||
? entries.OrderByDescending(selector)
|
||||
: ordered.ThenByDescending(selector)
|
||||
: ordered is null
|
||||
// ReSharper disable once PossibleMultipleEnumeration
|
||||
? entries.OrderBy(selector)
|
||||
: ordered.ThenBy(selector);
|
||||
}
|
||||
|
||||
return ordered ?? entries;
|
||||
}
|
||||
|
||||
private const string Token = "S";
|
||||
|
||||
/// <summary> Sort must have at least one of the token labels T, F, M, L, S or ID. Use lower case for descending direction and add multiple tokens to sort by multiple fields. Spaces may be used to separate tokens.</summary>
|
||||
[GeneratedRegex($@"[Ss]ort\(\s*(?i:(?<pattern>(?:{Token}\s*?)+))\s*\)")]
|
||||
private static partial Regex SortRegex();
|
||||
|
||||
[GeneratedRegex($@"\G(?<token>{Token})(?<descending>(?-i:(?<=\G\P{{Lu}}+)))?\s*", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex SortTokenizer();
|
||||
|
||||
/// <summary> Format must have the string {S} (optionally with formatting like {S:u})</summary>
|
||||
[GeneratedRegex($@"[Ff]ormat\((?<format>.*?\{{{Token}(?::.*?)?\}}.*?)\)")]
|
||||
public static partial Regex FormatRegex();
|
||||
}
|
||||
@@ -34,6 +34,7 @@ public sealed class TemplateTags : ITemplateTag
|
||||
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 Minutes { get; } = new TemplateTags("minutes", "Length in minutes");
|
||||
public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "Bitrate (kbps) of the last downloaded audiobook");
|
||||
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "Sample rate (Hz) of the last downloaded audiobook");
|
||||
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels in the last downloaded audiobook");
|
||||
@@ -42,6 +43,7 @@ public sealed class TemplateTags : ITemplateTag
|
||||
public static TemplateTags LibationVersion { get; } = new TemplateTags("libation version", "Libation version used during last download of the audiobook");
|
||||
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 Tag { get; } = new TemplateTags("tag", "Tag(s)");
|
||||
public static TemplateTags Locale { get; } = new("locale", "Region/country");
|
||||
public static TemplateTags YearPublished { get; } = new("year", "Year published");
|
||||
public static TemplateTags Language { get; } = new("language", "Book's language");
|
||||
@@ -56,6 +58,7 @@ public sealed class TemplateTags : ITemplateTag
|
||||
public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<if podcast-><-if podcast>", "<if podcast->...<-if podcast>");
|
||||
public static TemplateTags IfPodcastParent { get; } = new TemplateTags("if podcastparent", "Only include if item is a podcast series parent", "<if podcastparent-><-if podcastparent>", "<if podcastparent->...<-if podcastparent>");
|
||||
public static TemplateTags IfBookseries { get; } = new TemplateTags("if bookseries", "Only include if part of a book series", "<if bookseries-><-if bookseries>", "<if bookseries->...<-if bookseries>");
|
||||
public static TemplateTags IfAbridged { get; } = new TemplateTags("if abridged", "Only include if abridged", "<if abridged-><-if abridged>", "<if abridged->...<-if abridged>");
|
||||
public static TemplateTags Has { get; } = new TemplateTags("has", "Only include if PROPERTY has a value (i.e. not null or empty)", "<has -><-has>", "<has PROPERTY->...<-has>");
|
||||
public static TemplateTags Is { get; } = new TemplateTags("is", "Only include if PROPERTY has a value satisfying the check (i.e. string comparison)", "<is -><-is>", "<is PROPERTY->...<-is>");
|
||||
}
|
||||
|
||||
@@ -288,11 +288,13 @@ public abstract class Templates
|
||||
{ TemplateTags.DatePublished, lb => lb.DatePublished },
|
||||
{ TemplateTags.DateAdded, lb => lb.DateAdded },
|
||||
{ TemplateTags.FileDate, lb => lb.FileDate },
|
||||
{ TemplateTags.Tag, lb => lb.Tags, StringListFormat.Formatter, StringListFormat.Finalizer },
|
||||
};
|
||||
|
||||
private static readonly PropertyTagCollection<LibraryBookDto> audioFilePropertyTags =
|
||||
new(caseSensitive: true, CommonFormatters.StringFormatter, CommonFormatters.IntegerFormatter)
|
||||
{
|
||||
{ TemplateTags.Minutes, lb => lb.LengthInMinutes, CommonFormatters.MinutesFormatter },
|
||||
{ TemplateTags.Bitrate, lb => lb.BitRate },
|
||||
{ TemplateTags.SampleRate, lb => lb.SampleRate },
|
||||
{ TemplateTags.Channels, lb => lb.Channels },
|
||||
@@ -324,6 +326,7 @@ public abstract class Templates
|
||||
|
||||
private static readonly ConditionalTagCollection<LibraryBookDto> conditionalTags = new()
|
||||
{
|
||||
{ TemplateTags.IfAbridged, lb => lb.IsAbridged },
|
||||
{ TemplateTags.IfSeries, lb => lb.IsSeries || lb.IsPodcastParent },
|
||||
{ TemplateTags.IfPodcast, lb => lb.IsPodcast || lb.IsPodcastParent },
|
||||
{ TemplateTags.IfBookseries, lb => lb is { IsSeries: true, IsPodcast: false, IsPodcastParent: false } },
|
||||
|
||||
@@ -57,6 +57,9 @@ namespace TemplatesTests
|
||||
Codec = "AAC-LC",
|
||||
FileVersion = null, // explicitly null
|
||||
LibationVersion = "", // explicitly empty string
|
||||
LengthInMinutes = 100,
|
||||
IsAbridged = true,
|
||||
Tags = [new StringDto("Tag1"), new StringDto("Tag2"), new StringDto("Tag3")],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,6 +191,31 @@ namespace TemplatesTests
|
||||
fileTemplate.GetFilename(bookDto, "", "", Replacements).PathWithoutPrefix.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<minutes>", 100, "100")]
|
||||
[DataRow("<minutes[{m}]>", 100, "100")]
|
||||
[DataRow("<minutes[{m:2}]>", 100, "100")]
|
||||
[DataRow("<minutes[{h}-{m}]>", 100, "1-40")]
|
||||
[DataRow("<minutes[{h:2}-{m:2}]>", 100, "01-40")]
|
||||
[DataRow("<minutes[{d}.{h:2}-{m:2}]>", 100, "0.01-40")]
|
||||
[DataRow("<minutes[{d:2}d{h:2}h{m:2}m]>", 100, "00d01h40m")]
|
||||
[DataRow("<minutes[{d} days, {h} hours, {m} minutes]>", 100, "0 days, 1 hours, 40 minutes")]
|
||||
[DataRow("<minutes[{h}-{m}]>", 2000, "33-20")]
|
||||
[DataRow("<minutes[{d:3}-{h:3}-{m:3}]>", 2000, "001-009-020")]
|
||||
[DataRow("<minutes[{d}-{m}]>", 100, "0-100")]
|
||||
[DataRow("<minutes[{d}-{m}]>", 1500, "1-60")]
|
||||
[DataRow("<minutes[{d}-{m}]>", 2000, "1-560")]
|
||||
[DataRow("<minutes[{d}-{m}]>", 2880, "2-0")]
|
||||
[DataRow("<minutes[{d:2}-{m:2}]>", 1500, "01-60")]
|
||||
public void MinutesFormat(string template, int minutes, string expected)
|
||||
{
|
||||
var bookDto = GetLibraryBook();
|
||||
bookDto.LengthInMinutes = minutes;
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate.GetFilename(bookDto, "", "", Replacements).PathWithoutPrefix.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<id> - <filedate[yy-MM-dd]>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 23-01-28.m4b")]
|
||||
[DataRow("<id> - <filedate [ yy-MM-dd ] >", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 23-01-28.m4b")]
|
||||
@@ -492,6 +520,7 @@ namespace TemplatesTests
|
||||
[DataRow("<is author[format({L})separator(:)][=Doyle:Fry]->true<-has>", "true")]
|
||||
[DataRow("<is author[>=3]->true<-has>", "")]
|
||||
[DataRow("<is author[=Sherlock]->true<-has>", "")]
|
||||
[DataRow("<is tag[=Tag1]->true<-has>", "true")]
|
||||
public void HasValue_test(string template, string expected)
|
||||
{
|
||||
var bookDto = GetLibraryBook();
|
||||
@@ -614,6 +643,21 @@ namespace TemplatesTests
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<if abridged->Abridged<-if abridged>", "Abridged", true)]
|
||||
[DataRow("<if abridged->Abridged<-if abridged>", "", false)]
|
||||
public void IfAbridged_test(string template, string expected, bool isAbridged)
|
||||
{
|
||||
var bookDto = GetLibraryBook();
|
||||
bookDto.IsAbridged = isAbridged;
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate
|
||||
.GetName(bookDto, new MultiConvertFileProperties { OutputFileName = string.Empty })
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<audibletitle [u]>", "I", "en-US", "i")]
|
||||
[DataRow("<audibletitle [l]>", "ı", "tr-TR", "I")]
|
||||
@@ -630,6 +674,25 @@ namespace TemplatesTests
|
||||
.GetName(bookDto, new MultiConvertFileProperties { OutputFileName = string.Empty }, culture)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<tag>", "Tag1, Tag2, Tag3")]
|
||||
[DataRow("<tag [separator( - )]>", "Tag1 - Tag2 - Tag3")]
|
||||
[DataRow("<tag [format({S:u})]>", "TAG1, TAG2, TAG3")]
|
||||
[DataRow("<tag [format({S:l})]>", "tag1, tag2, tag3")]
|
||||
[DataRow("<tag [format(Tag: {S})]>", "Tag: Tag1, Tag: Tag2, Tag: Tag3")]
|
||||
[DataRow("<tag [max(1)]>", "Tag1")]
|
||||
[DataRow("<tag [sort(s)]>", "Tag3, Tag2, Tag1")]
|
||||
public void Tag_test(string template, string expected)
|
||||
{
|
||||
var bookDto = Shared.GetLibraryBook();
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
|
||||
fileTemplate
|
||||
.GetName(bookDto, new MultiConvertFileProperties { OutputFileName = string.Empty })
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ These tags will be replaced in the template with the audiobook's values.
|
||||
| \<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) |
|
||||
| \<minutes\> | Duration of the audiobook in minutes | [Minutes](#minutes-formatters) |
|
||||
| \<bitrate\> | Bitrate (kbps) of the last downloaded audiobook | [Number](#number-formatters) |
|
||||
| \<samplerate\> | Sample rate (Hz) of the last downloaded audiobook | [Number](#number-formatters) |
|
||||
| \<channels\> | Number of audio channels in the last downloaded audiobook | [Number](#number-formatters) |
|
||||
@@ -34,6 +35,7 @@ These tags will be replaced in the template with the audiobook's values.
|
||||
| \<libation version\> | Libation version used during last download of the audiobook | [Text](#text-formatters) |
|
||||
| \<account\> | Audible account of this book | [Text](#text-formatters) |
|
||||
| \<account nickname\> | Audible account nickname of this book | [Text](#text-formatters) |
|
||||
| \<tag\> | Tag(s) | [Text List](#text-list-formatters) |
|
||||
| \<locale\> | Region/country | [Text](#text-formatters) |
|
||||
| \<year\> | Year published | [Number](#number-formatters) |
|
||||
| \<language\> | Book's language | [Text](#text-formatters) |
|
||||
@@ -62,6 +64,7 @@ Anything between the opening tag (`<tagname->`) and closing tag (`<-tagname>`) w
|
||||
| \<if podcast-\>...\<-if podcast\> | Only include if part of a podcast | Conditional |
|
||||
| \<if bookseries-\>...\<-if bookseries\> | Only include if part of a book series | Conditional |
|
||||
| \<if podcastparent-\>...\<-if podcastparent\>**†** | Only include if item is a podcast series parent | Conditional |
|
||||
| \<if abridged-\>...\<-if abridged\> | Only include if item is abridged | Conditional |
|
||||
| \<has PROPERTY-\>...\<-has\> | Only include if the PROPERTY has a value (i.e. not null or empty) | Conditional |
|
||||
| \<is PROPERTY[[CHECK](#checks)]-\>...\<-is\> | Only include if the PROPERTY or a single value of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
| \<is PROPERTY[FORMAT][[CHECK](#checks)]-\>...\<-is\> | Only include if the formatted PROPERTY or a single value of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
@@ -117,11 +120,19 @@ Text formatting can change length and case of the text. Use <#>, <#><case> or <c
|
||||
| T | Converts text to title case where uppercase words are preserved | \<title[T]\> | The ABC Murders |
|
||||
| | | \<title[6T]\> | The AB |
|
||||
|
||||
### Text List Formatters
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | -------------------------------------------- |
|
||||
| separator() | Speficy the text used to join<br>multiple entries.<br><br>Default is ", " | `<tag[separator(_)]>` | Tag1_Tag2_Tag3_Tag4_Tag5 |
|
||||
| format(\{S\}) | Formats the entries by placing their values into the specified template.<br>Use {S:[Text_Formatter](#text-formatters)} to place the entry and optionally apply a format. | `<tag[format(Tag={S:l})`<br>`separator(;)]>` | Tag=tag1;Tag=tag2;Tag=tag3;Tag=tag4;Tag=tag5 |
|
||||
| max(#) | Only use the first # of entries | `<tag[max(1)]>` | Tag1 |
|
||||
|
||||
### 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>\{#:[Number_Formatter](#number-formatters)\} = Number order in series, formatted<br>\{ID\} = Audible Series ID<br><br>Default is \{N\} | `<first series>`<hr>`<first series[{N}]>`<hr>`<first series[{N}, {#}, {ID}]>`<hr>`<first series[{N}, {ID}, {#:00.0}]>` | Sherlock Holmes<hr>Sherlock Holmes<hr>Sherlock Holmes, 1-6, B08376S3R2<hr>Sherlock Holmes, B08376S3R2, 01.0-06.0 |
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| \{N \| # \| ID\} | Formats the series using<br>the series part tags.<br>\{N:[Text_Formatter](#text-formatters)\} = Series Name<br>\{#:[Number_Formatter](#number-formatters)\} = Number order in series<br>\{ID:[Text_Formatter](#text-formatters)\} = Audible Series ID<br><br>Formatter parts are optional and introduced by the colon. If specified the string will be used to format the part using the correspoing formatter.<br><br>Default is \{N\} | `<first series>`<hr>`<first series[{N:l}]>`<hr>`<first series[{N}, {#}, {ID}]>`<hr>`<first series[{N:10U}, {ID}, {#:00.0}]>` | Sherlock Holmes<hr>sherlock holmes<hr>Sherlock Holmes, 1-6, B08376S3R2<hr>SHERLOCK H, B08376S3R2, 01.0-06.0 |
|
||||
|
||||
### Series List Formatters
|
||||
|
||||
@@ -147,6 +158,12 @@ Text formatting can change length and case of the text. Use <#>, <#><case> or <c
|
||||
| sort(T \| F \| M \| L \| S \| ID) | Sorts the names by title,<br> first, middle, or last name,<br>suffix or Audible Contributor ID<br><br>These terms define the primary, secondary, tertiary, … sorting order.<br>You may combine multiple terms in sequence to specify multi‑level sorting.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<author[sort(M)]>`<hr>`<author[sort(Fl)]>`<hr><author[sort(L FM ID)]> | Stephen Fry, Arthur Conan Doyle<hr>Stephen King, Stephen Fry<hr>John P. Smith \_B000TTTBBB\_, John P. Smith \_B000TTTCCC\_, John S. Smith \_B000HHHVVV\_ |
|
||||
| max(#) | Only use the first # of names<br><br>Default is all names | `<author[max(1)]>` | Arthur Conan Doyle |
|
||||
|
||||
### Minutes Formatters
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
||||
| {M \| H \| D} | Format the minutes value in terms of minutes, hours and days.<br>{D:[Number_Formatter](#number-formatter) = Number of full days<br>{H:[Number_Formatter](#number-formatter) = Number of full (remaining) hours<br>{M:[Number_Formatter](#number-formatter) = Number of (remaining) minutes<br><br>These tags only work in the order from day to minute.<br><br>Default is {M} | `<minutes[{M:4}minutes]>`<hr>`<minutes[{D:2}d {M:2}m]>`<hr>`<minutes[{D}-{H}-{M}]>`<hr>`<minutes[{M}-{H}-{D}]>` | 03000minutes<hr>02d 120m<hr>2-2-0<hr> 3000-0-0 |
|
||||
|
||||
### Number Formatters
|
||||
|
||||
For more custom formatters and examples, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings).
|
||||
|
||||
Reference in New Issue
Block a user