Compare commits

..

24 Commits

Author SHA1 Message Date
Robert McRackan
283a46e1e2 Add debugging for issue #149 2021-11-05 13:42:47 -04:00
Robert McRackan
6ff2859c39 Update dependencies 2021-11-04 16:13:43 -04:00
Robert McRackan
e8df4952fc update dependencies 2021-11-03 16:51:40 -04:00
rmcrackan
b19e1e8a30 Update README for Custom File Naming 2021-11-02 22:20:51 -04:00
Robert McRackan
a3cf6ac40d Custom file naming: manual testing complete 2021-11-02 21:53:58 -04:00
Robert McRackan
ab450c37c4 Custom File Naming complete. Final testing remains 2021-11-02 17:05:29 -04:00
Robert McRackan
c837fefbdd template file naming: code complete. Clean up and testing remain 2021-11-02 14:26:11 -04:00
Robert McRackan
46b120ee41 Bug fix: slashes in template values (eg: title) breaks file management #145 2021-11-01 11:42:05 -04:00
Robert McRackan
cae8ca7ef3 Template error and warning checks return specific errors/warnings in addition to bools 2021-10-29 17:05:57 -04:00
Robert McRackan
904665da7f Bug fix: #143 2021-10-28 20:58:32 -04:00
Robert McRackan
2478c61df6 Bugfix: template validation was opposite #142 2021-10-28 14:38:01 -04:00
Robert McRackan
288ed75b5d increm ver 2021-10-27 20:46:02 -04:00
Robert McRackan
ad5efbd9a9 Bug fix for #141 2021-10-27 20:45:21 -04:00
Robert McRackan
7eb7b2a0f9 better formatting for download/decrypt ETA 2021-10-27 17:05:17 -04:00
Robert McRackan
d0051c0f02 Add templates to settings dialog incl load validate save. Edit buttons are in place but currently do nothing 2021-10-27 16:51:31 -04:00
Robert McRackan
d20517063e Settings: single screen => tabs 2021-10-27 15:50:41 -04:00
Robert McRackan
bcca69a102 Bug fix. Wrong template referenced 2021-10-26 16:35:08 -04:00
Robert McRackan
35f8c05106 File naming is Configuration driven: Configuration, AudioFileStorageExt, Templates, TemplateTags, 2021-10-26 16:18:27 -04:00
Robert McRackan
a3d38e082d Path.GetInvalidPathChars() acts differently in C# interactive vs live code 2021-10-26 13:54:37 -04:00
Robert McRackan
b2e956e70b Update dependencies 2021-10-26 13:06:24 -04:00
Robert McRackan
e5119357b2 File naming is fully template driven 2021-10-22 17:06:42 -04:00
Robert McRackan
b42ff827d5 GetStandardizedExtension unit tests 2021-10-22 13:09:05 -04:00
Robert McRackan
68da9779da Expose internal to Test projects 2021-10-22 11:07:18 -04:00
Robert McRackan
8e358d8f04 expose library book to multipart decrypter for file naming 2021-10-21 16:43:49 -04:00
46 changed files with 1779 additions and 229 deletions

View File

@@ -5,8 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean" Version="0.1.9" />
<PackageReference Include="Dinah.Core" Version="2.0.1.1" />
<PackageReference Include="AAXClean" Version="0.1.10" />
</ItemGroup>
<ItemGroup>

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) + " - <chapter> - <title>" + Path.GetExtension(outputFileName);
var fileTemplate = new FileTemplate(template) { IllegalCharacterReplacements = " " };
fileTemplate.AddParameterReplacement("chapter", 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.2.8.1</Version>
<Version>6.4.2.1</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -59,6 +59,7 @@ namespace AppScaffolding
Migrations.migrate_to_v5_7_1(config);
Migrations.migrate_to_v6_1_2(config);
Migrations.migrate_to_v6_2_0(config);
Migrations.migrate_to_v6_2_9(config);
}
/// <summary>Initialize logging. Run after migration</summary>
@@ -345,5 +346,18 @@ namespace AppScaffolding
if (!config.Exists(nameof(config.SplitFilesByChapter)))
config.SplitFilesByChapter = false;
}
// add file naming templates
public static void migrate_to_v6_2_9(Configuration config)
{
if (!config.Exists(nameof(config.FolderTemplate)))
config.FolderTemplate = Templates.Folder.DefaultTemplate;
if (!config.Exists(nameof(config.FileTemplate)))
config.FileTemplate = Templates.File.DefaultTemplate;
if (!config.Exists(nameof(config.ChapterFileTemplate)))
config.ChapterFileTemplate = Templates.ChapterFile.DefaultTemplate;
}
}
}

View File

@@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="CsvHelper" Version="27.1.1" />
<PackageReference Include="NPOI" Version="2.5.4" />
<PackageReference Include="NPOI" Version="2.5.5" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="2.3.1.1" />
<PackageReference Include="AudibleApi" Version="2.3.4.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(AudibleUtilities) + ".Tests")]

View File

@@ -185,6 +185,9 @@ namespace DataLayer
{
get
{
if (_seriesLink is null)
return "";
// first: alphabetical by name
var withNames = _seriesLink
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))

View File

@@ -11,59 +11,46 @@ namespace FileLiberator
{
public static class AudioFileStorageExt
{
public static string MultipartFilename(this AudioFileStorage _, string outputFileName, int partsPosition, int partsTotal, AAXClean.NewSplitCallback newSplitCallback)
=> MultipartFilename(outputFileName, partsPosition, partsTotal, newSplitCallback);
public static string MultipartFilename(string outputFileName, int partsPosition, int partsTotal, AAXClean.NewSplitCallback newSplitCallback)
private class MultipartRenamer
{
var template = Path.ChangeExtension(outputFileName, null) + " - <chapter> - <title>" + Path.GetExtension(outputFileName);
private LibraryBook libraryBook { get; }
var fileTemplate = new FileTemplate(template) { IllegalCharacterReplacements = " " };
fileTemplate.AddParameterReplacement("chapter", FileUtility.GetSequenceFormatted(partsPosition, partsTotal));
fileTemplate.AddParameterReplacement("title", newSplitCallback?.Chapter?.Title ?? "");
internal MultipartRenamer(LibraryBook libraryBook) => this.libraryBook = libraryBook;
return fileTemplate.GetFilePath();
internal string MultipartFilename(AaxDecrypter.MultiConvertFileProperties props)
=> Templates.ChapterFile.GetFilename(libraryBook.ToDto(), props);
}
public static Func<AaxDecrypter.MultiConvertFileProperties, string> CreateMultipartRenamerFunc(this AudioFileStorage _, LibraryBook libraryBook)
=> new MultipartRenamer(libraryBook).MultipartFilename;
/// <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)
=> Templates.Folder.GetFilename(libraryBook.ToDto());
/// <summary>
/// DownloadDecryptBook:
/// Path: in progress directory.
/// File name: final file name.
/// </summary>
public static string GetInProgressFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
=> GetInProgressFilename(libraryBook, extension);
public static string GetInProgressFilename(LibraryBook libraryBook, string extension)
=> GetValidFilename(AudibleFileStorage.DecryptInProgressDirectory, libraryBook.Book.Title, extension, libraryBook);
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension);
/// <summary>
/// PDF: audio file does not exist
/// </summary>
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
=> GetBooksDirectoryFilename(libraryBook, extension);
public static string GetBooksDirectoryFilename(LibraryBook libraryBook, string extension)
=> GetValidFilename(AudibleFileStorage.BooksDirectory, libraryBook.Book.Title, extension, libraryBook);
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, extension);
public static string CreateDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook)
=> CreateDestinationDirectory(libraryBook);
public static string CreateDestinationDirectory(LibraryBook libraryBook)
{
var title = libraryBook.Book.Title;
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
var underscoreIndex = title.IndexOf(':');
var titleDir
= underscoreIndex < 4
? title
: title.Substring(0, underscoreIndex);
var destinationDir = GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, libraryBook);
Directory.CreateDirectory(destinationDir);
return destinationDir;
}
public static string GetValidFilename(string dirFullPath, string filename, string extension, LibraryBook libraryBook)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(filename, nameof(filename));
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", libraryBook.Book.AudibleProductId);
return fileTemplate.GetFilePath();
}
/// <summary>
/// PDF: audio file already exists
/// </summary>
public static string GetCustomDirFilename(this AudioFileStorage _, LibraryBook libraryBook, string dirFullPath, string extension)
=> Templates.File.GetFilename(libraryBook.ToDto(), dirFullPath, extension);
}
}

