Add a more general NamingTemplate
This commit is contained in:
parent
867085600c
commit
20474e0b3c
29 changed files with 1689 additions and 1075 deletions
|
|
@ -80,7 +80,7 @@ namespace LibationFileManager
|
|||
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public string Books { get => GetString(); set => SetString(value); }
|
||||
public LongPath Books { get => GetString(); set => SetString(value); }
|
||||
|
||||
// temp/working dir(s) should be outside of dropbox
|
||||
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
|
||||
|
|
@ -223,36 +223,41 @@ namespace LibationFileManager
|
|||
[Description("How to format the folders in which files will be saved")]
|
||||
public string FolderTemplate
|
||||
{
|
||||
get => Templates.Folder.GetValid(GetString(defaultValue: Templates.Folder.DefaultTemplate));
|
||||
set => setTemplate(Templates.Folder, value);
|
||||
get => getTemplate<Templates.FolderTemplate>();
|
||||
set => setTemplate<Templates.FolderTemplate>(value);
|
||||
}
|
||||
|
||||
[Description("How to format the saved pdf and audio files")]
|
||||
public string FileTemplate
|
||||
{
|
||||
get => Templates.File.GetValid(GetString(defaultValue: Templates.File.DefaultTemplate));
|
||||
set => setTemplate(Templates.File, value);
|
||||
get => getTemplate<Templates.FileTemplate>();
|
||||
set => setTemplate<Templates.FileTemplate>(value);
|
||||
}
|
||||
|
||||
[Description("How to format the saved audio files when split by chapters")]
|
||||
public string ChapterFileTemplate
|
||||
{
|
||||
get => Templates.ChapterFile.GetValid(GetString(defaultValue: Templates.ChapterFile.DefaultTemplate));
|
||||
set => setTemplate(Templates.ChapterFile, value);
|
||||
get => getTemplate<Templates.ChapterFileTemplate>();
|
||||
set => setTemplate<Templates.ChapterFileTemplate>(value);
|
||||
}
|
||||
|
||||
[Description("How to format the file's Tile stored in metadata")]
|
||||
public string ChapterTitleTemplate
|
||||
{
|
||||
get => Templates.ChapterTitle.GetValid(GetString(defaultValue: Templates.ChapterTitle.DefaultTemplate));
|
||||
set => setTemplate(Templates.ChapterTitle, value);
|
||||
get => getTemplate<Templates.ChapterTitleTemplate>();
|
||||
set => setTemplate<Templates.ChapterTitleTemplate>(value);
|
||||
}
|
||||
|
||||
private void setTemplate(Templates templ, string newValue, [CallerMemberName] string propertyName = "")
|
||||
private string getTemplate<T>([CallerMemberName] string propertyName = "")
|
||||
where T : Templates, ITemplate, new()
|
||||
{
|
||||
var template = newValue?.Trim();
|
||||
if (templ.IsValid(template))
|
||||
SetString(template, propertyName);
|
||||
return Templates.GetTemplate<T>(GetString(defaultValue: T.DefaultTemplate, propertyName)).TemplateText;
|
||||
}
|
||||
|
||||
private void setTemplate<T>(string newValue, [CallerMemberName] string propertyName = "")
|
||||
where T : Templates, ITemplate, new()
|
||||
{
|
||||
SetString(Templates.GetTemplate<T>(newValue).TemplateText, propertyName);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace LibationFileManager
|
|||
|
||||
public string SeriesName { get; set; }
|
||||
public string SeriesNumber { get; set; }
|
||||
public bool IsPodcast { get; set; }
|
||||
|
||||
public int BitRate { get; set; }
|
||||
public int SampleRate { get; set; }
|
||||
|
|
|
|||
130
Source/LibationFileManager/TemplateEditor[T].cs
Normal file
130
Source/LibationFileManager/TemplateEditor[T].cs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
using AaxDecrypter;
|
||||
using FileManager;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public interface ITemplateEditor
|
||||
{
|
||||
bool IsFolder { get; }
|
||||
bool IsFilePath { get; }
|
||||
LongPath BaseDirectory { get; }
|
||||
string DefaultTemplate { get; }
|
||||
Templates Folder { get; }
|
||||
Templates File { get; }
|
||||
Templates Name { get; }
|
||||
Templates EditingTemplate { get; }
|
||||
void SetTemplateText(string templateText);
|
||||
string GetFolderName();
|
||||
string GetFileName();
|
||||
string GetName();
|
||||
}
|
||||
|
||||
public class TemplateEditor<T> : ITemplateEditor where T : Templates, ITemplate, new()
|
||||
{
|
||||
public bool IsFolder => EditingTemplate is Templates.FolderTemplate;
|
||||
public bool IsFilePath => EditingTemplate is not Templates.ChapterTitleTemplate;
|
||||
public LongPath BaseDirectory { get; private init; }
|
||||
public string DefaultTemplate { get; private init; }
|
||||
public Templates Folder { get; private set; }
|
||||
public Templates File { get; private set; }
|
||||
public Templates Name { get; private set; }
|
||||
public Templates EditingTemplate
|
||||
{
|
||||
get => _editingTemplate;
|
||||
private set => _editingTemplate = !IsFilePath ? Name = value : IsFolder ? Folder = value : File = value;
|
||||
}
|
||||
|
||||
private Templates _editingTemplate;
|
||||
|
||||
public void SetTemplateText(string templateText)
|
||||
{
|
||||
Templates.TryGetTemplate<T>(templateText, out var template);
|
||||
EditingTemplate = template;
|
||||
}
|
||||
|
||||
private static readonly LibraryBookDto libraryBookDto
|
||||
= new()
|
||||
{
|
||||
Account = "my account",
|
||||
DateAdded = new DateTime(2022, 6, 9, 0, 0, 0),
|
||||
DatePublished = new DateTime(2017, 2, 27, 0, 0, 0),
|
||||
AudibleProductId = "123456789",
|
||||
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 = "Sherlock Holmes",
|
||||
SeriesNumber = "1",
|
||||
BitRate = 128,
|
||||
SampleRate = 44100,
|
||||
Channels = 2,
|
||||
Language = "English"
|
||||
};
|
||||
|
||||
private static readonly MultiConvertFileProperties partFileProperties
|
||||
= new()
|
||||
{
|
||||
OutputFileName = "",
|
||||
PartsPosition = 4,
|
||||
PartsTotal = 10,
|
||||
Title = "A Flight for Life"
|
||||
};
|
||||
|
||||
public string GetFolderName()
|
||||
{
|
||||
/*
|
||||
* Path must be rooted for windows to allow long file paths. This is
|
||||
* only necessary for folder templates because they may contain several
|
||||
* subdirectories. Without rooting, we won't be allowed to create a
|
||||
* relative path longer than MAX_PATH.
|
||||
*/
|
||||
var dir = Folder.GetFilename(libraryBookDto, BaseDirectory, "");
|
||||
return Path.GetRelativePath(BaseDirectory, dir);
|
||||
}
|
||||
|
||||
public string GetFileName()
|
||||
=> File.GetFilename(libraryBookDto, partFileProperties, "", "");
|
||||
public string GetName()
|
||||
=> Name.GetName(libraryBookDto, partFileProperties);
|
||||
|
||||
public static ITemplateEditor CreateFilenameEditor(LongPath baseDir, string templateText)
|
||||
{
|
||||
Templates.TryGetTemplate<T>(templateText, out var template);
|
||||
|
||||
var templateEditor = new TemplateEditor<T>
|
||||
{
|
||||
_editingTemplate = template,
|
||||
BaseDirectory = baseDir,
|
||||
DefaultTemplate = T.DefaultTemplate
|
||||
};
|
||||
|
||||
if (!templateEditor.IsFolder && !templateEditor.IsFilePath)
|
||||
throw new InvalidOperationException($"This method is only for File and Folder templates. Use {nameof(CreateNameEditor)} for name templates");
|
||||
|
||||
templateEditor.Folder = templateEditor.IsFolder ? template : Templates.Folder;
|
||||
templateEditor.File = templateEditor.IsFolder ? Templates.File : template;
|
||||
|
||||
return templateEditor;
|
||||
}
|
||||
|
||||
public static ITemplateEditor CreateNameEditor(string templateText)
|
||||
{
|
||||
Templates.TryGetTemplate<T>(templateText, out var nameTemplate);
|
||||
|
||||
var templateEditor = new TemplateEditor<T>
|
||||
{
|
||||
_editingTemplate = nameTemplate,
|
||||
DefaultTemplate = T.DefaultTemplate
|
||||
};
|
||||
|
||||
if (templateEditor.IsFolder || templateEditor.IsFilePath)
|
||||
throw new InvalidOperationException($"This method is only for name templates. Use {nameof(CreateFilenameEditor)} for file templates");
|
||||
|
||||
return templateEditor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +1,27 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using FileManager.NamingTemplate;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public sealed class TemplateTags : Enumeration<TemplateTags>
|
||||
{
|
||||
public string TagName => DisplayName;
|
||||
public string DefaultValue { get; }
|
||||
public sealed class TemplateTags : ITemplateTag
|
||||
{
|
||||
public const string DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||
public string TagName { get; }
|
||||
public string DefaultValue { get; }
|
||||
public string Description { get; }
|
||||
public bool IsChapterOnly { get; }
|
||||
|
||||
private static int value = 0;
|
||||
private TemplateTags(string tagName, string description, bool isChapterOnly = false, string defaultValue = null) : base(value++, tagName)
|
||||
{
|
||||
Description = description;
|
||||
IsChapterOnly = isChapterOnly;
|
||||
DefaultValue = defaultValue ?? $"<{tagName}>";
|
||||
public string Display { get; }
|
||||
|
||||
private TemplateTags(string tagName, string description, string defaultValue = null, string display = null)
|
||||
{
|
||||
TagName = tagName;
|
||||
Description = description;
|
||||
DefaultValue = defaultValue ?? $"<{tagName}>";
|
||||
Display = display ?? $"<{tagName}>";
|
||||
}
|
||||
|
||||
// putting these first is the incredibly lazy way to make them show up first in the EditTemplateDialog
|
||||
public static TemplateTags ChCount { get; } = new TemplateTags("ch count", "Number of chapters", true);
|
||||
public static TemplateTags ChTitle { get; } = new TemplateTags("ch title", "Chapter title", true);
|
||||
public static TemplateTags ChNumber { get; } = new TemplateTags("ch#", "Chapter #", true);
|
||||
public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter # with leading zeros", true);
|
||||
public static TemplateTags ChCount { get; } = new TemplateTags("ch count", "Number of chapters");
|
||||
public static TemplateTags ChTitle { get; } = new TemplateTags("ch title", "Chapter title");
|
||||
public static TemplateTags ChNumber { get; } = new TemplateTags("ch#", "Chapter #");
|
||||
public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter # with leading zeros");
|
||||
|
||||
public static TemplateTags Id { get; } = new TemplateTags("id", "Audible ID");
|
||||
public static TemplateTags Title { get; } = new TemplateTags("title", "Full title");
|
||||
|
|
@ -41,16 +37,15 @@ namespace LibationFileManager
|
|||
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate");
|
||||
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels");
|
||||
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
|
||||
public static TemplateTags Locale { get; } = new("locale", "Region/country");
|
||||
public static TemplateTags Locale { get; } = new ("locale", "Region/country");
|
||||
public static TemplateTags YearPublished { get; } = new("year", "Year published");
|
||||
public static TemplateTags Language { get; } = new("language", "Book's language");
|
||||
public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG");
|
||||
public static TemplateTags Language { get; } = new("language", "Book's language");
|
||||
public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG");
|
||||
|
||||
// Special cases. Aren't mapped to replacements in Templates.cs
|
||||
// Included here for display by EditTemplateDialog
|
||||
public static TemplateTags FileDate { get; } = new TemplateTags("file date [...]", "File date/time. e.g. yyyy-MM-dd HH-mm", false, $"<file date [{Templates.DEFAULT_DATE_FORMAT}]>");
|
||||
public static TemplateTags DatePublished { get; } = new TemplateTags("pub date [...]", "Publication date. e.g. yyyy-MM-dd", false, $"<pub date [{Templates.DEFAULT_DATE_FORMAT}]>");
|
||||
public static TemplateTags DateAdded { get; } = new TemplateTags("date added [...]", "Date added to your Audible account. e.g. yyyy-MM-dd", false, $"<date added [{Templates.DEFAULT_DATE_FORMAT}]>");
|
||||
public static TemplateTags IfSeries { get; } = new TemplateTags("if series->...<-if series", "Only include if part of a series", false, "<if series-><-if series>");
|
||||
public static TemplateTags FileDate { get; } = new TemplateTags("file date", "File date/time. e.g. yyyy-MM-dd HH-mm", $"<file date [{DEFAULT_DATE_FORMAT}]>", "<file date [...]>");
|
||||
public static TemplateTags DatePublished { get; } = new TemplateTags("pub date", "Publication date. e.g. yyyy-MM-dd", $"<pub date [{DEFAULT_DATE_FORMAT}]>", "<pub date [...]>");
|
||||
public static TemplateTags DateAdded { get; } = new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"<date added [{DEFAULT_DATE_FORMAT}]>", "<date added [...]>");
|
||||
public static TemplateTags IfSeries { get; } = new TemplateTags("if series", "Only include if part of a series", "<if series-><-if series>", "<if series->...<-if series>");
|
||||
public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<if podcast-><-if podcast>", "<if podcast->...<-if podcast>");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,115 +2,253 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AaxDecrypter;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using FileManager;
|
||||
using FileManager.NamingTemplate;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public interface ITemplate
|
||||
{
|
||||
static abstract string DefaultTemplate { get; }
|
||||
static abstract IEnumerable<TagClass> TagClass { get; }
|
||||
}
|
||||
|
||||
public abstract class Templates
|
||||
{
|
||||
protected static string[] Valid => Array.Empty<string>();
|
||||
public const string ERROR_NULL_IS_INVALID = "Null template is invalid.";
|
||||
public const string ERROR_FULL_PATH_IS_INVALID = @"No colons or full paths allowed. Eg: should not start with C:\";
|
||||
public const string ERROR_INVALID_FILE_NAME_CHAR = @"Only file name friendly characters allowed. Eg: no colons or slashes";
|
||||
|
||||
public const string WARNING_EMPTY = "Template is empty.";
|
||||
public const string WARNING_WHITE_SPACE = "Template is white space.";
|
||||
public const string WARNING_NO_TAGS = "Should use tags. Eg: <title>";
|
||||
public const string WARNING_HAS_CHAPTER_TAGS = "Chapter tags should only be used in the template used for naming files which are split by chapter. Eg: <ch title>";
|
||||
public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>";
|
||||
|
||||
public static FolderTemplate Folder { get; } = new FolderTemplate();
|
||||
public static FileTemplate File { get; } = new FileTemplate();
|
||||
public static ChapterFileTemplate ChapterFile { get; } = new ChapterFileTemplate();
|
||||
public static ChapterTitleTemplate ChapterTitle { get; } = new ChapterTitleTemplate();
|
||||
//Assign the properties in the static constructor will require all
|
||||
//Templates users to have a valid configuration file. To allow tests
|
||||
//to work without access to Configuration, only load templates on demand.
|
||||
private static FolderTemplate _folder;
|
||||
private static FileTemplate _file;
|
||||
private static ChapterFileTemplate _chapterFile;
|
||||
private static ChapterTitleTemplate _chapterTitle;
|
||||
|
||||
public static FolderTemplate Folder => _folder ??= GetTemplate<FolderTemplate>(Configuration.Instance.FolderTemplate);
|
||||
public static FileTemplate File => _file ??= GetTemplate<FileTemplate>(Configuration.Instance.FileTemplate);
|
||||
public static ChapterFileTemplate ChapterFile => _chapterFile ??= GetTemplate<ChapterFileTemplate>(Configuration.Instance.ChapterFileTemplate);
|
||||
public static ChapterTitleTemplate ChapterTitle => _chapterTitle ??= GetTemplate<ChapterTitleTemplate>(Configuration.Instance.ChapterTitleTemplate);
|
||||
|
||||
#region Template Parsing
|
||||
public static T GetTemplate<T>(string templateText) where T : Templates, ITemplate, new()
|
||||
=> TryGetTemplate<T>(templateText, out var template) ? template : GetDefaultTemplate<T>();
|
||||
|
||||
public static bool TryGetTemplate<T>(string templateText, out T template) where T : Templates, ITemplate, new()
|
||||
{
|
||||
var namingTemplate = NamingTemplate.Parse(templateText, T.TagClass);
|
||||
|
||||
template = new() { Template = namingTemplate };
|
||||
return !namingTemplate.Errors.Any();
|
||||
}
|
||||
|
||||
private static T GetDefaultTemplate<T>() where T : Templates, ITemplate, new()
|
||||
=> new() { Template = NamingTemplate.Parse(T.DefaultTemplate, T.TagClass) };
|
||||
|
||||
static Templates()
|
||||
{
|
||||
Configuration.Instance.PropertyChanged += FolderTemplate_PropertyChanged;
|
||||
Configuration.Instance.PropertyChanged += FileTemplate_PropertyChanged;
|
||||
Configuration.Instance.PropertyChanged += ChapterFileTemplate_PropertyChanged;
|
||||
Configuration.Instance.PropertyChanged += ChapterTitleTemplate_PropertyChanged;
|
||||
}
|
||||
|
||||
[PropertyChangeFilter(nameof(Configuration.FolderTemplate))]
|
||||
private static void FolderTemplate_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||
{
|
||||
_folder = GetTemplate<FolderTemplate>((string)e.NewValue);
|
||||
}
|
||||
[PropertyChangeFilter(nameof(Configuration.FileTemplate))]
|
||||
private static void FileTemplate_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||
{
|
||||
_file = GetTemplate<FileTemplate>((string)e.NewValue);
|
||||
}
|
||||
[PropertyChangeFilter(nameof(Configuration.ChapterFileTemplate))]
|
||||
private static void ChapterFileTemplate_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||
{
|
||||
_chapterFile = GetTemplate<ChapterFileTemplate>((string)e.NewValue);
|
||||
}
|
||||
[PropertyChangeFilter(nameof(Configuration.ChapterTitleTemplate))]
|
||||
private static void ChapterTitleTemplate_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||
{
|
||||
_chapterTitle = GetTemplate<ChapterTitleTemplate>((string)e.NewValue);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Template Properties
|
||||
public IEnumerable<TemplateTags> TagsRegistered => Template.TagsRegistered.Cast<TemplateTags>();
|
||||
public IEnumerable<TemplateTags> TagsInUse => Template.TagsInUse.Cast<TemplateTags>();
|
||||
public abstract string Name { get; }
|
||||
public abstract string Description { get; }
|
||||
public abstract string DefaultTemplate { get; }
|
||||
protected abstract bool IsChapterized { get; }
|
||||
public string TemplateText => Template.TemplateText;
|
||||
protected NamingTemplate Template { get; private set; }
|
||||
|
||||
protected Templates() { }
|
||||
#endregion
|
||||
|
||||
#region validation
|
||||
internal string GetValid(string configValue)
|
||||
{
|
||||
var value = configValue?.Trim();
|
||||
return IsValid(value) ? value : DefaultTemplate;
|
||||
}
|
||||
|
||||
public abstract IEnumerable<string> GetErrors(string template);
|
||||
public bool IsValid(string template) => !GetErrors(template).Any();
|
||||
public virtual IEnumerable<string> Errors => Template.Errors;
|
||||
public bool IsValid => !Errors.Any();
|
||||
|
||||
public abstract IEnumerable<string> GetWarnings(string template);
|
||||
public bool HasWarnings(string template) => GetWarnings(template).Any();
|
||||
public virtual IEnumerable<string> Warnings => Template.Warnings;
|
||||
public bool HasWarnings => Warnings.Any();
|
||||
|
||||
protected static string[] GetFileErrors(string template)
|
||||
{
|
||||
// File name only; not path. all other path chars are valid enough to pass this check and will be handled on final save.
|
||||
|
||||
// null is invalid. whitespace is valid but not recommended
|
||||
if (template is null)
|
||||
return new[] { ERROR_NULL_IS_INVALID };
|
||||
|
||||
if (ReplacementCharacters.ContainsInvalidFilenameChar(template.Replace("<","").Replace(">","")))
|
||||
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
|
||||
|
||||
return Valid;
|
||||
}
|
||||
|
||||
protected IEnumerable<string> GetStandardWarnings(string template)
|
||||
{
|
||||
var warnings = GetErrors(template).ToList();
|
||||
if (template is null)
|
||||
return warnings;
|
||||
|
||||
if (string.IsNullOrEmpty(template))
|
||||
warnings.Add(WARNING_EMPTY);
|
||||
else if (string.IsNullOrWhiteSpace(template))
|
||||
warnings.Add(WARNING_WHITE_SPACE);
|
||||
|
||||
if (TagCount(template) == 0)
|
||||
warnings.Add(WARNING_NO_TAGS);
|
||||
|
||||
if (!IsChapterized && ContainsChapterOnlyTags(template))
|
||||
warnings.Add(WARNING_HAS_CHAPTER_TAGS);
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
internal int TagCount(string template)
|
||||
=> GetTemplateTags()
|
||||
// for <id><id> == 1, use:
|
||||
// .Count(t => template.Contains($"<{t.TagName}>"))
|
||||
// .Sum() impl: <id><id> == 2
|
||||
.Sum(t => template.Split($"<{t.TagName}>").Length - 1);
|
||||
|
||||
internal static bool ContainsChapterOnlyTags(string template)
|
||||
=> TemplateTags.GetAll()
|
||||
.Where(t => t.IsChapterOnly)
|
||||
.Any(t => ContainsTag(template, t.TagName));
|
||||
|
||||
internal static bool ContainsTag(string template, string tag) => template.Contains($"<{tag}>");
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>
|
||||
/// EditTemplateDialog: Get template generated filename for portion of path
|
||||
/// </summary>
|
||||
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, string fileExtension)
|
||||
=> string.IsNullOrWhiteSpace(template)
|
||||
? ""
|
||||
: getFileNamingTemplate(libraryBookDto, template, null, fileExtension, Configuration.Instance.ReplacementCharacters)
|
||||
.GetFilePath(fileExtension).PathWithoutPrefix;
|
||||
|
||||
public const string DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||
private static Regex fileDateTagRegex { get; } = new Regex(@"<file\s*?date\s*?(?:\[([^\[\]]*?)\]\s*?)?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static Regex dateAddedTagRegex { get; } = new Regex(@"<date\s*?added\s*?(?:\[([^\[\]]*?)\]\s*?)?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static Regex datePublishedTagRegex { get; } = new Regex(@"<pub\s*?date\s*?(?:\[([^\[\]]*?)\]\s*?)?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
public string GetName(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps));
|
||||
return string.Join("", Template.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value));
|
||||
}
|
||||
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
ArgumentValidator.EnsureNotNull(baseDir, nameof(baseDir));
|
||||
ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension));
|
||||
|
||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||
return GetFilename(baseDir, fileExtension, returnFirstExisting, replacements, libraryBookDto);
|
||||
}
|
||||
|
||||
public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir = "", string fileExtension = null, ReplacementCharacters replacements = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps));
|
||||
ArgumentValidator.EnsureNotNull(baseDir, nameof(baseDir));
|
||||
|
||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||
fileExtension ??= Path.GetExtension(multiChapProps.OutputFileName);
|
||||
return GetFilename(baseDir, fileExtension, false, replacements, libraryBookDto, multiChapProps);
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
|
||||
=> parts.Select(p => replacements.ReplaceFilenameChars(p.Value));
|
||||
|
||||
private LongPath GetFilename(string baseDir, string fileExtension, bool returnFirstExisting, ReplacementCharacters replacements, params object[] dtos)
|
||||
{
|
||||
var parts = Template.Evaluate(dtos).ToList();
|
||||
|
||||
var pathParts = GetPathParts(GetTemplatePartsStrings(parts, replacements));
|
||||
|
||||
//Remove 1 character from the end of the longest filename part until
|
||||
//the total filename is less than max filename length
|
||||
foreach (var part in pathParts)
|
||||
{
|
||||
while (part.Sum(LongPath.GetFilesystemStringLength) > LongPath.MaxFilenameLength)
|
||||
{
|
||||
int maxLength = part.Max(p => p.Length);
|
||||
var maxEntry = part.First(p => p.Length == maxLength);
|
||||
|
||||
var maxIndex = part.IndexOf(maxEntry);
|
||||
part.RemoveAt(maxIndex);
|
||||
part.Insert(maxIndex, maxEntry.Remove(maxLength - 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
var fullPath = Path.Combine(pathParts.Select(p => string.Join("", p)).Prepend(baseDir).ToArray());
|
||||
|
||||
return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Organize template parts into directories.
|
||||
/// </summary>
|
||||
/// <returns>A List of template directories. Each directory is a list of template part strings</returns>
|
||||
private List<List<string>> GetPathParts(IEnumerable<string> templateParts)
|
||||
{
|
||||
List<List<string>> directories = new();
|
||||
List<string> dir = new();
|
||||
|
||||
foreach (var part in templateParts)
|
||||
{
|
||||
int slashIndex, lastIndex = 0;
|
||||
while((slashIndex = part.IndexOf(Path.DirectorySeparatorChar, lastIndex)) > -1)
|
||||
{
|
||||
dir.Add(part[lastIndex..slashIndex]);
|
||||
directories.Add(dir);
|
||||
dir = new();
|
||||
|
||||
lastIndex = slashIndex + 1;
|
||||
}
|
||||
dir.Add(part[lastIndex..]);
|
||||
}
|
||||
directories.Add(dir);
|
||||
|
||||
return directories;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Registered Template Properties
|
||||
|
||||
private static readonly PropertyTagClass<LibraryBookDto> filePropertyTags = GetFilePropertyTags();
|
||||
private static readonly ConditionalTagClass<LibraryBookDto> conditionalTags = GetConditionalTags();
|
||||
private static readonly List<TagClass> chapterPropertyTags = GetChapterPropertyTags();
|
||||
|
||||
private static ConditionalTagClass<LibraryBookDto> GetConditionalTags()
|
||||
{
|
||||
ConditionalTagClass<LibraryBookDto> lbConditions = new();
|
||||
|
||||
lbConditions.RegisterCondition(TemplateTags.IfSeries, lb => !string.IsNullOrWhiteSpace(lb.SeriesName));
|
||||
lbConditions.RegisterCondition(TemplateTags.IfPodcast, lb => lb.IsPodcast);
|
||||
|
||||
return lbConditions;
|
||||
}
|
||||
|
||||
private static PropertyTagClass<LibraryBookDto> GetFilePropertyTags()
|
||||
{
|
||||
PropertyTagClass<LibraryBookDto> lbProperties = new();
|
||||
lbProperties.RegisterProperty(TemplateTags.Id, lb => lb.AudibleProductId);
|
||||
lbProperties.RegisterProperty(TemplateTags.Title, lb => lb.Title ?? "", StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.TitleShort, lb => lb.Title.IndexOf(':') < 1 ? lb.Title : lb.Title.Substring(0, lb.Title.IndexOf(':')), StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Author, lb => lb.AuthorNames, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.FirstAuthor, lb => lb.FirstAuthor, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Narrator, lb => lb.NarratorNames, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.FirstNarrator, lb => lb.FirstNarrator, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Series, lb => lb.SeriesName ?? "", StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.SeriesNumber, lb => lb.SeriesNumber);
|
||||
lbProperties.RegisterProperty(TemplateTags.Language, lb => lb.Language);
|
||||
lbProperties.RegisterProperty(TemplateTags.LanguageShort, lb => getLanguageShort(lb.Language));
|
||||
lbProperties.RegisterProperty(TemplateTags.Bitrate, lb => lb.BitRate, IntegerFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.SampleRate, lb => lb.SampleRate, IntegerFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Channels, lb => lb.Channels, IntegerFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Account, lb => lb.Account, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.Locale, lb => lb.Locale, StringFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.YearPublished, lb => lb.YearPublished, IntegerFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.DatePublished, lb => lb.DatePublished, DateTimeFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.DateAdded, lb => lb.DateAdded, DateTimeFormatter);
|
||||
lbProperties.RegisterProperty(TemplateTags.FileDate, lb => lb.FileDate, DateTimeFormatter);
|
||||
return lbProperties;
|
||||
}
|
||||
|
||||
private static List<TagClass> GetChapterPropertyTags()
|
||||
{
|
||||
PropertyTagClass<LibraryBookDto> lbProperties = new();
|
||||
PropertyTagClass<MultiConvertFileProperties> multiConvertProperties = new();
|
||||
|
||||
lbProperties.RegisterProperty(TemplateTags.Title, lb => lb.Title ?? "");
|
||||
lbProperties.RegisterProperty(TemplateTags.TitleShort, lb => lb.Title.IndexOf(':') < 1 ? lb.Title : lb.Title.Substring(0, lb.Title.IndexOf(':')));
|
||||
lbProperties.RegisterProperty(TemplateTags.Series, lb => lb.SeriesName ?? "");
|
||||
|
||||
multiConvertProperties.RegisterProperty(TemplateTags.ChCount, lb => lb.PartsTotal, IntegerFormatter);
|
||||
multiConvertProperties.RegisterProperty(TemplateTags.ChNumber, lb => lb.PartsPosition, IntegerFormatter);
|
||||
multiConvertProperties.RegisterProperty(TemplateTags.ChNumber0, m => m.PartsPosition.ToString("D" + ((int)Math.Log10(m.PartsTotal) + 1)));
|
||||
multiConvertProperties.RegisterProperty(TemplateTags.ChTitle, m => m.Title ?? "", StringFormatter);
|
||||
|
||||
return new List<TagClass> { lbProperties, multiConvertProperties };
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tag Formatters
|
||||
|
||||
private static string getLanguageShort(string language)
|
||||
{
|
||||
|
|
@ -123,330 +261,94 @@ namespace LibationFileManager
|
|||
return language[..3].ToUpper();
|
||||
}
|
||||
|
||||
internal static FileNamingTemplate getFileNamingTemplate(LibraryBookDto libraryBookDto, string template, string dirFullPath, string extension, ReplacementCharacters replacements)
|
||||
private static string StringFormatter(ITemplateTag templateTag, string value, string formatString)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
|
||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||
dirFullPath = dirFullPath?.Trim() ?? "";
|
||||
|
||||
// for non-series, remove <if series-> and <-if series> tags and everything in between
|
||||
// for series, remove <if series-> and <-if series> tags, what's in between will remain
|
||||
template = ifSeriesRegex.Replace(
|
||||
template,
|
||||
string.IsNullOrWhiteSpace(libraryBookDto.SeriesName) ? "" : "$1");
|
||||
|
||||
//Get date replacement parameters. Sanitizes the format text and replaces
|
||||
//the template with the sanitized text before creating FileNamingTemplate
|
||||
var fileDateParams = getSanitizeDateReplacementParameters(fileDateTagRegex, ref template, replacements, libraryBookDto.FileDate);
|
||||
var dateAddedParams = getSanitizeDateReplacementParameters(dateAddedTagRegex, ref template, replacements, libraryBookDto.DateAdded);
|
||||
var pubDateParams = getSanitizeDateReplacementParameters(datePublishedTagRegex, ref template, replacements, libraryBookDto.DatePublished);
|
||||
|
||||
var t = template + FileUtility.GetStandardizedExtension(extension);
|
||||
var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t);
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename, replacements);
|
||||
|
||||
var title = libraryBookDto.Title ?? "";
|
||||
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
|
||||
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Id, libraryBookDto.AudibleProductId);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Title, title);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.TitleShort, titleShort);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Author, libraryBookDto.AuthorNames);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.FirstAuthor, libraryBookDto.FirstAuthor);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Narrator, libraryBookDto.NarratorNames);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.FirstNarrator, libraryBookDto.FirstNarrator);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Series, libraryBookDto.SeriesName);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.SeriesNumber, libraryBookDto.SeriesNumber);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Bitrate, libraryBookDto.BitRate);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.SampleRate, libraryBookDto.SampleRate);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Channels, libraryBookDto.Channels);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Account, libraryBookDto.Account);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Locale, libraryBookDto.Locale);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.YearPublished, libraryBookDto.YearPublished?.ToString() ?? "1900");
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Language, libraryBookDto.Language);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.LanguageShort, getLanguageShort(libraryBookDto.Language));
|
||||
|
||||
//Add the sanitized replacement parameters
|
||||
foreach (var param in fileDateParams)
|
||||
fileNamingTemplate.ParameterReplacements.AddIfNotContains(param);
|
||||
foreach (var param in dateAddedParams)
|
||||
fileNamingTemplate.ParameterReplacements.AddIfNotContains(param);
|
||||
foreach (var param in pubDateParams)
|
||||
fileNamingTemplate.ParameterReplacements.AddIfNotContains(param);
|
||||
|
||||
return fileNamingTemplate;
|
||||
if (string.Compare(formatString, "u", ignoreCase: true) == 0) return value?.ToUpper();
|
||||
else if (string.Compare(formatString, "l", ignoreCase: true) == 0) return value?.ToLower();
|
||||
else return value;
|
||||
}
|
||||
|
||||
private static string IntegerFormatter(ITemplateTag templateTag, int value, string formatString)
|
||||
{
|
||||
if (int.TryParse(formatString, out var numDigits))
|
||||
return value.ToString($"D{numDigits}");
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
private static string DateTimeFormatter(ITemplateTag templateTag, DateTime value, string formatString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(formatString))
|
||||
return value.ToString(TemplateTags.DEFAULT_DATE_FORMAT);
|
||||
return value.ToString(formatString);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DateTime Tags
|
||||
|
||||
/// <param name="template">the file naming template. Any found date tags will be sanitized,
|
||||
/// and the template's original date tag will be replaced with the sanitized tag.</param>
|
||||
/// <returns>A list of parameter replacement key-value pairs</returns>
|
||||
private static List<KeyValuePair<string, object>> getSanitizeDateReplacementParameters(Regex datePattern, ref string template, ReplacementCharacters replacements, DateTime? dateTime)
|
||||
{
|
||||
List<KeyValuePair<string, object>> dateParams = new();
|
||||
|
||||
foreach (Match dateTag in datePattern.Matches(template))
|
||||
{
|
||||
var sanitizedTag = sanitizeDateParameterTag(dateTag, replacements, out var sanitizedFormatter);
|
||||
if (tryFormatDateTime(dateTime, sanitizedFormatter, replacements, out var formattedDateString))
|
||||
{
|
||||
dateParams.Add(new(sanitizedTag, formattedDateString));
|
||||
template = template.Replace(dateTag.Value, sanitizedTag);
|
||||
}
|
||||
}
|
||||
return dateParams;
|
||||
}
|
||||
|
||||
/// <returns>a date parameter replacement tag with the format string sanitized</returns>
|
||||
private static string sanitizeDateParameterTag(Match dateTag, ReplacementCharacters replacements, out string sanitizedFormatter)
|
||||
{
|
||||
if (dateTag.Groups.Count != 2 || string.IsNullOrWhiteSpace(dateTag.Groups[1].Value))
|
||||
{
|
||||
sanitizedFormatter = DEFAULT_DATE_FORMAT;
|
||||
return dateTag.Value;
|
||||
}
|
||||
|
||||
var formatter = dateTag.Groups[1].Value;
|
||||
|
||||
sanitizedFormatter = replacements.ReplaceFilenameChars(formatter).Trim();
|
||||
|
||||
return dateTag.Value.Replace(formatter, sanitizedFormatter);
|
||||
}
|
||||
|
||||
private static bool tryFormatDateTime(DateTime? dateTime, string sanitizedFormatter, ReplacementCharacters replacements, out string formattedDateString)
|
||||
{
|
||||
if (!dateTime.HasValue)
|
||||
{
|
||||
formattedDateString = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
formattedDateString = replacements.ReplaceFilenameChars(dateTime.Value.ToString(sanitizedFormatter)).Trim();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
formattedDateString = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public virtual IEnumerable<TemplateTags> GetTemplateTags()
|
||||
=> TemplateTags.GetAll()
|
||||
// yeah, this line is a little funky but it works when you think through it. also: trust the unit tests
|
||||
.Where(t => IsChapterized || !t.IsChapterOnly);
|
||||
|
||||
public string Sanitize(string template, ReplacementCharacters replacements)
|
||||
{
|
||||
var value = template ?? "";
|
||||
|
||||
// Replace invalid filename characters in the DateTime format provider so we don't trip any alarms.
|
||||
// Illegal filename characters in the formatter are allowed because they will be replaced by
|
||||
// getFileNamingTemplate()
|
||||
value = fileDateTagRegex.Replace(value, m => sanitizeDateParameterTag(m, replacements, out _));
|
||||
value = dateAddedTagRegex.Replace(value, m => sanitizeDateParameterTag(m, replacements, out _));
|
||||
value = datePublishedTagRegex.Replace(value, m => sanitizeDateParameterTag(m, replacements, out _));
|
||||
|
||||
// don't use alt slash
|
||||
value = value.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
|
||||
// don't allow double slashes
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
var dbl = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}";
|
||||
while (value.Contains(dbl))
|
||||
value = value.Replace(dbl, sing);
|
||||
|
||||
// trim. don't start or end with slash
|
||||
while (true)
|
||||
{
|
||||
var start = value.Length;
|
||||
value = value
|
||||
.Trim()
|
||||
.Trim(Path.DirectorySeparatorChar);
|
||||
var end = value.Length;
|
||||
if (start == end)
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public class FolderTemplate : Templates
|
||||
public class FolderTemplate : Templates, ITemplate
|
||||
{
|
||||
public override string Name => "Folder Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||
public override string DefaultTemplate { get; } = "<title short> [<id>]";
|
||||
protected override bool IsChapterized { get; } = false;
|
||||
public static string DefaultTemplate { get; } = "<title short> [<id>]";
|
||||
public static IEnumerable<TagClass> TagClass => new TagClass[] { filePropertyTags, conditionalTags };
|
||||
|
||||
internal FolderTemplate() : base() { }
|
||||
public override IEnumerable<string> Errors
|
||||
=> TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors;
|
||||
|
||||
#region validation
|
||||
public override IEnumerable<string> GetErrors(string template)
|
||||
protected override List<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
|
||||
{
|
||||
// null is invalid. whitespace is valid but not recommended
|
||||
if (template is null)
|
||||
return new[] { ERROR_NULL_IS_INVALID };
|
||||
foreach (var tp in parts)
|
||||
{
|
||||
//FolderTemplate literals can have directory separator characters
|
||||
if (tp.TemplateTag is null)
|
||||
tp.Value = replacements.ReplacePathChars(tp.Value.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar));
|
||||
else
|
||||
tp.Value = replacements.ReplaceFilenameChars(tp.Value);
|
||||
}
|
||||
if (parts.Count > 0)
|
||||
{
|
||||
//Remove DirectorySeparatorChar at beginning and end of template
|
||||
if (parts[0].Value.Length > 0 && parts[0].Value[0] == Path.DirectorySeparatorChar)
|
||||
parts[0].Value = parts[0].Value.Remove(0,1);
|
||||
|
||||
// must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save.
|
||||
if (template.Contains(':'))
|
||||
return new[] { ERROR_FULL_PATH_IS_INVALID };
|
||||
|
||||
// must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save.
|
||||
if (ReplacementCharacters.ContainsInvalidPathChar(template.Replace("<", "").Replace(">", "")))
|
||||
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
|
||||
|
||||
return Valid;
|
||||
if (parts[^1].Value.Length > 0 && parts[^1].Value[^1] == Path.DirectorySeparatorChar)
|
||||
parts[^1].Value = parts[^1].Value.Remove(parts[^1].Value.Length - 1, 1);
|
||||
}
|
||||
return parts.Select(p => p.Value).ToList();
|
||||
}
|
||||
|
||||
public override IEnumerable<string> GetWarnings(string template) => GetStandardWarnings(template);
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null)
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null, Configuration.Instance.ReplacementCharacters)
|
||||
.GetFilePath(string.Empty);
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class FileTemplate : Templates
|
||||
public class FileTemplate : Templates, ITemplate
|
||||
{
|
||||
public override string Name => "File Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||
public override string DefaultTemplate { get; } = "<title> [<id>]";
|
||||
protected override bool IsChapterized { get; } = false;
|
||||
|
||||
internal FileTemplate() : base() { }
|
||||
|
||||
#region validation
|
||||
public override IEnumerable<string> GetErrors(string template) => GetFileErrors(template);
|
||||
|
||||
public override IEnumerable<string> GetWarnings(string template) => GetStandardWarnings(template);
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false)
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension, Configuration.Instance.ReplacementCharacters)
|
||||
.GetFilePath(extension, returnFirstExisting);
|
||||
#endregion
|
||||
public static string DefaultTemplate { get; } = "<title> [<id>]";
|
||||
public static IEnumerable<TagClass> TagClass { get; } = new TagClass[] { filePropertyTags, conditionalTags };
|
||||
}
|
||||
|
||||
public class ChapterFileTemplate : Templates
|
||||
public class ChapterFileTemplate : Templates, ITemplate
|
||||
{
|
||||
public override string Name => "Chapter File Template";
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||
public override string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
|
||||
protected override bool IsChapterized { get; } = true;
|
||||
public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
|
||||
public static IEnumerable<TagClass> TagClass { get; }
|
||||
= chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags);
|
||||
|
||||
internal ChapterFileTemplate() : base() { }
|
||||
|
||||
#region validation
|
||||
public override IEnumerable<string> GetErrors(string template) => GetFileErrors(template);
|
||||
|
||||
public override IEnumerable<string> GetWarnings(string template)
|
||||
{
|
||||
var warnings = GetStandardWarnings(template).ToList();
|
||||
if (template is null)
|
||||
return warnings;
|
||||
|
||||
// recommended to incl. <ch#> or <ch# 0>
|
||||
if (!ContainsTag(template, TemplateTags.ChNumber.TagName) && !ContainsTag(template, TemplateTags.ChNumber0.TagName))
|
||||
warnings.Add(WARNING_NO_CHAPTER_NUMBER_TAG);
|
||||
|
||||
return warnings;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props)
|
||||
=> GetPortionFilename(libraryBookDto, Configuration.Instance.ChapterFileTemplate, props, AudibleFileStorage.DecryptInProgressDirectory);
|
||||
|
||||
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath, ReplacementCharacters replacements = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(template)) return string.Empty;
|
||||
|
||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||
var fileExtension = Path.GetExtension(props.OutputFileName);
|
||||
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, fileExtension, replacements);
|
||||
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber, props.PartsPosition);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
|
||||
|
||||
foreach (Match dateTag in fileDateTagRegex.Matches(fileNamingTemplate.Template))
|
||||
{
|
||||
var sanitizedTag = sanitizeDateParameterTag(dateTag, replacements, out string sanitizedFormatter);
|
||||
if (tryFormatDateTime(props.FileDate, sanitizedFormatter, replacements, out var formattedDateString))
|
||||
fileNamingTemplate.ParameterReplacements[sanitizedTag] = formattedDateString;
|
||||
}
|
||||
|
||||
return fileNamingTemplate.GetFilePath(fileExtension).PathWithoutPrefix;
|
||||
}
|
||||
#endregion
|
||||
public override IEnumerable<string> Warnings
|
||||
=> Template.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName))
|
||||
? base.Warnings
|
||||
: base.Warnings.Append(WARNING_NO_CHAPTER_NUMBER_TAG);
|
||||
}
|
||||
|
||||
public class ChapterTitleTemplate : Templates
|
||||
public class ChapterTitleTemplate : Templates, ITemplate
|
||||
{
|
||||
private List<TemplateTags> _templateTags { get; } = new()
|
||||
{
|
||||
TemplateTags.Title,
|
||||
TemplateTags.TitleShort,
|
||||
TemplateTags.Series,
|
||||
TemplateTags.ChCount,
|
||||
TemplateTags.ChNumber,
|
||||
TemplateTags.ChNumber0,
|
||||
TemplateTags.ChTitle,
|
||||
};
|
||||
public override string Name => "Chapter Title Template";
|
||||
|
||||
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
||||
public static string DefaultTemplate => "<ch#> - <title short>: <ch title>";
|
||||
public static IEnumerable<TagClass> TagClass { get; }
|
||||
= chapterPropertyTags.Append(conditionalTags);
|
||||
|
||||
public override string DefaultTemplate => "<ch#> - <title short>: <ch title>";
|
||||
|
||||
protected override bool IsChapterized => true;
|
||||
|
||||
public override IEnumerable<string> GetErrors(string template)
|
||||
=> new List<string>();
|
||||
|
||||
public override IEnumerable<string> GetWarnings(string template)
|
||||
=> GetStandardWarnings(template).ToList();
|
||||
|
||||
public string GetTitle(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props)
|
||||
=> GetPortionTitle(libraryBookDto, Configuration.Instance.ChapterTitleTemplate, props);
|
||||
|
||||
public string GetPortionTitle(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props)
|
||||
{
|
||||
if (string.IsNullOrEmpty(template)) return string.Empty;
|
||||
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
|
||||
var fileNamingTemplate = new MetadataNamingTemplate(template);
|
||||
|
||||
var title = libraryBookDto.Title ?? "";
|
||||
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
|
||||
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Title, title);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.TitleShort, titleShort);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Series, libraryBookDto.SeriesName);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber, props.PartsPosition);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
|
||||
|
||||
return fileNamingTemplate.GetTagContents();
|
||||
}
|
||||
public override IEnumerable<TemplateTags> GetTemplateTags() => _templateTags;
|
||||
protected override IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
|
||||
=> parts.Select(p => p.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FileManager;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public static class UtilityExtensions
|
||||
{
|
||||
public static void AddParameterReplacement(this NamingTemplate fileNamingTemplate, TemplateTags templateTags, object value)
|
||||
=> fileNamingTemplate.AddParameterReplacement(templateTags.TagName, value);
|
||||
|
||||
public static void AddUniqueParameterReplacement(this NamingTemplate namingTemplate, string key, object value)
|
||||
=> namingTemplate.ParameterReplacements[key] = value;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue