343 lines
14 KiB
C#
343 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using Dinah.Core;
|
|
using FileManager;
|
|
|
|
namespace LibationFileManager
|
|
{
|
|
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();
|
|
|
|
public abstract string Name { get; }
|
|
public abstract string Description { get; }
|
|
public abstract string DefaultTemplate { get; }
|
|
protected abstract bool IsChapterized { get; }
|
|
|
|
protected Templates() { }
|
|
|
|
#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 abstract IEnumerable<string> GetWarnings(string template);
|
|
public bool HasWarnings(string template) => GetWarnings(template).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.ContainsInvalid(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.IsNullOrWhiteSpace(template)
|
|
? ""
|
|
: getFileNamingTemplate(libraryBookDto, template, null, null)
|
|
.GetFilePath(Configuration.Instance.ReplacementCharacters).PathWithoutPrefix;
|
|
|
|
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
|
|
internal static FileNamingTemplate getFileNamingTemplate(LibraryBookDto libraryBookDto, string template, string dirFullPath, string extension)
|
|
{
|
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
|
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
|
|
|
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");
|
|
|
|
var t = template + FileUtility.GetStandardizedExtension(extension);
|
|
var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t);
|
|
|
|
var fileNamingTemplate = new FileNamingTemplate(fullfilename);
|
|
|
|
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);
|
|
|
|
return fileNamingTemplate;
|
|
}
|
|
#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)
|
|
{
|
|
var value = template ?? "";
|
|
|
|
// 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 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;
|
|
|
|
internal FolderTemplate() : base() { }
|
|
|
|
#region validation
|
|
public override IEnumerable<string> GetErrors(string template)
|
|
{
|
|
// null is invalid. whitespace is valid but not recommended
|
|
if (template is null)
|
|
return new[] { ERROR_NULL_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 (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.ContainsInvalid(template.Replace("<", "").Replace(">", "")))
|
|
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
|
|
|
|
return Valid;
|
|
}
|
|
|
|
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)
|
|
.GetFilePath(Configuration.Instance.ReplacementCharacters);
|
|
#endregion
|
|
}
|
|
|
|
public class FileTemplate : Templates
|
|
{
|
|
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)
|
|
.GetFilePath(Configuration.Instance.ReplacementCharacters, returnFirstExisting);
|
|
#endregion
|
|
}
|
|
|
|
public class ChapterFileTemplate : Templates
|
|
{
|
|
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;
|
|
|
|
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)
|
|
{
|
|
replacements ??= Configuration.Instance.ReplacementCharacters;
|
|
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName));
|
|
|
|
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.GetFilePath(replacements).PathWithoutPrefix;
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
public class ChapterTitleTemplate : Templates
|
|
{
|
|
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 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)
|
|
{
|
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
|
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;
|
|
}
|
|
}
|
|
}
|