View File

@@ -121,7 +121,10 @@ namespace FileLiberator
abDownloader
= contentLic.DrmType != AudibleApi.Common.DrmType.Adrm ? new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic)
: Configuration.Instance.SplitFilesByChapter ? new AaxcDownloadMultiConverter(outFileName, cacheDir, audiobookDlLic, outputFormat, AudibleFileStorage.Audio.MultipartFilename)
: Configuration.Instance.SplitFilesByChapter ? new AaxcDownloadMultiConverter(
outFileName, cacheDir, audiobookDlLic, outputFormat,
AudibleFileStorage.Audio.CreateMultipartRenamerFunc(libraryBook)
)
: new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic, outputFormat);
abDownloader.DecryptProgressUpdate += (_, progress) => OnStreamingProgressChanged(progress);
abDownloader.DecryptTimeRemaining += (_, remaining) => OnStreamingTimeRemaining(remaining);
@@ -176,9 +179,10 @@ namespace FileLiberator
private static bool moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
{
// create final directory. move each file into it
var destinationDir = AudibleFileStorage.Audio.CreateDestinationDirectory(libraryBook);
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
Directory.CreateDirectory(destinationDir);
FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
if (getFirstAudio() == default)
return false;

View File

@@ -40,14 +40,14 @@ namespace FileLiberator
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
{
var extension = Path.GetExtension(getdownloadUrl(libraryBook));
// if audio file exists, get it's dir. else return base Book dir
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
var file = getdownloadUrl(libraryBook);
if (existingPath is not null)
return AudibleFileStorage.Audio.GetCustomDirFilename(libraryBook, existingPath, extension);
if (existingPath != null)
return Path.Combine(existingPath, Path.GetFileName(file));
return AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, Path.GetExtension(file));
return AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, extension);
}
private static string getdownloadUrl(LibraryBook libraryBook)

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

@@ -0,0 +1 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(FileLiberator) + ".Tests")]

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="2.0.1.1" />
<PackageReference Include="Dinah.Core" Version="3.0.1.1" />
<PackageReference Include="Polly" Version="7.2.2" />
</ItemGroup>

View File

@@ -1,25 +1,26 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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, string> ParameterReplacements { get; } = new Dictionary<string, string>();
public Dictionary<string, object> ParameterReplacements { get; } = new Dictionary<string, object>();
/// <summary>Convenience method</summary>
public void AddParameterReplacement(string key ,string value) => ParameterReplacements.Add(key, value);
public void AddParameterReplacement(string key, object value)
// using .Add() instead of "[key] = value" will make unintended overwriting throw exception
=> ParameterReplacements.Add(key, value);
/// <summary>If set, truncate each parameter replacement to this many characters. Default 50</summary>
public int? ParameterMaxSize { get; set; } = 50;
@@ -38,14 +39,26 @@ namespace FileManager
return FileUtility.GetValidFilename(filename, IllegalCharacterReplacements);
}
private string formatKey(string key)
private static string formatKey(string key)
=> key
.Replace("<", "")
.Replace(">", "");
private string formatValue(string value)
=> ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0
? value?.Truncate(ParameterMaxSize.Value)
: value;
private string formatValue(object value)
{
if (value is null)
return "";
// Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders.
// Esp important for file templates.
var val = value
.ToString()
.Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements)
.Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements);
return
ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0
? val.Truncate(ParameterMaxSize.Value)
: val;
}
}
}

View File

@@ -18,7 +18,7 @@ namespace FileManager
public static string GetStandardizedExtension(string extension)
=> string.IsNullOrWhiteSpace(extension)
? (extension ?? "")?.Trim()
: '.' + extension.Trim('.');
: '.' + extension.Trim().Trim('.');
/// <summary>
/// Return position with correct number of leading zeros.
@@ -86,10 +86,15 @@ namespace FileManager
{
ArgumentValidator.EnsureNotNull(path, nameof(path));
var invalidChars = Path.GetInvalidPathChars().Union(new[] {
'*', '?',
// these are weird. If you run Path.GetInvalidPathChars() in C# interactive, these characters are included.
// In live code, Path.GetInvalidPathChars() does not include them
'"', '<', '>'
}).ToArray();
var fixedPath = string
.Join(illegalCharacterReplacements ?? "", path.Split(Path.GetInvalidPathChars()))
.Replace("*", illegalCharacterReplacements)
.Replace("?", illegalCharacterReplacements)
.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars))
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
// replace all colons except within the first 2 chars
@@ -103,6 +108,11 @@ namespace FileManager
builder.Append(c);
}
fixedPath = builder.ToString();
var dblSeparator = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}";
while (fixedPath.Contains(dblSeparator))
fixedPath = fixedPath.Replace(dblSeparator, $"{Path.DirectorySeparatorChar}");
return fixedPath;
}

View File

@@ -205,8 +205,25 @@ namespace FileManager
private JObject readFile()
{
if (!File.Exists(Filepath))
{
var msg = "Unrecoverable error. Settings file cannot be found";
var ex = new FileNotFoundException(msg, Filepath);
Serilog.Log.Logger.Error(msg, ex);
throw ex;
}
var settingsJsonContents = File.ReadAllText(Filepath);
var jObject = JsonConvert.DeserializeObject<JObject>(settingsJsonContents);
if (jObject is null)
{
var msg = "Unrecoverable error. Unable to read settings from Settings file";
var ex = new NullReferenceException(msg);
Serilog.Log.Logger.Error(msg, ex);
throw ex;
}
return jObject;
}
}

View File

@@ -0,0 +1 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(FileManager) + ".Tests")]

View File

@@ -62,6 +62,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator.Tests", "_Tes
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager.Tests", "_Tests\FileManager.Tests\FileManager.Tests.csproj", "{F2E04270-4551-41C4-99FF-E7125BED708C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibationFileManager.Tests", "_Tests\LibationFileManager.Tests\LibationFileManager.Tests.csproj", "{EB781571-8548-477E-82AD-FB9FAB548D2F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -136,6 +138,10 @@ Global
{F2E04270-4551-41C4-99FF-E7125BED708C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2E04270-4551-41C4-99FF-E7125BED708C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2E04270-4551-41C4-99FF-E7125BED708C}.Release|Any CPU.Build.0 = Release|Any CPU
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -158,6 +164,7 @@ Global
{788294BE-0D8E-40D4-9CEE-67896FBB52CE} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{F2E04270-4551-41C4-99FF-E7125BED708C} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{EB781571-8548-477E-82AD-FB9FAB548D2F} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}

View File

@@ -145,8 +145,40 @@ namespace LibationFileManager
{
get => persistentDictionary.GetNonString<bool>(nameof(DownloadEpisodes));
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
}
#region templates: custom file naming
[Description("How to format the folders in which files will be saved")]
public string FolderTemplate
{
get => getTemplate(nameof(FolderTemplate), Templates.Folder);
set => setTemplate(nameof(FolderTemplate), Templates.Folder, value);
}
[Description("How to format the saved pdf and audio files")]
public string FileTemplate
{
get => getTemplate(nameof(FileTemplate), Templates.File);
set => setTemplate(nameof(FileTemplate), Templates.File, value);
}
[Description("How to format the saved audio files when split by chapters")]
public string ChapterFileTemplate
{
get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile);
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
}
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();
if (templ.IsValid(template))
persistentDictionary.SetString(settingName, template);
}
#endregion
#endregion
#region known directories

View File

