template file naming: code complete. Clean up and testing remain

This commit is contained in:
Robert McRackan 2021-11-02 14:26:11 -04:00
parent 46b120ee41
commit c837fefbdd
16 changed files with 369 additions and 192 deletions

View File

@ -12,23 +12,13 @@ namespace AaxDecrypter
{
protected override StepSequence Steps { get; }
private Func<string, int, int, NewSplitCallback, string> multipartFileNameCallback { get; }
private static string DefaultMultipartFilename(string outputFileName, int partsPosition, int partsTotal, NewSplitCallback newSplitCallback)
{
var template = Path.ChangeExtension(outputFileName, null) + " - <ch# 0> - <title>" + Path.GetExtension(outputFileName);
var fileTemplate = new FileTemplate(template) { IllegalCharacterReplacements = " " };
fileTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(partsPosition, partsTotal));
fileTemplate.AddParameterReplacement("title", newSplitCallback?.Chapter?.Title ?? "");
return fileTemplate.GetFilePath();
}
private Func<MultiConvertFileProperties, string> multipartFileNameCallback { get; }
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
private List<string> multiPartFilePaths { get; } = new List<string>();
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat,
Func<string, int, int, NewSplitCallback, string> multipartFileNameCallback = null)
Func<MultiConvertFileProperties, string> multipartFileNameCallback = null)
: base(outFileName, cacheDirectory, dlLic, outputFormat)
{
Steps = new StepSequence
@ -39,7 +29,7 @@ namespace AaxDecrypter
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
["Step 3: Cleanup"] = Step_Cleanup,
};
this.multipartFileNameCallback = multipartFileNameCallback ?? DefaultMultipartFilename;
this.multipartFileNameCallback = multipartFileNameCallback ?? MultiConvertFileProperties.DefaultMultipartFilename;
}
/*
@ -133,7 +123,13 @@ That naming may not be desirable for everyone, but it's an easy change to instea
private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
{
var fileName = multipartFileNameCallback(OutputFileName, currentChapter, splitChapters.Count, newSplitCallback);
var fileName = multipartFileNameCallback(new()
{
OutputFileName = OutputFileName,
PartsPosition = currentChapter,
PartsTotal = splitChapters.Count,
Title = newSplitCallback?.Chapter?.Title
});
fileName = FileUtility.GetValidFilename(fileName);
multiPartFilePaths.Add(fileName);

View File

@ -0,0 +1,25 @@
using System;
using System.IO;
using FileManager;
namespace AaxDecrypter
{
public class MultiConvertFileProperties
{
public string OutputFileName { get; set; }
public int PartsPosition { get; set; }
public int PartsTotal { get; set; }
public string Title { get; set; }
public static string DefaultMultipartFilename(MultiConvertFileProperties multiConvertFileProperties)
{
var template = Path.ChangeExtension(multiConvertFileProperties.OutputFileName, null) + " - <ch# 0> - <title>" + Path.GetExtension(multiConvertFileProperties.OutputFileName);
var fileNamingTemplate = new FileNamingTemplate(template) { IllegalCharacterReplacements = " " };
fileNamingTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(multiConvertFileProperties.PartsPosition, multiConvertFileProperties.PartsTotal));
fileNamingTemplate.AddParameterReplacement("title", multiConvertFileProperties.Title ?? "");
return fileNamingTemplate.GetFilePath();
}
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Version>6.3.4.1</Version>
<Version>6.3.4.19</Version>
</PropertyGroup>
<ItemGroup>

View File

@ -11,72 +11,94 @@ namespace FileLiberator
{
public static class AudioFileStorageExt
{
private static void AddParameterReplacement(this FileTemplate fileTemplate, TemplateTags templateTags, object value)
=> fileTemplate.AddParameterReplacement(templateTags.TagName, value);
internal class MultipartRenamer
public class MultipartRenamer
{
public LibraryBook libraryBook { get; }
private LibraryBookDto libraryBookDto { get; }
public MultipartRenamer(LibraryBook libraryBook) => this.libraryBook = libraryBook;
public MultipartRenamer(LibraryBook libraryBook) : this(libraryBook.ToDto()) { }
public MultipartRenamer(LibraryBookDto libraryBookDto) => this.libraryBookDto = libraryBookDto;
internal string MultipartFilename(string outputFileName, int partsPosition, int partsTotal, AAXClean.NewSplitCallback newSplitCallback)
=> MultipartFilename(Configuration.Instance.ChapterFileTemplate, AudibleFileStorage.DecryptInProgressDirectory, Path.GetExtension(outputFileName), partsPosition, partsTotal, newSplitCallback?.Chapter?.Title ?? "");
internal string MultipartFilename(AaxDecrypter.MultiConvertFileProperties props)
=> MultipartFilename(props, Configuration.Instance.ChapterFileTemplate, AudibleFileStorage.DecryptInProgressDirectory);
internal string MultipartFilename(string template, string fullDirPath, string extension, int partsPosition, int partsTotal, string chapterTitle)
public string MultipartFilename(AaxDecrypter.MultiConvertFileProperties props, string template, string fullDirPath)
{
var fileTemplate = GetFileTemplateSingle(template, libraryBook, fullDirPath, extension);
var fileNamingTemplate = GetFileNamingTemplate(template, libraryBookDto, fullDirPath, Path.GetExtension(props.OutputFileName));
fileTemplate.AddParameterReplacement(TemplateTags.ChCount, partsTotal);
fileTemplate.AddParameterReplacement(TemplateTags.ChNumber, partsPosition);
fileTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(partsPosition, partsTotal));
fileTemplate.AddParameterReplacement(TemplateTags.ChTitle, chapterTitle);
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 fileTemplate.GetFilePath();
return fileNamingTemplate.GetFilePath();
}
}
public static Func<string, int, int, AAXClean.NewSplitCallback, string> CreateMultipartRenamerFunc(this AudioFileStorage _, LibraryBook libraryBook)
public static Func<AaxDecrypter.MultiConvertFileProperties, string> CreateMultipartRenamerFunc(this AudioFileStorage _, LibraryBook libraryBook)
=> new MultipartRenamer(libraryBook).MultipartFilename;
public static Func<AaxDecrypter.MultiConvertFileProperties, string> CreateMultipartRenamerFunc(this AudioFileStorage _, LibraryBookDto libraryBookDto)
=> new MultipartRenamer(libraryBookDto).MultipartFilename;
/// <summary>
/// DownloadDecryptBook:
/// Path: in progress directory.
/// File name: final file name.
/// </summary>
public static string GetInProgressFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
=> GetCustomDirFilename(_, libraryBook, AudibleFileStorage.DecryptInProgressDirectory, extension);
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
=> GetCustomDirFilename(_, libraryBook, AudibleFileStorage.BooksDirectory, extension);
=> GetFileNamingTemplate(Configuration.Instance.FileTemplate, libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension)
.GetFilePath();
/// <summary>
/// DownloadDecryptBook:
/// File path for where to move files into.
/// Path: directory nested inside of Books directory
/// File name: n/a
/// </summary>
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook)
=> GetFileTemplateSingle(Configuration.Instance.FolderTemplate, libraryBook, AudibleFileStorage.BooksDirectory, null)
=> GetFileNamingTemplate(Configuration.Instance.FolderTemplate, libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, null)
.GetFilePath();
/// <summary>
/// PDF: audio file does not exist
/// </summary>
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
=> GetFileNamingTemplate(Configuration.Instance.FileTemplate, libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, extension)
.GetFilePath();
/// <summary>
/// PDF: audio file already exists
/// </summary>
public static string GetCustomDirFilename(this AudioFileStorage _, LibraryBook libraryBook, string dirFullPath, string extension)
=> GetFileTemplateSingle(Configuration.Instance.FileTemplate, libraryBook, dirFullPath, extension)
=> GetFileNamingTemplate(Configuration.Instance.FileTemplate, libraryBook.ToDto(), dirFullPath, extension)
.GetFilePath();
internal static FileTemplate GetFileTemplateSingle(string template, LibraryBook libraryBook, string dirFullPath, string extension)
public static FileNamingTemplate GetFileNamingTemplate(string template, LibraryBookDto libraryBookDto, string dirFullPath, string extension)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension));
var fileTemplate = new FileTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
dirFullPath = dirFullPath?.Trim() ?? "";
var t = template + FileUtility.GetStandardizedExtension(extension);
var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t);
var title = libraryBook.Book.Title ?? "";
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
fileTemplate.AddParameterReplacement(TemplateTags.Id, libraryBook.Book.AudibleProductId);
fileTemplate.AddParameterReplacement(TemplateTags.Title, title);
fileTemplate.AddParameterReplacement(TemplateTags.TitleShort, title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':')));
fileTemplate.AddParameterReplacement(TemplateTags.Author, libraryBook.Book.AuthorNames);
fileTemplate.AddParameterReplacement(TemplateTags.FirstAuthor, libraryBook.Book.Authors.FirstOrDefault()?.Name);
fileTemplate.AddParameterReplacement(TemplateTags.Narrator, libraryBook.Book.NarratorNames);
fileTemplate.AddParameterReplacement(TemplateTags.FirstNarrator, libraryBook.Book.Narrators.FirstOrDefault()?.Name);
var title = libraryBookDto.Title ?? "";
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
var seriesLink = libraryBook.Book.SeriesLink.FirstOrDefault();
fileTemplate.AddParameterReplacement(TemplateTags.Series, seriesLink?.Series.Name);
fileTemplate.AddParameterReplacement(TemplateTags.SeriesNumber, seriesLink?.Order);
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.Account, libraryBookDto.Account);
fileNamingTemplate.AddParameterReplacement(TemplateTags.Locale, libraryBookDto.Locale);
return fileTemplate;
return fileNamingTemplate;
}
}
}

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core;
using LibationFileManager;
namespace FileLiberator
{
@ -22,5 +23,21 @@ namespace FileLiberator
var apiExtended = await AudibleUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
return apiExtended.Api;
}
public static LibraryBookDto ToDto(this LibraryBook libraryBook) => new()
{
Account = libraryBook.Account,
AudibleProductId = libraryBook.Book.AudibleProductId,
Title = libraryBook.Book.Title ?? "",
Locale = libraryBook.Book.Locale,
Authors = libraryBook.Book.Authors.Select(c => c.Name).ToList(),
Narrators = libraryBook.Book.Narrators.Select(c => c.Name).ToList(),
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Order
};
}
}

View File

@ -6,13 +6,13 @@ using Dinah.Core;
namespace FileManager
{
/// <summary>Get valid filename. Advanced features incl. parameterized template</summary>
public class FileTemplate
public class FileNamingTemplate
{
/// <summary>Proposed full file path. May contain optional html-styled template tags. Eg: &lt;name&gt;</summary>
public string Template { get; }
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
public FileTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
public FileNamingTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
/// <summary>Optional step 1: Replace html-styled template tags with parameters. Eg {"name", "Bill Gates"} => /&lt;name&gt;/ => /Bill Gates/</summary>
public Dictionary<string, object> ParameterReplacements { get; } = new Dictionary<string, object>();

View File

@ -170,14 +170,10 @@ namespace LibationFileManager
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
}
private string getTemplate(string settingName, Templates templ)
{
var value = persistentDictionary.GetString(settingName)?.Trim();
return templ.IsValid(value) ? value : templ.DefaultTemplate;
}
private string getTemplate(string settingName, Templates templ) => templ.GetValid(persistentDictionary.GetString(settingName));
private void setTemplate(string settingName, Templates templ, string newValue)
{
var template = newValue.Trim();
var template = newValue?.Trim();
if (templ.IsValid(template))
persistentDictionary.SetString(settingName, template);
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationFileManager
{
public class BookDto
{
public string AudibleProductId { get; set; }
public string Title { get; set; }
public string Locale { get; set; }
public IEnumerable<string> Authors { get; set; }
public string AuthorNames => string.Join(", ", Authors);
public string FirstAuthor => Authors.FirstOrDefault();
public IEnumerable<string> Narrators { get; set; }
public string NarratorNames => string.Join(", ", Narrators);
public string FirstNarrator => Narrators.FirstOrDefault();
public string SeriesName { get; set; }
public string SeriesNumber { get; set; }
}
public class LibraryBookDto : BookDto
{
public string Account { get; set; }
}
}

View File

@ -18,6 +18,12 @@ namespace LibationFileManager
IsChapterOnly = isChapterOnly;
}
// putting these first is the incredibly lazy way to make them show up first in the settings dialog
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 number", true);
public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter number with leading zeros", true);
public static TemplateTags Id { get; } = new TemplateTags("id", "Audible ID");
public static TemplateTags Title { get; } = new TemplateTags("title", "Full title");
public static TemplateTags TitleShort { get; } = new TemplateTags("title short", "Title. Stop at first colon");
@ -28,10 +34,7 @@ namespace LibationFileManager
public static TemplateTags Series { get; } = new TemplateTags("series", "Name of series");
// can't also have a leading zeros version. Too many weird edge cases. Eg: "1-4"
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series");
public static TemplateTags 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 number", true);
public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter number with leading zeros", true);
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
public static TemplateTags Locale { get; } = new TemplateTags("locale", "Region/country");
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace LibationFileManager
@ -8,7 +9,7 @@ namespace LibationFileManager
{
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 full paths allowed. Eg: should not start with C:\";
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.";
@ -26,6 +27,40 @@ namespace LibationFileManager
public abstract string DefaultTemplate { get; }
protected abstract bool IsChapterized { get; }
internal string GetValid(string configValue)
{
var value = configValue?.Trim();
return IsValid(value) ? value : DefaultTemplate;
}
public static 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 abstract IEnumerable<string> GetErrors(string template);
public bool IsValid(string template) => !GetErrors(template).Any();
@ -51,7 +86,7 @@ namespace LibationFileManager
public static bool ContainsTag(string template, string tag) => template.Contains($"<{tag}>");
protected static string[] getFileErrors(string template)
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.
@ -60,15 +95,15 @@ namespace LibationFileManager
return new[] { ERROR_NULL_IS_INVALID };
if (template.Contains(':')
|| template.Contains(System.IO.Path.DirectorySeparatorChar)
|| template.Contains(System.IO.Path.AltDirectorySeparatorChar)
|| template.Contains(Path.DirectorySeparatorChar)
|| template.Contains(Path.AltDirectorySeparatorChar)
)
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
return Valid;
}
protected IEnumerable<string> getWarnings(string template)
protected IEnumerable<string> GetStandardWarnings(string template)
{
var warnings = GetErrors(template).ToList();
if (template is null)
@ -108,7 +143,7 @@ namespace LibationFileManager
return Valid;
}
public override IEnumerable<string> GetWarnings(string template) => getWarnings(template);
public override IEnumerable<string> GetWarnings(string template) => GetStandardWarnings(template);
}
private class FileTemplate : Templates
@ -118,9 +153,9 @@ namespace LibationFileManager
public override string DefaultTemplate { get; } = "<title> [<id>]";
protected override bool IsChapterized { get; } = false;
public override IEnumerable<string> GetErrors(string template) => getFileErrors(template);
public override IEnumerable<string> GetErrors(string template) => GetFileErrors(template);
public override IEnumerable<string> GetWarnings(string template) => getWarnings(template);
public override IEnumerable<string> GetWarnings(string template) => GetStandardWarnings(template);
}
private class ChapterFileTemplate : Templates
@ -130,11 +165,11 @@ namespace LibationFileManager
public override string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
protected override bool IsChapterized { get; } = true;
public override IEnumerable<string> GetErrors(string template) => getFileErrors(template);
public override IEnumerable<string> GetErrors(string template) => GetFileErrors(template);
public override IEnumerable<string> GetWarnings(string template)
{
var warnings = getWarnings(template).ToList();
var warnings = GetStandardWarnings(template).ToList();
if (template is null)
return warnings;

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FileManager;
namespace LibationFileManager
{
public static class UtilityExtensions
{
public static void AddParameterReplacement(this FileNamingTemplate fileNamingTemplate, TemplateTags templateTags, object value)
=> fileNamingTemplate.AddParameterReplacement(templateTags.TagName, value);
}
}

View File

@ -33,16 +33,17 @@
this.templateTb = new System.Windows.Forms.TextBox();
this.templateLbl = new System.Windows.Forms.Label();
this.resetToDefaultBtn = new System.Windows.Forms.Button();
this.outputTb = new System.Windows.Forms.TextBox();
this.listView1 = new System.Windows.Forms.ListView();
this.columnHeader1 = new System.Windows.Forms.ColumnHeader();
this.columnHeader2 = new System.Windows.Forms.ColumnHeader();
this.richTextBox1 = new System.Windows.Forms.RichTextBox();
this.warningsLbl = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(714, 496);
this.saveBtn.Location = new System.Drawing.Point(714, 345);
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(88, 27);
@ -55,7 +56,7 @@
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(832, 496);
this.cancelBtn.Location = new System.Drawing.Point(832, 345);
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
@ -94,19 +95,6 @@
this.resetToDefaultBtn.UseVisualStyleBackColor = true;
this.resetToDefaultBtn.Click += new System.EventHandler(this.resetToDefaultBtn_Click);
//
// outputTb
//
this.outputTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.outputTb.Location = new System.Drawing.Point(346, 56);
this.outputTb.Multiline = true;
this.outputTb.Name = "outputTb";
this.outputTb.ReadOnly = true;
this.outputTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.outputTb.Size = new System.Drawing.Size(574, 434);
this.outputTb.TabIndex = 4;
//
// listView1
//
this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
@ -117,8 +105,8 @@
this.listView1.HideSelection = false;
this.listView1.Location = new System.Drawing.Point(12, 56);
this.listView1.Name = "listView1";
this.listView1.Size = new System.Drawing.Size(328, 434);
this.listView1.TabIndex = 100;
this.listView1.Size = new System.Drawing.Size(328, 283);
this.listView1.TabIndex = 3;
this.listView1.UseCompatibleStateImageBehavior = false;
this.listView1.View = System.Windows.Forms.View.Details;
//
@ -132,15 +120,41 @@
this.columnHeader2.Text = "Description";
this.columnHeader2.Width = 230;
//
// richTextBox1
//
this.richTextBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.richTextBox1.Location = new System.Drawing.Point(346, 56);
this.richTextBox1.Name = "richTextBox1";
this.richTextBox1.ReadOnly = true;
this.richTextBox1.Size = new System.Drawing.Size(574, 203);
this.richTextBox1.TabIndex = 4;
this.richTextBox1.Text = "";
//
// warningsLbl
//
this.warningsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.warningsLbl.AutoSize = true;
this.warningsLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.warningsLbl.ForeColor = System.Drawing.Color.Firebrick;
this.warningsLbl.Location = new System.Drawing.Point(346, 262);
this.warningsLbl.Name = "warningsLbl";
this.warningsLbl.Size = new System.Drawing.Size(40, 15);
this.warningsLbl.TabIndex = 100;
this.warningsLbl.Text = "label1";
//
// EditTemplateDialog
//
this.AcceptButton = this.saveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(933, 539);
this.ClientSize = new System.Drawing.Size(933, 388);
this.Controls.Add(this.warningsLbl);
this.Controls.Add(this.richTextBox1);
this.Controls.Add(this.listView1);
this.Controls.Add(this.outputTb);
this.Controls.Add(this.resetToDefaultBtn);
this.Controls.Add(this.templateLbl);
this.Controls.Add(this.templateTb);
@ -163,9 +177,10 @@
private System.Windows.Forms.TextBox templateTb;
private System.Windows.Forms.Label templateLbl;
private System.Windows.Forms.Button resetToDefaultBtn;
private System.Windows.Forms.TextBox outputTb;
private System.Windows.Forms.ListView listView1;
private System.Windows.Forms.ColumnHeader columnHeader1;
private System.Windows.Forms.ColumnHeader columnHeader2;
private System.Windows.Forms.RichTextBox richTextBox1;
private System.Windows.Forms.Label warningsLbl;
}
}

View File

@ -10,8 +10,19 @@ namespace LibationWinForms.Dialogs
{
public partial class EditTemplateDialog : Form
{
// final valid value
public string TemplateText { get; private set; }
// work-in-progress. not guaranteed to be valid
private string _workingTemplateText;
private string workingTemplateText
{
get => _workingTemplateText;
set => _workingTemplateText = Templates.Sanitize(value);
}
private void resetTextBox(string value) => this.templateTb.Text = workingTemplateText = value;
private Configuration config { get; } = Configuration.Instance;
private Templates template { get; }
@ -35,108 +46,124 @@ namespace LibationWinForms.Dialogs
return;
}
warningsLbl.Text = "";
this.Text = $"Edit {template.Name}";
this.templateLbl.Text = template.Description;
this.templateTb.Text = inputTemplateText;
resetTextBox(inputTemplateText);
// populate list view
foreach (var tag in template.GetTemplateTags())
listView1.Items.Add(new ListViewItem(new[] { $"<{tag.TagName}>", tag.Description }));
}
private void resetToDefaultBtn_Click(object sender, EventArgs e) => templateTb.Text = template.DefaultTemplate;
private void resetToDefaultBtn_Click(object sender, EventArgs e) => resetTextBox(template.DefaultTemplate);
private void templateTb_TextChanged(object sender, EventArgs e)
{
var t = templateTb.Text;
workingTemplateText = templateTb.Text;
var warnings
= !template.HasWarnings(t)
? ""
: "Warnings:\r\n" +
template
.GetWarnings(t)
.Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}");
var isFolder = template == Templates.Folder;
var libraryBookDto = new LibraryBookDto
{
Account = "my account",
AudibleProductId = "123456789",
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
Locale = "us",
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
Narrators = new List<string> { "Stephen Fry" },
SeriesName = "Sherlock Holmes",
SeriesNumber = "1"
};
var chapterName = "A Flight for Life";
var chapterNumber = 4;
var chaptersTotal = 10;
var books = config.Books;
var folderTemplate = template == Templates.Folder ? t : config.FolderTemplate;
folderTemplate = folderTemplate.Trim().Trim(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Trim();
var fileTemplate = template == Templates.Folder ? config.FileTemplate : t;
fileTemplate = fileTemplate.Trim();
var folder = FileLiberator.AudioFileStorageExt.GetFileNamingTemplate(
isFolder ? workingTemplateText : config.FolderTemplate,
libraryBookDto,
null,
null)
.GetFilePath();
var file
= (template == Templates.ChapterFile)
? new FileLiberator.AudioFileStorageExt.MultipartRenamer(libraryBookDto).MultipartFilename(
new() { OutputFileName = "", PartsPosition = chapterNumber, PartsTotal = chaptersTotal, Title = chapterName },
workingTemplateText,
"")
: FileLiberator.AudioFileStorageExt.GetFileNamingTemplate(
isFolder ? config.FileTemplate : workingTemplateText,
libraryBookDto,
null,
null)
.GetFilePath();
var ext = config.DecryptToLossy ? "mp3" : "m4b";
var path = Path.Combine(books, folderTemplate, $"{fileTemplate}.{ext}");
// this logic should be external
path = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
var sing = $"{Path.DirectorySeparatorChar}";
var dbl = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}";
while (path.Contains(dbl))
path = path.Replace(dbl, sing);
// once path is finalized
const char ZERO_WIDTH_SPACE = '\u200B';
path = path.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
var sing = $"{Path.DirectorySeparatorChar}";
// result: can wrap long paths. eg:
// |-- LINE WRAP BOUNDARIES --|
// \books\author with a very <= normal line break on space between words
// long name\narrator narrator
// \title <= line break on the zero-with space we added before slashes
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
var book = new DataLayer.Book(
new DataLayer.AudibleProductId("123456789"),
"A Study in Scarlet: A Sherlock Holmes Novel",
"Fake description",
1234,
DataLayer.ContentType.Product,
new List<DataLayer.Contributor>
{
new("Arthur Conan Doyle"),
new("Stephen Fry - introductions")
},
new List<DataLayer.Contributor> { new("Stephen Fry") },
new DataLayer.Category(new DataLayer.AudibleCategoryId("cat12345"), "Mystery"),
"us"
);
var libraryBook = new DataLayer.LibraryBook(book, DateTime.Now, "my account");
warningsLbl.Text
= !template.HasWarnings(workingTemplateText)
? ""
: "Warning:\r\n" +
template
.GetWarnings(workingTemplateText)
.Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}");
outputTb.Text = @$"
var bold = new System.Drawing.Font(richTextBox1.Font, System.Drawing.FontStyle.Bold);
var reg = new System.Drawing.Font(richTextBox1.Font, System.Drawing.FontStyle.Regular);
Example:
richTextBox1.Clear();
richTextBox1.SelectionFont = reg;
{books}
{folderTemplate}
{fileTemplate}
{ext}
{path}
richTextBox1.AppendText(slashWrap(books));
richTextBox1.AppendText(sing);
{book.AudibleProductId}
{book.Title}
{book.AuthorNames}
{book.NarratorNames}
series: {"Sherlock Holmes"}
if (isFolder)
richTextBox1.SelectionFont = bold;
{warnings}
richTextBox1.AppendText(slashWrap(folder));
".Trim();
if (isFolder)
richTextBox1.SelectionFont = reg;
richTextBox1.AppendText(sing);
if (!isFolder)
richTextBox1.SelectionFont = bold;
richTextBox1.AppendText(file);
if (!isFolder)
richTextBox1.SelectionFont = reg;
richTextBox1.AppendText($".{ext}");
}
private void saveBtn_Click(object sender, EventArgs e)
{
if (!template.IsValid(templateTb.Text))
if (!template.IsValid(workingTemplateText))
{
var errors = template
.GetErrors(templateTb.Text)
.GetErrors(workingTemplateText)
.Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}");
MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
TemplateText = templateTb.Text;
TemplateText = workingTemplateText;
this.DialogResult = DialogResult.OK;
this.Close();

View File

@ -12,12 +12,18 @@ namespace AudioFileStorageExtTests
{
public static class Shared
{
public static DataLayer.LibraryBook GetLibraryBook(string asin)
{
var book = new DataLayer.Book(new DataLayer.AudibleProductId(asin), "title", "desc", 1, DataLayer.ContentType.Product, new List<DataLayer.Contributor> { new DataLayer.Contributor("author") }, new List<DataLayer.Contributor> { new DataLayer.Contributor("narrator") }, new DataLayer.Category(new DataLayer.AudibleCategoryId("seriesId"), "name"), "us");
var libraryBook = new DataLayer.LibraryBook(book, DateTime.Now, "my us");
return libraryBook;
}
public static LibationFileManager.LibraryBookDto GetLibraryBook(string asin)
=> new()
{
Account = "my account",
AudibleProductId = asin,
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
Locale = "us",
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
Narrators = new List<string> { "Stephen Fry" },
SeriesName = "Sherlock Holmes",
SeriesNumber = "1"
};
}
[TestClass]
@ -28,31 +34,25 @@ namespace AudioFileStorageExtTests
[DataRow("asin", "<ch#>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\6.txt")]
public void Tests(string asin, string template, string dir, string ext, int pos, int total, string chapter, string expected)
=> new AudioFileStorageExt.MultipartRenamer(GetLibraryBook(asin))
.MultipartFilename(template, dir, ext, pos, total, chapter)
.MultipartFilename(new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, template, dir)
.Should().Be(expected);
}
[TestClass]
public class GetFileTemplateSingle
public class GetFileNamingTemplate
{
[TestMethod]
[DataRow(null, "asin", @"C:\", "ext")]
[DataRow("f.txt", null, @"C:\", "ext")]
[DataRow("f.txt", "asin", null, "ext")]
[ExpectedException(typeof(ArgumentNullException))]
public void arg_null_exception(string template, string asin, string dirFullPath, string extension)
=> AudioFileStorageExt.GetFileTemplateSingle(template, GetLibraryBook(asin), dirFullPath, extension);
=> AudioFileStorageExt.GetFileNamingTemplate(template, GetLibraryBook(asin), dirFullPath, extension);
[TestMethod]
[DataRow("", "asin", @"C:\foo\bar", "ext")]
[DataRow(" ", "asin", @"C:\foo\bar", "ext")]
[DataRow("f.txt", "", @"C:\foo\bar", "ext")]
[DataRow("f.txt", " ", @"C:\foo\bar", "ext")]
[DataRow("f.txt", "asin", "", "ext")]
[DataRow("f.txt", "asin", " ", "ext")]
[ExpectedException(typeof(ArgumentException))]
public void arg_exception(string template, string asin, string dirFullPath, string extension)
=> AudioFileStorageExt.GetFileTemplateSingle(template, GetLibraryBook(asin), dirFullPath, extension);
=> AudioFileStorageExt.GetFileNamingTemplate(template, GetLibraryBook(asin), dirFullPath, extension);
[TestMethod]
public void null_extension() => Tests("f.txt", "asin", @"C:\foo\bar", null, @"C:\foo\bar\f.txt");
@ -62,7 +62,7 @@ namespace AudioFileStorageExtTests
[DataRow("f", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\f.ext")]
[DataRow("<id>", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\asin.ext")]
public void Tests(string template, string asin, string dirFullPath, string extension, string expected)
=> AudioFileStorageExt.GetFileTemplateSingle(template, GetLibraryBook(asin), dirFullPath, extension)
=> AudioFileStorageExt.GetFileNamingTemplate(template, GetLibraryBook(asin), dirFullPath, extension)
.GetFilePath()
.Should().Be(expected);
}

View File

@ -7,7 +7,7 @@ using FileManager;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FileTemplateTests
namespace FileNamingTemplateTests
{
[TestClass]
public class GetFilePath
@ -17,7 +17,7 @@ namespace FileTemplateTests
{
var expected = @"C:\foo\bar\my_ book LONG_1234567890_1234567890_1234567890_123 [ID123456].txt";
var f1 = OLD_GetValidFilename(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
var f2 = NEW_GetValidFilename_FileTemplate(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
var f2 = NEW_GetValidFilename_FileNamingTemplate(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
f1.Should().Be(expected);
f1.Should().Be(f2);
@ -49,16 +49,16 @@ namespace FileTemplateTests
return fullfilename;
}
private static string NEW_GetValidFilename_FileTemplate(string dirFullPath, string filename, string extension, string metadataSuffix)
private static string NEW_GetValidFilename_FileNamingTemplate(string dirFullPath, string filename, string extension, string metadataSuffix)
{
var template = $"<title> [<id>]";
var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension));
var fileTemplate = new FileTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
fileTemplate.AddParameterReplacement("title", filename);
fileTemplate.AddParameterReplacement("id", metadataSuffix);
return fileTemplate.GetFilePath();
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
fileNamingTemplate.AddParameterReplacement("title", filename);
fileNamingTemplate.AddParameterReplacement("id", metadataSuffix);
return fileNamingTemplate.GetFilePath();
}
[TestMethod]
@ -66,7 +66,7 @@ namespace FileTemplateTests
{
var expected = @"C:\foo\bar\my file - 002 - title.txt";
var f1 = OLD_GetMultipartFileName(@"C:\foo\bar\my file.txt", 2, 100, "title");
var f2 = NEW_GetMultipartFileName_FileTemplate(@"C:\foo\bar\my file.txt", 2, 100, "title");
var f2 = NEW_GetMultipartFileName_FileNamingTemplate(@"C:\foo\bar\my file.txt", 2, 100, "title");
f1.Should().Be(expected);
f1.Should().Be(f2);
@ -89,7 +89,7 @@ namespace FileTemplateTests
var path = Path.Combine(Path.GetDirectoryName(originalPath), fileName + extension);
return path;
}
private static string NEW_GetMultipartFileName_FileTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
private static string NEW_GetMultipartFileName_FileNamingTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
{
// 1-9 => 1-9
// 10-99 => 01-99
@ -98,19 +98,18 @@ namespace FileTemplateTests
var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath);
var fileTemplate = new FileTemplate(t) { IllegalCharacterReplacements = " " };
fileTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
fileTemplate.AddParameterReplacement("title", suffix);
return fileTemplate.GetFilePath();
var fileNamingTemplate = new FileNamingTemplate(t) { IllegalCharacterReplacements = " " };
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
fileNamingTemplate.AddParameterReplacement("title", suffix);
return fileNamingTemplate.GetFilePath();
}
[TestMethod]
public void remove_slashes()
{
var fileTemplate = new FileTemplate(@"\foo\<title>.txt");
fileTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
fileTemplate.GetFilePath().Should().Be(@"\foo\slashes.txt");
var fileNamingTemplate = new FileNamingTemplate(@"\foo\<title>.txt");
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
fileNamingTemplate.GetFilePath().Should().Be(@"\foo\slashes.txt");
}
}
}

View File

@ -24,7 +24,7 @@ STRUCTURE
* 2 Utilities (domain ignorant)
Stand-alone libraries with no knowledge of anything having to do with Libation or other programs. In theory any of these should be able to one day be converted to a nuget pkg
* 3 Domain Internal Utilities (db ignorant)
Can have knowledge of Libation concepts. Cannot access the database.
Cannot access the database. Can have knowledge of Libation concepts. Can even have knowledge of some db concepts, but no actual db access.
* 4 Domain (db)
All database access
* 5 Domain Utilities (db aware)