@@ -9,6 +9,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />
</ItemGroup>

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

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dinah.Core;
namespace LibationFileManager
{
public sealed class TemplateTags : Enumeration<TemplateTags>
{
public string TagName => DisplayName;
public string Description { get; }
public bool IsChapterOnly { get; }
private static int value = 0;
private TemplateTags(string tagName, string description, bool isChapterOnly = false) : base(value++, tagName)
{
Description = description;
IsChapterOnly = isChapterOnly;
}
// 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 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");
public static TemplateTags Author { get; } = new TemplateTags("author", "Author(s)");
public static TemplateTags FirstAuthor { get; } = new TemplateTags("first author", "First author");
public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)");
public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator");
public static TemplateTags Series { get; } = new TemplateTags("series", "Name of series");
// can't also have a leading zeros version. Too many weird edge cases. Eg: "1-4"
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series");
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
public static TemplateTags Locale { get; } = new TemplateTags("locale", "Region/country");
}
}

View File

@@ -0,0 +1,274 @@
using Dinah.Core;
using FileManager;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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 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 (template.Contains(':')
|| template.Contains(Path.DirectorySeparatorChar)
|| template.Contains(Path.AltDirectorySeparatorChar)
)
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();
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() ?? "";
var t = template + FileUtility.GetStandardizedExtension(extension);
var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t);
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
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.Account, libraryBookDto.Account);
fileNamingTemplate.AddParameterReplacement(TemplateTags.Locale, libraryBookDto.Locale);
return fileNamingTemplate;
}
#endregion
public 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 };
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)
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, AudibleFileStorage.BooksDirectory, null)
.GetFilePath();
#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)
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension)
.GetFilePath();
#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)
{
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();
}
#endregion
}
}
}

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

@@ -0,0 +1 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(LibationFileManager) + ".Tests")]

View File

@@ -0,0 +1 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(LibationSearchEngine) + ".Tests")]

View File

@@ -103,6 +103,15 @@ namespace LibationWinForms.BookLiberation
=> bookInfoLbl.UIThreadAsync(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
private void updateRemainingTime(int remaining)
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{remaining} sec");
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{formatTime(remaining)}");
private string formatTime(int seconds)
{
var timeSpan = new TimeSpan(0, 0, seconds);
return
timeSpan.TotalHours >= 1 ? $"{timeSpan:%h}h {timeSpan:mm}m {timeSpan:ss}s"
: timeSpan.TotalMinutes >= 1 ? $"{timeSpan:%m}m {timeSpan:ss}s"
: $"{seconds} sec";
}
}
}

View File

@@ -0,0 +1,198 @@
namespace LibationWinForms.Dialogs
{
partial class EditTemplateDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.saveBtn = new System.Windows.Forms.Button();
this.cancelBtn = new System.Windows.Forms.Button();
this.templateTb = new System.Windows.Forms.TextBox();
this.templateLbl = new System.Windows.Forms.Label();
this.resetToDefaultBtn = new System.Windows.Forms.Button();
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.exampleLbl = 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, 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);
this.saveBtn.TabIndex = 98;
this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
//
// cancelBtn
//
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, 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);
this.cancelBtn.TabIndex = 99;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// templateTb
//
this.templateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.templateTb.Location = new System.Drawing.Point(12, 27);
this.templateTb.Name = "templateTb";
this.templateTb.Size = new System.Drawing.Size(779, 23);
this.templateTb.TabIndex = 1;
this.templateTb.TextChanged += new System.EventHandler(this.templateTb_TextChanged);
//
// templateLbl
//
this.templateLbl.AutoSize = true;
this.templateLbl.Location = new System.Drawing.Point(12, 9);
this.templateLbl.Name = "templateLbl";
this.templateLbl.Size = new System.Drawing.Size(89, 15);
this.templateLbl.TabIndex = 0;
this.templateLbl.Text = "[template desc]";
//
// resetToDefaultBtn
//
this.resetToDefaultBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.resetToDefaultBtn.Location = new System.Drawing.Point(797, 26);
this.resetToDefaultBtn.Name = "resetToDefaultBtn";
this.resetToDefaultBtn.Size = new System.Drawing.Size(124, 23);
this.resetToDefaultBtn.TabIndex = 2;
this.resetToDefaultBtn.Text = "Reset to default";
this.resetToDefaultBtn.UseVisualStyleBackColor = true;
this.resetToDefaultBtn.Click += new System.EventHandler(this.resetToDefaultBtn_Click);
//
// listView1
//
this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)));
this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader1,
this.columnHeader2});
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, 283);
this.listView1.TabIndex = 3;
this.listView1.UseCompatibleStateImageBehavior = false;
this.listView1.View = System.Windows.Forms.View.Details;
//
// columnHeader1
//
this.columnHeader1.Text = "Tag";
this.columnHeader1.Width = 90;
//
// columnHeader2
//
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, 74);
this.richTextBox1.Name = "richTextBox1";
this.richTextBox1.ReadOnly = true;
this.richTextBox1.Size = new System.Drawing.Size(574, 185);
this.richTextBox1.TabIndex = 5;
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(14, 15);
this.warningsLbl.TabIndex = 100;
this.warningsLbl.Text = "6";
//
// exampleLbl
//
this.exampleLbl.AutoSize = true;
this.exampleLbl.Location = new System.Drawing.Point(346, 56);
this.exampleLbl.Name = "exampleLbl";
this.exampleLbl.Size = new System.Drawing.Size(55, 15);
this.exampleLbl.TabIndex = 4;
this.exampleLbl.Text = "Example:";
//
// 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, 388);
this.Controls.Add(this.exampleLbl);
this.Controls.Add(this.warningsLbl);
this.Controls.Add(this.richTextBox1);
this.Controls.Add(this.listView1);
this.Controls.Add(this.resetToDefaultBtn);
this.Controls.Add(this.templateLbl);
this.Controls.Add(this.templateTb);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "EditTemplateDialog";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Edit Template";
this.Load += new System.EventHandler(this.EditTemplateDialog_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.TextBox templateTb;
private System.Windows.Forms.Label templateLbl;
private System.Windows.Forms.Button resetToDefaultBtn;
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;
private System.Windows.Forms.Label exampleLbl;
}
}

View File

@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Windows.Forms;
using Dinah.Core;
using LibationFileManager;
namespace LibationWinForms.Dialogs
{
public partial class EditTemplateDialog : Form
{
// final value. post-validity check
public string TemplateText { get; private set; }
// hold the work-in-progress value. not guaranteed to be valid
private string _workingTemplateText;
private string workingTemplateText
{
get => _workingTemplateText;
set => _workingTemplateText = template.Sanitize(value);
}
private void resetTextBox(string value) => this.templateTb.Text = workingTemplateText = value;
private Configuration config { get; } = Configuration.Instance;
private Templates template { get; }
private string inputTemplateText { get; }
public EditTemplateDialog() => InitializeComponent();
public EditTemplateDialog(Templates template, string inputTemplateText) : this()
{
this.template = ArgumentValidator.EnsureNotNull(template, nameof(template));
this.inputTemplateText = inputTemplateText ?? "";
}
private void EditTemplateDialog_Load(object sender, EventArgs e)
{
if (this.DesignMode)
return;
if (template is null)
{
MessageBoxAlertAdmin.Show($"Programming error. {nameof(EditTemplateDialog)} was not created correctly", "Edit template error", new NullReferenceException($"{nameof(template)} is null"));
return;
}
warningsLbl.Text = "";
this.Text = $"Edit {template.Name}";
this.templateLbl.Text = template.Description;
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) => resetTextBox(template.DefaultTemplate);
private void templateTb_TextChanged(object sender, EventArgs e)
{
workingTemplateText = templateTb.Text;
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 folder = Templates.Folder.GetPortionFilename(
libraryBookDto,
isFolder ? workingTemplateText : config.FolderTemplate);
var file
= template == Templates.ChapterFile
? Templates.ChapterFile.GetPortionFilename(
libraryBookDto,
workingTemplateText,
new() { OutputFileName = "", PartsPosition = chapterNumber, PartsTotal = chaptersTotal, Title = chapterName },
"")
: Templates.File.GetPortionFilename(
libraryBookDto,
isFolder ? config.FileTemplate : workingTemplateText);
var ext = config.DecryptToLossy ? "mp3" : "m4b";
const char ZERO_WIDTH_SPACE = '\u200B';
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}");
warningsLbl.Text
= !template.HasWarnings(workingTemplateText)
? ""
: "Warning:\r\n" +
template
.GetWarnings(workingTemplateText)
.Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}");
var bold = new System.Drawing.Font(richTextBox1.Font, System.Drawing.FontStyle.Bold);
var reg = new System.Drawing.Font(richTextBox1.Font, System.Drawing.FontStyle.Regular);
richTextBox1.Clear();
richTextBox1.SelectionFont = reg;
richTextBox1.AppendText(slashWrap(books));
richTextBox1.AppendText(sing);
if (isFolder)
richTextBox1.SelectionFont = bold;
richTextBox1.AppendText(slashWrap(folder));
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(workingTemplateText))
{
var errors = template
.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 = workingTemplateText;
this.DialogResult = DialogResult.OK;
this.Close();
}
private void cancelBtn_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
}
}

View File

@@ -0,0 +1,60 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -32,7 +32,6 @@
this.inProgressDescLbl = new System.Windows.Forms.Label();
this.saveBtn = new System.Windows.Forms.Button();
this.cancelBtn = new System.Windows.Forms.Button();
this.advancedSettingsGb = new System.Windows.Forms.GroupBox();
this.importEpisodesCb = new System.Windows.Forms.CheckBox();
this.downloadEpisodesCb = new System.Windows.Forms.CheckBox();
this.badBookGb = new System.Windows.Forms.GroupBox();
@@ -48,13 +47,33 @@
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
this.logsBtn = new System.Windows.Forms.Button();
this.booksSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
this.booksGb = new System.Windows.Forms.GroupBox();
this.loggingLevelLbl = new System.Windows.Forms.Label();
this.loggingLevelCb = new System.Windows.Forms.ComboBox();
this.advancedSettingsGb.SuspendLayout();
this.tabControl = new System.Windows.Forms.TabControl();
this.tab1ImportantSettings = new System.Windows.Forms.TabPage();
this.booksGb = new System.Windows.Forms.GroupBox();
this.tab2ImportLibrary = new System.Windows.Forms.TabPage();
this.tab3DownloadDecrypt = new System.Windows.Forms.TabPage();
this.inProgressFilesGb = new System.Windows.Forms.GroupBox();
this.customFileNamingGb = new System.Windows.Forms.GroupBox();
this.chapterFileTemplateBtn = new System.Windows.Forms.Button();
this.chapterFileTemplateTb = new System.Windows.Forms.TextBox();
this.chapterFileTemplateLbl = new System.Windows.Forms.Label();
this.fileTemplateBtn = new System.Windows.Forms.Button();
this.fileTemplateTb = new System.Windows.Forms.TextBox();
this.fileTemplateLbl = new System.Windows.Forms.Label();
this.folderTemplateBtn = new System.Windows.Forms.Button();
this.folderTemplateTb = new System.Windows.Forms.TextBox();
this.folderTemplateLbl = new System.Windows.Forms.Label();
this.badBookGb.SuspendLayout();
this.decryptAndConvertGb.SuspendLayout();
this.tabControl.SuspendLayout();
this.tab1ImportantSettings.SuspendLayout();
this.booksGb.SuspendLayout();
this.tab2ImportLibrary.SuspendLayout();
this.tab3DownloadDecrypt.SuspendLayout();
this.inProgressFilesGb.SuspendLayout();
this.customFileNamingGb.SuspendLayout();
this.SuspendLayout();
//
// booksLocationDescLbl
@@ -70,12 +89,12 @@
// inProgressDescLbl
//
this.inProgressDescLbl.AutoSize = true;
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 199);
this.inProgressDescLbl.Location = new System.Drawing.Point(7, 19);
this.inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.inProgressDescLbl.Name = "inProgressDescLbl";
this.inProgressDescLbl.Size = new System.Drawing.Size(43, 45);
this.inProgressDescLbl.Size = new System.Drawing.Size(100, 45);
this.inProgressDescLbl.TabIndex = 18;
this.inProgressDescLbl.Text = "[desc]\r\n[line 2]\r\n[line 3]";
this.inProgressDescLbl.Text = "[in progress desc]\r\n[line 2]\r\n[line 3]";
//
// saveBtn
//
@@ -102,30 +121,10 @@
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// advancedSettingsGb
//
this.advancedSettingsGb.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.advancedSettingsGb.Controls.Add(this.importEpisodesCb);
this.advancedSettingsGb.Controls.Add(this.downloadEpisodesCb);
this.advancedSettingsGb.Controls.Add(this.badBookGb);
this.advancedSettingsGb.Controls.Add(this.decryptAndConvertGb);
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
this.advancedSettingsGb.Controls.Add(this.inProgressDescLbl);
this.advancedSettingsGb.Location = new System.Drawing.Point(12, 176);
this.advancedSettingsGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.advancedSettingsGb.Name = "advancedSettingsGb";
this.advancedSettingsGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 309);
this.advancedSettingsGb.TabIndex = 6;
this.advancedSettingsGb.TabStop = false;
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
//
// importEpisodesCb
//
this.importEpisodesCb.AutoSize = true;
this.importEpisodesCb.Location = new System.Drawing.Point(7, 22);
this.importEpisodesCb.Location = new System.Drawing.Point(6, 6);
this.importEpisodesCb.Name = "importEpisodesCb";
this.importEpisodesCb.Size = new System.Drawing.Size(146, 19);
this.importEpisodesCb.TabIndex = 7;
@@ -135,7 +134,7 @@
// downloadEpisodesCb
//
this.downloadEpisodesCb.AutoSize = true;
this.downloadEpisodesCb.Location = new System.Drawing.Point(7, 47);
this.downloadEpisodesCb.Location = new System.Drawing.Point(6, 31);
this.downloadEpisodesCb.Name = "downloadEpisodesCb";
this.downloadEpisodesCb.Size = new System.Drawing.Size(163, 19);
this.downloadEpisodesCb.TabIndex = 8;
@@ -148,9 +147,9 @@
this.badBookGb.Controls.Add(this.badBookRetryRb);
this.badBookGb.Controls.Add(this.badBookAbortRb);
this.badBookGb.Controls.Add(this.badBookAskRb);
this.badBookGb.Location = new System.Drawing.Point(372, 72);
this.badBookGb.Location = new System.Drawing.Point(371, 6);
this.badBookGb.Name = "badBookGb";
this.badBookGb.Size = new System.Drawing.Size(529, 124);
this.badBookGb.Size = new System.Drawing.Size(524, 124);
this.badBookGb.TabIndex = 13;
this.badBookGb.TabStop = false;
this.badBookGb.Text = "[bad book desc]";
@@ -205,7 +204,7 @@
this.decryptAndConvertGb.Controls.Add(this.allowLibationFixupCbox);
this.decryptAndConvertGb.Controls.Add(this.convertLossyRb);
this.decryptAndConvertGb.Controls.Add(this.convertLosslessRb);
this.decryptAndConvertGb.Location = new System.Drawing.Point(7, 72);
this.decryptAndConvertGb.Location = new System.Drawing.Point(6, 6);
this.decryptAndConvertGb.Name = "decryptAndConvertGb";
this.decryptAndConvertGb.Size = new System.Drawing.Size(359, 124);
this.decryptAndConvertGb.TabIndex = 9;
@@ -261,15 +260,15 @@
//
this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 247);
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 68);
this.inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.inProgressSelectControl.Name = "inProgressSelectControl";
this.inProgressSelectControl.Size = new System.Drawing.Size(894, 52);
this.inProgressSelectControl.Size = new System.Drawing.Size(875, 52);
this.inProgressSelectControl.TabIndex = 19;
//
// logsBtn
//
this.logsBtn.Location = new System.Drawing.Point(262, 147);
this.logsBtn.Location = new System.Drawing.Point(256, 169);
this.logsBtn.Name = "logsBtn";
this.logsBtn.Size = new System.Drawing.Size(132, 23);
this.logsBtn.TabIndex = 5;
@@ -284,26 +283,13 @@
this.booksSelectControl.Location = new System.Drawing.Point(7, 37);
this.booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.booksSelectControl.Name = "booksSelectControl";
this.booksSelectControl.Size = new System.Drawing.Size(895, 87);
this.booksSelectControl.Size = new System.Drawing.Size(876, 87);
this.booksSelectControl.TabIndex = 2;
//
// booksGb
//
this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.booksGb.Controls.Add(this.booksSelectControl);
this.booksGb.Controls.Add(this.booksLocationDescLbl);
this.booksGb.Location = new System.Drawing.Point(12, 12);
this.booksGb.Name = "booksGb";
this.booksGb.Size = new System.Drawing.Size(908, 129);
this.booksGb.TabIndex = 0;
this.booksGb.TabStop = false;
this.booksGb.Text = "Books location";
//
// loggingLevelLbl
//
this.loggingLevelLbl.AutoSize = true;
this.loggingLevelLbl.Location = new System.Drawing.Point(12, 150);
this.loggingLevelLbl.Location = new System.Drawing.Point(6, 172);
this.loggingLevelLbl.Name = "loggingLevelLbl";
this.loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
this.loggingLevelLbl.TabIndex = 3;
@@ -313,11 +299,201 @@
//
this.loggingLevelCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.loggingLevelCb.FormattingEnabled = true;
this.loggingLevelCb.Location = new System.Drawing.Point(96, 147);
this.loggingLevelCb.Location = new System.Drawing.Point(90, 169);
this.loggingLevelCb.Name = "loggingLevelCb";
this.loggingLevelCb.Size = new System.Drawing.Size(129, 23);
this.loggingLevelCb.TabIndex = 4;
//
// tabControl
//
this.tabControl.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.tabControl.Controls.Add(this.tab1ImportantSettings);
this.tabControl.Controls.Add(this.tab2ImportLibrary);
this.tabControl.Controls.Add(this.tab3DownloadDecrypt);
this.tabControl.Location = new System.Drawing.Point(12, 12);
this.tabControl.Name = "tabControl";
this.tabControl.SelectedIndex = 0;
this.tabControl.Size = new System.Drawing.Size(909, 478);
this.tabControl.TabIndex = 100;
//
// tab1ImportantSettings
//
this.tab1ImportantSettings.Controls.Add(this.booksGb);
this.tab1ImportantSettings.Controls.Add(this.logsBtn);
this.tab1ImportantSettings.Controls.Add(this.loggingLevelCb);
this.tab1ImportantSettings.Controls.Add(this.loggingLevelLbl);
this.tab1ImportantSettings.Location = new System.Drawing.Point(4, 24);
this.tab1ImportantSettings.Name = "tab1ImportantSettings";
this.tab1ImportantSettings.Padding = new System.Windows.Forms.Padding(3);
this.tab1ImportantSettings.Size = new System.Drawing.Size(901, 450);
this.tab1ImportantSettings.TabIndex = 0;
this.tab1ImportantSettings.Text = "Important settings";
this.tab1ImportantSettings.UseVisualStyleBackColor = true;
//
// booksGb
//
this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.booksGb.Controls.Add(this.booksSelectControl);
this.booksGb.Controls.Add(this.booksLocationDescLbl);
this.booksGb.Location = new System.Drawing.Point(6, 6);
this.booksGb.Name = "booksGb";
this.booksGb.Size = new System.Drawing.Size(889, 129);
this.booksGb.TabIndex = 0;
this.booksGb.TabStop = false;
this.booksGb.Text = "Books location";
//
// tab2ImportLibrary
//
this.tab2ImportLibrary.Controls.Add(this.importEpisodesCb);
this.tab2ImportLibrary.Controls.Add(this.downloadEpisodesCb);
this.tab2ImportLibrary.Location = new System.Drawing.Point(4, 24);
this.tab2ImportLibrary.Name = "tab2ImportLibrary";
this.tab2ImportLibrary.Padding = new System.Windows.Forms.Padding(3);
this.tab2ImportLibrary.Size = new System.Drawing.Size(901, 450);
this.tab2ImportLibrary.TabIndex = 1;
this.tab2ImportLibrary.Text = "Import library";
this.tab2ImportLibrary.UseVisualStyleBackColor = true;
//
// tab3DownloadDecrypt
//
this.tab3DownloadDecrypt.Controls.Add(this.inProgressFilesGb);
this.tab3DownloadDecrypt.Controls.Add(this.customFileNamingGb);
this.tab3DownloadDecrypt.Controls.Add(this.decryptAndConvertGb);
this.tab3DownloadDecrypt.Controls.Add(this.badBookGb);
this.tab3DownloadDecrypt.Location = new System.Drawing.Point(4, 24);
this.tab3DownloadDecrypt.Name = "tab3DownloadDecrypt";
this.tab3DownloadDecrypt.Padding = new System.Windows.Forms.Padding(3);
this.tab3DownloadDecrypt.Size = new System.Drawing.Size(901, 450);
this.tab3DownloadDecrypt.TabIndex = 2;
this.tab3DownloadDecrypt.Text = "Download/Decrypt";
this.tab3DownloadDecrypt.UseVisualStyleBackColor = true;
//
// inProgressFilesGb
//
this.inProgressFilesGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.inProgressFilesGb.Controls.Add(this.inProgressDescLbl);
this.inProgressFilesGb.Controls.Add(this.inProgressSelectControl);
this.inProgressFilesGb.Location = new System.Drawing.Point(7, 299);
this.inProgressFilesGb.Name = "inProgressFilesGb";
this.inProgressFilesGb.Size = new System.Drawing.Size(888, 128);
this.inProgressFilesGb.TabIndex = 21;
this.inProgressFilesGb.TabStop = false;
this.inProgressFilesGb.Text = "In progress files";
//
// customFileNamingGb
//
this.customFileNamingGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateBtn);
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateTb);
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateLbl);
this.customFileNamingGb.Controls.Add(this.fileTemplateBtn);
this.customFileNamingGb.Controls.Add(this.fileTemplateTb);
this.customFileNamingGb.Controls.Add(this.fileTemplateLbl);
this.customFileNamingGb.Controls.Add(this.folderTemplateBtn);
this.customFileNamingGb.Controls.Add(this.folderTemplateTb);
this.customFileNamingGb.Controls.Add(this.folderTemplateLbl);
this.customFileNamingGb.Location = new System.Drawing.Point(7, 136);
this.customFileNamingGb.Name = "customFileNamingGb";
this.customFileNamingGb.Size = new System.Drawing.Size(888, 157);
this.customFileNamingGb.TabIndex = 20;
this.customFileNamingGb.TabStop = false;
this.customFileNamingGb.Text = "Custom file naming";
//
// chapterFileTemplateBtn
//
this.chapterFileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.chapterFileTemplateBtn.Location = new System.Drawing.Point(808, 124);
this.chapterFileTemplateBtn.Name = "chapterFileTemplateBtn";
this.chapterFileTemplateBtn.Size = new System.Drawing.Size(75, 23);
this.chapterFileTemplateBtn.TabIndex = 8;
this.chapterFileTemplateBtn.Text = "Edit...";
this.chapterFileTemplateBtn.UseVisualStyleBackColor = true;
this.chapterFileTemplateBtn.Click += new System.EventHandler(this.chapterFileTemplateBtn_Click);
//
// chapterFileTemplateTb
//
this.chapterFileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.chapterFileTemplateTb.Location = new System.Drawing.Point(6, 125);
this.chapterFileTemplateTb.Name = "chapterFileTemplateTb";
this.chapterFileTemplateTb.ReadOnly = true;
this.chapterFileTemplateTb.Size = new System.Drawing.Size(796, 23);
this.chapterFileTemplateTb.TabIndex = 7;
//
// chapterFileTemplateLbl
//
this.chapterFileTemplateLbl.AutoSize = true;
this.chapterFileTemplateLbl.Location = new System.Drawing.Point(6, 107);
this.chapterFileTemplateLbl.Name = "chapterFileTemplateLbl";
this.chapterFileTemplateLbl.Size = new System.Drawing.Size(123, 15);
this.chapterFileTemplateLbl.TabIndex = 6;
this.chapterFileTemplateLbl.Text = "[folder template desc]";
//
// fileTemplateBtn
//
this.fileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.fileTemplateBtn.Location = new System.Drawing.Point(808, 80);
this.fileTemplateBtn.Name = "fileTemplateBtn";
this.fileTemplateBtn.Size = new System.Drawing.Size(75, 23);
this.fileTemplateBtn.TabIndex = 5;
this.fileTemplateBtn.Text = "Edit...";
this.fileTemplateBtn.UseVisualStyleBackColor = true;
this.fileTemplateBtn.Click += new System.EventHandler(this.fileTemplateBtn_Click);
//
// fileTemplateTb
//
this.fileTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.fileTemplateTb.Location = new System.Drawing.Point(6, 81);
this.fileTemplateTb.Name = "fileTemplateTb";
this.fileTemplateTb.ReadOnly = true;
this.fileTemplateTb.Size = new System.Drawing.Size(796, 23);
this.fileTemplateTb.TabIndex = 4;
//
// fileTemplateLbl
//
this.fileTemplateLbl.AutoSize = true;
this.fileTemplateLbl.Location = new System.Drawing.Point(6, 63);
this.fileTemplateLbl.Name = "fileTemplateLbl";
this.fileTemplateLbl.Size = new System.Drawing.Size(123, 15);
this.fileTemplateLbl.TabIndex = 3;
this.fileTemplateLbl.Text = "[folder template desc]";
//
// folderTemplateBtn
//
this.folderTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.folderTemplateBtn.Location = new System.Drawing.Point(807, 36);
this.folderTemplateBtn.Name = "folderTemplateBtn";
this.folderTemplateBtn.Size = new System.Drawing.Size(75, 23);
this.folderTemplateBtn.TabIndex = 2;
this.folderTemplateBtn.Text = "Edit...";
this.folderTemplateBtn.UseVisualStyleBackColor = true;
this.folderTemplateBtn.Click += new System.EventHandler(this.folderTemplateBtn_Click);
//
// folderTemplateTb
//
this.folderTemplateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.folderTemplateTb.Location = new System.Drawing.Point(5, 37);
this.folderTemplateTb.Name = "folderTemplateTb";
this.folderTemplateTb.ReadOnly = true;
this.folderTemplateTb.Size = new System.Drawing.Size(796, 23);
this.folderTemplateTb.TabIndex = 1;
//
// folderTemplateLbl
//
this.folderTemplateLbl.AutoSize = true;
this.folderTemplateLbl.Location = new System.Drawing.Point(5, 19);
this.folderTemplateLbl.Name = "folderTemplateLbl";
this.folderTemplateLbl.Size = new System.Drawing.Size(123, 15);
this.folderTemplateLbl.TabIndex = 0;
this.folderTemplateLbl.Text = "[folder template desc]";
//
// SettingsDialog
//
this.AcceptButton = this.saveBtn;
@@ -325,11 +501,7 @@
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(933, 539);
this.Controls.Add(this.logsBtn);
this.Controls.Add(this.loggingLevelCb);
this.Controls.Add(this.loggingLevelLbl);
this.Controls.Add(this.booksGb);
this.Controls.Add(this.advancedSettingsGb);
this.Controls.Add(this.tabControl);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
@@ -338,16 +510,23 @@
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Edit Settings";
this.Load += new System.EventHandler(this.SettingsDialog_Load);
this.advancedSettingsGb.ResumeLayout(false);
this.advancedSettingsGb.PerformLayout();
this.badBookGb.ResumeLayout(false);
this.badBookGb.PerformLayout();
this.decryptAndConvertGb.ResumeLayout(false);
this.decryptAndConvertGb.PerformLayout();
this.tabControl.ResumeLayout(false);
this.tab1ImportantSettings.ResumeLayout(false);
this.tab1ImportantSettings.PerformLayout();
this.booksGb.ResumeLayout(false);
this.booksGb.PerformLayout();
this.tab2ImportLibrary.ResumeLayout(false);
this.tab2ImportLibrary.PerformLayout();
this.tab3DownloadDecrypt.ResumeLayout(false);
this.inProgressFilesGb.ResumeLayout(false);
this.inProgressFilesGb.PerformLayout();
this.customFileNamingGb.ResumeLayout(false);
this.customFileNamingGb.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
@@ -356,13 +535,11 @@
private System.Windows.Forms.Label inProgressDescLbl;
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.GroupBox advancedSettingsGb;
private System.Windows.Forms.CheckBox allowLibationFixupCbox;
private DirectoryOrCustomSelectControl booksSelectControl;
private DirectorySelectControl inProgressSelectControl;
private System.Windows.Forms.RadioButton convertLossyRb;
private System.Windows.Forms.RadioButton convertLosslessRb;
private System.Windows.Forms.GroupBox booksGb;
private System.Windows.Forms.Button logsBtn;
private System.Windows.Forms.Label loggingLevelLbl;
private System.Windows.Forms.ComboBox loggingLevelCb;
@@ -375,5 +552,21 @@
private System.Windows.Forms.CheckBox downloadEpisodesCb;
private System.Windows.Forms.CheckBox importEpisodesCb;
private System.Windows.Forms.CheckBox splitFilesByChapterCbox;
}
private System.Windows.Forms.TabControl tabControl;
private System.Windows.Forms.TabPage tab1ImportantSettings;
private System.Windows.Forms.GroupBox booksGb;
private System.Windows.Forms.TabPage tab2ImportLibrary;
private System.Windows.Forms.TabPage tab3DownloadDecrypt;
private System.Windows.Forms.GroupBox inProgressFilesGb;
private System.Windows.Forms.GroupBox customFileNamingGb;
private System.Windows.Forms.Button chapterFileTemplateBtn;
private System.Windows.Forms.TextBox chapterFileTemplateTb;
private System.Windows.Forms.Label chapterFileTemplateLbl;
private System.Windows.Forms.Button fileTemplateBtn;
private System.Windows.Forms.TextBox fileTemplateTb;
private System.Windows.Forms.Label fileTemplateLbl;
private System.Windows.Forms.Button folderTemplateBtn;
private System.Windows.Forms.TextBox folderTemplateTb;
private System.Windows.Forms.Label folderTemplateLbl;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Windows.Forms;
@@ -78,6 +79,13 @@ namespace LibationWinForms.Dialogs
_ => this.badBookAskRb
};
rb.Checked = true;
folderTemplateLbl.Text = desc(nameof(config.FolderTemplate));
fileTemplateLbl.Text = desc(nameof(config.FileTemplate));
chapterFileTemplateLbl.Text = desc(nameof(config.ChapterFileTemplate));
folderTemplateTb.Text = config.FolderTemplate;
fileTemplateTb.Text = config.FileTemplate;
chapterFileTemplateTb.Text = config.ChapterFileTemplate;
}
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
@@ -95,28 +103,56 @@ namespace LibationWinForms.Dialogs
private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(Configuration.Instance.LibationFiles);
private void folderTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.Folder, folderTemplateTb);
private void fileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.File, fileTemplateTb);
private void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb);
private static void editTemplate(Templates template, TextBox textBox)
{
var form = new EditTemplateDialog(template, textBox.Text);
if (form.ShowDialog() == DialogResult.OK)
textBox.Text = form.TemplateText;
}
private void saveBtn_Click(object sender, EventArgs e)
{
var newBooks = booksSelectControl.SelectedDirectory;
#region validation
static void validationError(string text, string caption)
=> MessageBox.Show(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
if (string.IsNullOrWhiteSpace(newBooks))
{
MessageBox.Show("Cannot set Books Location to blank", "Location is blank", MessageBoxButtons.OK, MessageBoxIcon.Error);
validationError("Cannot set Books Location to blank", "Location is blank");
return;
}
if (!Directory.Exists(newBooks))
if (!Directory.Exists(newBooks) && booksSelectControl.SelectedDirectoryIsCustom)
{
if (booksSelectControl.SelectedDirectoryIsCustom)
{
MessageBox.Show($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (booksSelectControl.SelectedDirectoryIsKnown)
Directory.CreateDirectory(newBooks);
validationError($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist");
return;
}
// these 3 should do nothing. Configuration will only init these with a valid value. EditTemplateDialog ensures valid before returning
if (!Templates.Folder.IsValid(folderTemplateTb.Text))
{
validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template");
return;
}
if (!Templates.File.IsValid(fileTemplateTb.Text))
{
validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template");
return;
}
if (!Templates.ChapterFile.IsValid(chapterFileTemplateTb.Text))
{
validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template");
return;
}
#endregion
if (!Directory.Exists(newBooks) && booksSelectControl.SelectedDirectoryIsKnown)
Directory.CreateDirectory(newBooks);
config.Books = newBooks;
{
@@ -145,6 +181,10 @@ namespace LibationWinForms.Dialogs
: badBookIgnoreRb.Checked ? Configuration.BadBookAction.Ignore
: Configuration.BadBookAction.Ask;
config.FolderTemplate = folderTemplateTb.Text;
config.FileTemplate = fileTemplateTb.Text;
config.ChapterFileTemplate = chapterFileTemplateTb.Text;
this.DialogResult = DialogResult.OK;
this.Close();
}

View File

@@ -29,7 +29,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="2.0.1.2" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="2.1.1.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -27,6 +27,7 @@
- [Files and folders](#files-and-folders)
- [Linux and Mac (unofficial)](#linux-and-mac)
- [Settings](#settings)
- [Custom File Naming](#custom-file-naming)
- [Command Line Interface](#command-line-interface)
## Audible audiobook manager
@@ -243,6 +244,12 @@ Although Libation only currently officially supports Windows, [some users](https
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
### Custom File Naming
In Settings, on the Download/Decrypt tab, you can specify the format in which you want your files to be named. As you edit these templates, a live example will be shown. Parameters are listed for folders, files, and files split by chapter including an explanation of what each naming option means. For instance: you can use template `<title short> - <ch# 0> of <ch count> - <ch title>` to create the file `A Study in Scarlet - 04 of 10 - A Flight for Life.m4b`.
These templates apply to GUI and CLI.
### Command Line Interface
Libationcli.exe allows limited access to Libation's functionalities as a CLI.

View File

@@ -7,8 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />

View File

@@ -1,45 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dinah.Core;
using FileLiberator;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AudioFileStorageExtTests
{
[TestClass]
public class GetValidFilename
{
private 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;
}
[TestMethod]
[DataRow(null, "name", "ext", "suffix")]
[DataRow(@"C:\", null, "ext", "suffix")]
[ExpectedException(typeof(ArgumentNullException))]
public void arg_null_exception(string dirFullPath, string filename, string extension, string metadataSuffix)
=> AudioFileStorageExt.GetValidFilename(dirFullPath, filename, extension, GetLibraryBook(metadataSuffix));
[TestMethod]
[DataRow("", "name", "ext", "suffix")]
[DataRow(" ", "name", "ext", "suffix")]
[DataRow(@"C:\", "", "ext", "suffix")]
[DataRow(@"C:\", " ", "ext", "suffix")]
[ExpectedException(typeof(ArgumentException))]
public void arg_exception(string dirFullPath, string filename, string extension, string metadataSuffix)
=> AudioFileStorageExt.GetValidFilename(dirFullPath, filename, extension, GetLibraryBook(metadataSuffix));
[TestMethod]
public void null_extension() => Tests(@"C:\foo\bar", "my file", null, "meta", @"C:\foo\bar\my file [meta]");
[TestMethod]
[DataRow(@"C:\foo\bar", "my file", "txt", "my id", @"C:\foo\bar\my file [my id].txt")]
public void Tests(string dirFullPath, string filename, string extension, string metadataSuffix, string expected)
=> AudioFileStorageExt.GetValidFilename(dirFullPath, filename, extension, GetLibraryBook(metadataSuffix)).Should().Be(expected);
}
}

View File

@@ -7,8 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
<PackageReference Include="coverlet.collector" Version="3.1.0">

View File

@@ -7,8 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
<PackageReference Include="coverlet.collector" Version="3.1.0">

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,11 +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);
var fileNamingTemplate = new FileNamingTemplate(t) { IllegalCharacterReplacements = " " };
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
fileNamingTemplate.AddParameterReplacement("title", suffix);
return fileNamingTemplate.GetFilePath();
}
return fileTemplate.GetFilePath();
[TestMethod]
public void remove_slashes()
{
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

@@ -17,7 +17,7 @@ namespace FileUtilityTests
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
[TestMethod]
[DataRow("http://test.com/a/b/c", @"http\\test.com\a\b\c")]
[DataRow("http://test.com/a/b/c", @"http\test.com\a\b\c")]
public void null_replacement(string inStr, string outStr) => Tests(inStr, null, outStr);
[TestMethod]
@@ -31,8 +31,11 @@ namespace FileUtilityTests
[DataRow("a*?:z.txt", "Z", "aZZZz.txt")]
// retain drive letter path colon
[DataRow(@"C:\az.txt", "Z", @"C:\az.txt")]
// replace all other colongs
// replace all other colons
[DataRow(@"a\b:c\d.txt", "ZZZ", @"a\bZZZc\d.txt")]
// remove empty directories
[DataRow(@"C:\a\\\b\c\\\d.txt", "ZZZ", @"C:\a\b\c\d.txt")]
[DataRow(@"C:\""foo\<id>", "ZZZ", @"C:\ZZZfoo\ZZZidZZZ")]
public void Tests(string inStr, string replacement, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, replacement));
}
@@ -90,4 +93,24 @@ namespace FileUtilityTests
public void Tests(int partsPosition, int partsTotal, string expected)
=> FileUtility.GetSequenceFormatted(partsPosition, partsTotal).Should().Be(expected);
}
[TestClass]
public class GetStandardizedExtension
{
[TestMethod]
public void is_null() => Tests(null, "");
[TestMethod]
public void is_empty() => Tests("", "");
[TestMethod]
public void is_whitespace() => Tests(" ", "");
[TestMethod]
[DataRow("txt", ".txt")]
[DataRow(".txt", ".txt")]
[DataRow(" .txt ", ".txt")]
public void Tests(string input, string expected)
=> FileUtility.GetStandardizedExtension(input).Should().Be(expected);
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\LibationFileManager\LibationFileManager.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,375 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core;
using FluentAssertions;
using LibationFileManager;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static TemplatesTests.Shared;
namespace TemplatesTests
{
public static class Shared
{
public static 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]
public class ContainsChapterOnlyTags
{
[TestMethod]
[DataRow("<ch>", false)]
[DataRow("<ch#>", true)]
[DataRow("<id>", false)]
[DataRow("<id><ch#>", true)]
public void Tests(string template, bool expected) => Templates.ContainsChapterOnlyTags(template).Should().Be(expected);
}
[TestClass]
public class ContainsTag
{
[TestMethod]
[DataRow("<ch#>", "ch#", true)]
[DataRow("<id>", "ch#", false)]
[DataRow("<id><ch#>", "ch#", true)]
public void Tests(string template, string tag, bool expected) => Templates.ContainsTag(template, tag).Should().Be(expected);
}
[TestClass]
public class getFileNamingTemplate
{
[TestMethod]
[DataRow(null, "asin", @"C:\", "ext")]
[ExpectedException(typeof(ArgumentNullException))]
public void arg_null_exception(string template, string asin, string dirFullPath, string extension)
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension);
[TestMethod]
[DataRow("", "asin", @"C:\foo\bar", "ext")]
[DataRow(" ", "asin", @"C:\foo\bar", "ext")]
[ExpectedException(typeof(ArgumentException))]
public void arg_exception(string template, string asin, string dirFullPath, string extension)
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension);
[TestMethod]
public void null_extension() => Tests("f.txt", "asin", @"C:\foo\bar", null, @"C:\foo\bar\f.txt");
[TestMethod]
[DataRow("f.txt", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\f.txt.ext")]
[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)
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension)
.GetFilePath()
.Should().Be(expected);
}
}
namespace Templates_Folder_Tests
{
[TestClass]
public class GetErrors
{
[TestMethod]
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
[TestMethod]
public void empty_is_valid() => valid_tests("");
[TestMethod]
public void whitespace_is_valid() => valid_tests(" ");
[TestMethod]
[DataRow(@"foo")]
[DataRow(@"\foo")]
[DataRow(@"foo\")]
[DataRow(@"\foo\")]
[DataRow(@"foo\bar")]
[DataRow(@"<id>")]
[DataRow(@"<id>\<title>")]
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
[TestMethod]
[DataRow(@"C:\", Templates.ERROR_FULL_PATH_IS_INVALID)]
public void Tests(string template, params string[] expected)
{
var result = Templates.Folder.GetErrors(template);
result.Count().Should().Be(expected.Length);
result.Should().BeEquivalentTo(expected);
}
}
[TestClass]
public class IsValid
{
[TestMethod]
public void null_is_invalid() => Tests(null, false);
[TestMethod]
public void empty_is_valid() => Tests("", true);
[TestMethod]
public void whitespace_is_valid() => Tests(" ", true);
[TestMethod]
[DataRow(@"C:\", false)]
[DataRow(@"foo", true)]
[DataRow(@"\foo", true)]
[DataRow(@"foo\", true)]
[DataRow(@"\foo\", true)]
[DataRow(@"foo\bar", true)]
[DataRow(@"<id>", true)]
[DataRow(@"<id>\<title>", true)]
public void Tests(string template, bool expected) => Templates.Folder.IsValid(template).Should().Be(expected);
}
[TestClass]
public class GetWarnings
{
[TestMethod]
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
[TestMethod]
public void empty_has_warnings() => Tests("", Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS);
[TestMethod]
public void whitespace_has_warnings() => Tests(" ", Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS);
[TestMethod]
[DataRow(@"<id>\foo\bar")]
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
[TestMethod]
[DataRow(@"no tags", Templates.WARNING_NO_TAGS)]
[DataRow("<ch#> <id>", Templates.WARNING_HAS_CHAPTER_TAGS)]
[DataRow("<ch#> chapter tag", Templates.WARNING_NO_TAGS, Templates.WARNING_HAS_CHAPTER_TAGS)]
public void Tests(string template, params string[] expected)
{
var result = Templates.Folder.GetWarnings(template);
result.Count().Should().Be(expected.Length);
result.Should().BeEquivalentTo(expected);
}
}
[TestClass]
public class HasWarnings
{
[TestMethod]
public void null_has_warnings() => Tests(null, true);
[TestMethod]
public void empty_has_warnings() => Tests("", true);
[TestMethod]
public void whitespace_has_warnings() => Tests(" ", true);
[TestMethod]
[DataRow(@"no tags", true)]
[DataRow(@"<id>\foo\bar", false)]
[DataRow("<ch#> <id>", true)]
[DataRow("<ch#> chapter tag", true)]
public void Tests(string template, bool expected) => Templates.Folder.HasWarnings(template).Should().Be(expected);
}
[TestClass]
public class TagCount
{
[TestMethod]
public void null_throws() => Assert.ThrowsException<NullReferenceException>(() => Templates.Folder.TagCount(null));
[TestMethod]
public void empty() => Tests("", 0);
[TestMethod]
public void whitespace() => Tests(" ", 0);
[TestMethod]
[DataRow("no tags", 0)]
[DataRow(@"<id>\foo\bar", 1)]
[DataRow("<id> <id>", 2)]
[DataRow("<id <id> >", 1)]
[DataRow("<id> <title>", 2)]
[DataRow("id> <title incomplete tags", 0)]
[DataRow("<not a real tag>", 0)]
[DataRow("<ch#> non-folder tag", 0)]
[DataRow("<ID> case specific", 0)]
public void Tests(string template, int expected) => Templates.Folder.TagCount(template).Should().Be(expected);
}
}
namespace Templates_File_Tests
{
[TestClass]
public class GetErrors
{
[TestMethod]
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
[TestMethod]
public void empty_is_valid() => valid_tests("");
[TestMethod]
public void whitespace_is_valid() => valid_tests(" ");
[TestMethod]
[DataRow(@"foo")]
[DataRow(@"<id>")]
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
[TestMethod]
[DataRow(@"C:\", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"\foo", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"/foo", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"C:\", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
public void Tests(string template, params string[] expected)
{
var result = Templates.File.GetErrors(template);
result.Count().Should().Be(expected.Length);
result.Should().BeEquivalentTo(expected);
}
}
[TestClass]
public class IsValid
{
[TestMethod]
public void null_is_invalid() => Tests(null, false);
[TestMethod]
public void empty_is_valid() => Tests("", true);
[TestMethod]
public void whitespace_is_valid() => Tests(" ", true);
[TestMethod]
[DataRow(@"C:\", false)]
[DataRow(@"foo", true)]
[DataRow(@"\foo", false)]
[DataRow(@"/foo", false)]
[DataRow(@"<id>", true)]
public void Tests(string template, bool expected) => Templates.File.IsValid(template).Should().Be(expected);
}
// same as Templates.Folder.GetWarnings
//[TestClass]
//public class GetWarnings { }
// same as Templates.Folder.HasWarnings
//[TestClass]
//public class HasWarnings { }
// same as Templates.Folder.TagCount
//[TestClass]
//public class TagCount { }
}
namespace Templates_ChapterFile_Tests
{
// same as Templates.File.GetErrors
//[TestClass]
//public class GetErrors { }
// same as Templates.File.IsValid
//[TestClass]
//public class IsValid { }
[TestClass]
public class GetWarnings
{
[TestMethod]
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
[TestMethod]
public void empty_has_warnings() => Tests("", Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
[TestMethod]
public void whitespace_has_warnings() => Tests(" ", Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
[TestMethod]
[DataRow("<ch#>")]
[DataRow("<ch#> <id>")]
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
[TestMethod]
[DataRow(@"no tags", Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
[DataRow(@"<id>\foo\bar", Templates.ERROR_INVALID_FILE_NAME_CHAR, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
public void Tests(string template, params string[] expected)
{
var result = Templates.ChapterFile.GetWarnings(template);
result.Count().Should().Be(expected.Length);
result.Should().BeEquivalentTo(expected);
}
}
[TestClass]
public class HasWarnings
{
[TestMethod]
public void null_has_warnings() => Tests(null, true);
[TestMethod]
public void empty_has_warnings() => Tests("", true);
[TestMethod]
public void whitespace_has_warnings() => Tests(" ", true);
[TestMethod]
[DataRow(@"no tags", true)]
[DataRow(@"<id>\foo\bar", true)]
[DataRow("<ch#> <id>", false)]
[DataRow("<ch#> -- chapter tag", false)]
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", true)]
public void Tests(string template, bool expected) => Templates.ChapterFile.HasWarnings(template).Should().Be(expected);
}
[TestClass]
public class TagCount
{
[TestMethod]
public void null_is_not_recommended() => Assert.ThrowsException<NullReferenceException>(() => Tests(null, -1));
[TestMethod]
public void empty_is_not_recommended() => Tests("", 0);
[TestMethod]
public void whitespace_is_not_recommended() => Tests(" ", 0);
[TestMethod]
[DataRow("no tags", 0)]
[DataRow(@"<id>\foo\bar", 1)]
[DataRow("<id> <id>", 2)]
[DataRow("<id <id> >", 1)]
[DataRow("<id> <title>", 2)]
[DataRow("id> <title incomplete tags", 0)]
[DataRow("<not a real tag>", 0)]
[DataRow("<ch#> non-folder tag", 1)]
[DataRow("<ID> case specific", 0)]
public void Tests(string template, int expected) => Templates.ChapterFile.TagCount(template).Should().Be(expected);
}
[TestClass]
public class GetPortionFilename
{
[TestMethod]
[DataRow("asin", "[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt")]
[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)
=> Templates.ChapterFile.GetPortionFilename(GetLibraryBook(asin), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir)
.Should().Be(expected);
}
}

View File

@@ -7,8 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />

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)