Compare commits

...

18 Commits

Author SHA1 Message Date
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
Robert McRackan
0a986238bc Defensive FirstOrDefault 2021-10-21 15:39:53 -04:00
Robert McRackan
d636ceed8e File naming stuff is (finally) centralized under AudioFileStorageExt 2021-10-21 14:38:59 -04:00
Robert McRackan
e4fc104afe Naming logic for all new files can now originate from domian logic 2021-10-20 10:56:07 -04:00
Robert McRackan
87e3075fb3 Rename InternalUtilities to AudibleUtilities 2021-10-19 10:22:42 -04:00
67 changed files with 1129 additions and 349 deletions

View File

@@ -5,8 +5,8 @@
</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" />
<PackageReference Include="Dinah.Core" Version="2.0.2.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -12,10 +12,23 @@ namespace AaxDecrypter
{
protected override StepSequence Steps { get; }
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
private Func<string, int, int, NewSplitCallback, string> multipartFileNameCallback { get; }
private static string DefaultMultipartFilename(string outputFileName, int partsPosition, int partsTotal, NewSplitCallback newSplitCallback)
{
var template = Path.ChangeExtension(outputFileName, null) + " - <ch# 0> - <title>" + Path.GetExtension(outputFileName);
var fileTemplate = new FileTemplate(template) { IllegalCharacterReplacements = " " };
fileTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(partsPosition, partsTotal));
fileTemplate.AddParameterReplacement("title", newSplitCallback?.Chapter?.Title ?? "");
return fileTemplate.GetFilePath();
}
private 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)
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat,
Func<string, int, int, NewSplitCallback, string> multipartFileNameCallback = null)
: base(outFileName, cacheDirectory, dlLic, outputFormat)
{
Steps = new StepSequence
@@ -26,6 +39,7 @@ namespace AaxDecrypter
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
["Step 3: Cleanup"] = Step_Cleanup,
};
this.multipartFileNameCallback = multipartFileNameCallback ?? DefaultMultipartFilename;
}
/*
@@ -119,7 +133,9 @@ 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 = FileUtility.GetMultipartFileName(OutputFileName, currentChapter, splitChapters.Count, newSplitCallback.Chapter.Title);
var fileName = multipartFileNameCallback(OutputFileName, currentChapter, splitChapters.Count, newSplitCallback);
fileName = FileUtility.GetValidFilename(fileName);
multiPartFilePaths.Add(fileName);
FileUtility.SaferDelete(fileName);

View File

@@ -30,8 +30,7 @@ namespace AaxDecrypter
protected abstract StepSequence Steps { get; }
private NetworkFileStreamPersister nfsPersister;
private string cacheDir { get; }
private string jsonDownloadState => Path.Combine(cacheDir, Path.ChangeExtension(OutputFileName, ".json"));
private string jsonDownloadState { get; }
private string tempFile => Path.ChangeExtension(jsonDownloadState, ".tmp");
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
@@ -44,11 +43,12 @@ namespace AaxDecrypter
if (!Directory.Exists(cacheDirectory))
throw new DirectoryNotFoundException($"Directory does not exist: {nameof(cacheDirectory)}");
cacheDir = cacheDirectory;
jsonDownloadState = Path.Combine(cacheDirectory, Path.ChangeExtension(OutputFileName, ".json"));
DownloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
// delete file after validation is complete
FileUtility.SaferDelete(OutputFileName);
DownloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
}
public abstract void Cancel();

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Version>6.2.7.1</Version>
<Version>6.3.2.1</Version>
</PropertyGroup>
<ItemGroup>
@@ -15,7 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
</ItemGroup>
</Project>

View File

@@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.IO;
using Dinah.Core.Logging;
using InternalUtilities;
using LibationFileManager;
using Newtonsoft.Json.Linq;
using Serilog;
@@ -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>
@@ -211,10 +212,10 @@ namespace AppScaffolding
config.InProgress,
DownloadsInProgressDirectory = AudibleFileStorage.DownloadsInProgressDirectory,
AudibleFileStorage.DownloadsInProgressDirectory,
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
DecryptInProgressDirectory = AudibleFileStorage.DecryptInProgressDirectory,
AudibleFileStorage.DecryptInProgressDirectory,
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
});
}
@@ -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

@@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using AudibleUtilities;
using DataLayer;
using Dinah.Core;
using DtoImporterService;
using InternalUtilities;
using LibationFileManager;
using Serilog;
using static DtoImporterService.PerfLogger;

View File

@@ -6,7 +6,7 @@ using AudibleApi.Authorization;
using Dinah.Core;
using Newtonsoft.Json;
namespace InternalUtilities
namespace AudibleUtilities
{
public class Account : IUpdatable
{

View File

@@ -6,7 +6,7 @@ using AudibleApi.Authorization;
using Dinah.Core;
using Newtonsoft.Json;
namespace InternalUtilities
namespace AudibleUtilities
{
// 'AccountsSettings' is intentionally NOT IEnumerable<> so that properties can be added/extended
// from newtonsoft (https://www.newtonsoft.com/json/help/html/SerializationGuide.htm):

View File

@@ -3,7 +3,7 @@ using AudibleApi.Authorization;
using Dinah.Core.IO;
using Newtonsoft.Json;
namespace InternalUtilities
namespace AudibleUtilities
{
public class AccountsSettingsPersister : JsonFilePersister<AccountsSettings>
{

View File

@@ -8,7 +8,7 @@ using Dinah.Core;
using Polly;
using Polly.Retry;
namespace InternalUtilities
namespace AudibleUtilities
{
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
public class ApiExtended

View File

@@ -3,7 +3,7 @@ using System.IO;
using LibationFileManager;
using Newtonsoft.Json;
namespace InternalUtilities
namespace AudibleUtilities
{
public static class AudibleApiStorage
{

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
namespace InternalUtilities
namespace AudibleUtilities
{
public interface IValidator
{

View File

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

View File

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

View File

@@ -1,38 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace DataLayer.Utilities
{
public static class LocalDatabaseInfo
{
public static List<string> GetLocalDBInstances()
{
// Start the child process.
using var p = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
UseShellExecute = false,
RedirectStandardOutput = true,
FileName = "cmd.exe",
Arguments = "/C sqllocaldb info",
CreateNoWindow = true,
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
}
};
p.Start();
var output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
// if LocalDb is not installed then it will return that 'sqllocaldb' is not recognized as an internal or external command operable program or batch file
return string.IsNullOrWhiteSpace(output) || output.Contains("not recognized")
? new List<string>()
: output
.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)
.Select(i => i.Trim())
.Where(i => !string.IsNullOrEmpty(i))
.ToList();
}
}
}

View File

@@ -2,8 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
using AudibleUtilities;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{
@@ -68,7 +68,7 @@ namespace DtoImporterService
foreach (var item in importItems)
{
var book = DbContext.Books.Local.SingleOrDefault(p => p.AudibleProductId == item.DtoItem.ProductId);
var book = DbContext.Books.Local.FirstOrDefault(p => p.AudibleProductId == item.DtoItem.ProductId);
if (book is null)
{
book = createNewBook(item);

View File

@@ -2,8 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
using AudibleUtilities;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{

View File

@@ -2,8 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
using AudibleUtilities;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{

View File

@@ -5,8 +5,8 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleUtilities;
using DataLayer;
using Dinah.Core;
using InternalUtilities;
namespace DtoImporterService
{

View File

@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleUtilities;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{

View File

@@ -2,8 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
using AudibleUtilities;
using DataLayer;
using InternalUtilities;
namespace DtoImporterService
{

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DataLayer;
using Dinah.Core;
using FileManager;
using LibationFileManager;
namespace FileLiberator
{
public static class AudioFileStorageExt
{
private static void AddParameterReplacement(this FileTemplate fileTemplate, TemplateTags templateTags, object value)
=> fileTemplate.AddParameterReplacement(templateTags.TagName, value);
internal class MultipartRenamer
{
public LibraryBook libraryBook { get; }
public MultipartRenamer(LibraryBook libraryBook) => this.libraryBook = libraryBook;
internal string MultipartFilename(string outputFileName, int partsPosition, int partsTotal, AAXClean.NewSplitCallback newSplitCallback)
=> MultipartFilename(Configuration.Instance.ChapterFileTemplate, AudibleFileStorage.DecryptInProgressDirectory, Path.GetExtension(outputFileName), partsPosition, partsTotal, newSplitCallback?.Chapter?.Title ?? "");
internal string MultipartFilename(string template, string fullDirPath, string extension, int partsPosition, int partsTotal, string chapterTitle)
{
var fileTemplate = GetFileTemplateSingle(template, libraryBook, fullDirPath, extension);
fileTemplate.AddParameterReplacement(TemplateTags.ChCount, partsTotal);
fileTemplate.AddParameterReplacement(TemplateTags.ChNumber, partsPosition);
fileTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(partsPosition, partsTotal));
fileTemplate.AddParameterReplacement(TemplateTags.ChTitle, chapterTitle);
return fileTemplate.GetFilePath();
}
}
public static Func<string, int, int, AAXClean.NewSplitCallback, string> CreateMultipartRenamerFunc(this AudioFileStorage _, LibraryBook libraryBook)
=> new MultipartRenamer(libraryBook).MultipartFilename;
public static string GetInProgressFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
=> GetCustomDirFilename(_, libraryBook, AudibleFileStorage.DecryptInProgressDirectory, extension);
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
=> GetCustomDirFilename(_, libraryBook, AudibleFileStorage.BooksDirectory, extension);
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook)
=> GetFileTemplateSingle(Configuration.Instance.FolderTemplate, libraryBook, AudibleFileStorage.BooksDirectory, null)
.GetFilePath();
public static string GetCustomDirFilename(this AudioFileStorage _, LibraryBook libraryBook, string dirFullPath, string extension)
=> GetFileTemplateSingle(Configuration.Instance.FileTemplate, libraryBook, dirFullPath, extension)
.GetFilePath();
internal static FileTemplate GetFileTemplateSingle(string template, LibraryBook libraryBook, string dirFullPath, string extension)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension));
var fileTemplate = new FileTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
var title = libraryBook.Book.Title ?? "";
fileTemplate.AddParameterReplacement(TemplateTags.Id, libraryBook.Book.AudibleProductId);
fileTemplate.AddParameterReplacement(TemplateTags.Title, title);
fileTemplate.AddParameterReplacement(TemplateTags.TitleShort, title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':')));
fileTemplate.AddParameterReplacement(TemplateTags.Author, libraryBook.Book.AuthorNames);
fileTemplate.AddParameterReplacement(TemplateTags.FirstAuthor, libraryBook.Book.Authors.FirstOrDefault()?.Name);
fileTemplate.AddParameterReplacement(TemplateTags.Narrator, libraryBook.Book.NarratorNames);
fileTemplate.AddParameterReplacement(TemplateTags.FirstNarrator, libraryBook.Book.Narrators.FirstOrDefault()?.Name);
var seriesLink = libraryBook.Book.SeriesLink.FirstOrDefault();
fileTemplate.AddParameterReplacement(TemplateTags.Series, seriesLink?.Series.Name);
fileTemplate.AddParameterReplacement(TemplateTags.SeriesNumber, seriesLink?.Order);
return fileTemplate;
}
}
}

View File

@@ -59,13 +59,12 @@ namespace FileLiberator
FilePathCache.Removed -= FilePathCache_Removed;
}
// decrypt failed
if (!success)
return new StatusHandler { "Decrypt failed" };
// moves new files from temp dir to final dest
var movedAudioFile = moveFilesToBooksDir(libraryBook.Book, entries);
var movedAudioFile = moveFilesToBooksDir(libraryBook, entries);
// decrypt failed
if (!movedAudioFile)
@@ -115,14 +114,17 @@ namespace FileLiberator
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
}
var outFileName = FileUtility.GetValidFilename(AudibleFileStorage.DecryptInProgressDirectory, libraryBook.Book.Title, outputFormat.ToString().ToLower(), libraryBook.Book.AudibleProductId);
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, outputFormat.ToString().ToLower());
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
abDownloader
= contentLic.DrmType != AudibleApi.Common.DrmType.Adrm ? new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic)
: Configuration.Instance.SplitFilesByChapter ? new AaxcDownloadMultiConverter(outFileName, cacheDir, audiobookDlLic, outputFormat)
: 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);
@@ -134,7 +136,6 @@ namespace FileLiberator
// REAL WORK DONE HERE
var success = await Task.Run(abDownloader.Run);
return success;
}
finally
@@ -175,43 +176,35 @@ namespace FileLiberator
/// <summary>Move new files to 'Books' directory</summary>
/// <returns>True if audiobook file(s) were successfully created and can be located on disk. Else false.</returns>
private static bool moveFilesToBooksDir(Book book, List<FilePathCache.CacheEntry> entries)
private static bool moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
{
// create final directory. move each file into it
var title = book.Title;
var asin = book.AudibleProductId;
// 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 = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, asin);
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
Directory.CreateDirectory(destinationDir);
FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
if (getFirstAudio() == default)
return false;
if (getFirstAudio() == default)
return false;
for (var i = 0; i < entries.Count; i++)
{
var entry = entries[i];
for (var i = 0; i < entries.Count; i++)
{
var entry = entries[i];
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)));
FilePathCache.Insert(book.AudibleProductId, realDest);
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)));
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
// propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
entries[i] = entry with { Path = realDest };
}
// propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
entries[i] = entry with { Path = realDest };
}
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
if (cue != default)
Cue.UpdateFileName(cue.Path, getFirstAudio().Path);
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
if (cue != default)
Cue.UpdateFileName(cue.Path, getFirstAudio().Path);
AudibleFileStorage.Audio.Refresh();
AudibleFileStorage.Audio.Refresh();
return true;
}
}
return true;
}
}
}

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 FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, libraryBook.Book.Title, Path.GetExtension(file), libraryBook.Book.AudibleProductId);
return AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, extension);
}
private static string getdownloadUrl(LibraryBook libraryBook)

View File

@@ -7,7 +7,7 @@
<ItemGroup>
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
</ItemGroup>
</Project>

View File

@@ -19,7 +19,7 @@ namespace FileLiberator
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
{
var apiExtended = await InternalUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
var apiExtended = await AudibleUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
return apiExtended.Api;
}
}

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="2.0.2.1" />
<PackageReference Include="Polly" Version="7.2.2" />
</ItemGroup>

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core;
@@ -16,10 +15,12 @@ namespace FileManager
public FileTemplate(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;
@@ -28,7 +29,7 @@ namespace FileManager
public string IllegalCharacterReplacements { get; set; }
/// <summary>Generate a valid path for this file or directory</summary>
public string GetFilename()
public string GetFilePath()
{
var filename = Template;
@@ -38,14 +39,14 @@ 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)
private string formatValue(object value)
=> ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0
? value?.Truncate(ParameterMaxSize.Value)
: value;
? value?.ToString().Truncate(ParameterMaxSize.Value)
: value?.ToString();
}
}

View File

@@ -18,43 +18,24 @@ namespace FileManager
public static string GetStandardizedExtension(string extension)
=> string.IsNullOrWhiteSpace(extension)
? (extension ?? "")?.Trim()
: '.' + extension.Trim('.');
: '.' + extension.Trim().Trim('.');
public static string GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix)
/// <summary>
/// Return position with correct number of leading zeros.
/// <br />- 2 of 9 => "2"
/// <br />- 2 of 90 => "02"
/// <br />- 2 of 900 => "002"
/// </summary>
/// <param name="position">position in sequence. The 'x' in 'x of y'</param>
/// <param name="total">total qty in sequence. The 'y' in 'x of y'</param>
public static string GetSequenceFormatted(int position, int total)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(filename, nameof(filename));
ArgumentValidator.EnsureGreaterThan(position, nameof(position), 0);
ArgumentValidator.EnsureGreaterThan(total, nameof(total), 0);
if (position > total)
throw new ArgumentException($"{position} may not be greater than {total}");
var template = $"<title> [<id>]";
var fullfilename = Path.Combine(dirFullPath, template + GetStandardizedExtension(extension));
var fileTemplate = new FileTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
fileTemplate.AddParameterReplacement("title", filename);
fileTemplate.AddParameterReplacement("id", metadataSuffix);
return fileTemplate.GetFilename();
}
public static string GetMultipartFileName(string originalPath, int partsPosition, int partsTotal, string suffix)
{
ArgumentValidator.EnsureNotNull(originalPath, nameof(originalPath));
ArgumentValidator.EnsureGreaterThan(partsPosition, nameof(partsPosition), 0);
ArgumentValidator.EnsureGreaterThan(partsTotal, nameof(partsTotal), 0);
if (partsPosition > partsTotal)
throw new ArgumentException($"{partsPosition} may not be greater than {partsTotal}");
// 1-9 => 1-9
// 10-99 => 01-99
// 100-999 => 001-999
var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
var template = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath);
var fileTemplate = new FileTemplate(template) { IllegalCharacterReplacements = " " };
fileTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
fileTemplate.AddParameterReplacement("title", suffix);
return fileTemplate.GetFilename();
return position.ToString().PadLeft(total.ToString().Length, '0');
}
private const int MAX_FILENAME_LENGTH = 255;
@@ -105,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
@@ -122,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

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

View File

@@ -29,7 +29,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataLayer", "DataLayer\Data
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator", "FileLiberator\FileLiberator.csproj", "{393B5B27-D15C-4F77-9457-FA14BA8F3C73}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities", "InternalUtilities\InternalUtilities.csproj", "{06882742-27A6-4347-97D9-56162CEC9C11}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleUtilities", "AudibleUtilities\AudibleUtilities.csproj", "{06882742-27A6-4347-97D9-56162CEC9C11}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 Domain Internal Utilities (db ignorant)", "3 Domain Internal Utilities (db ignorant)", "{F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}"
EndProject
@@ -46,8 +46,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "Appl
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Tests", "_Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine.Tests", "_Tests\LibationSearchEngine.Tests\LibationSearchEngine.Tests.csproj", "{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
@@ -56,9 +54,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationCli", "LibationCli\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppScaffolding", "AppScaffolding\AppScaffolding.csproj", "{595E7C4D-506D-486D-98B7-5FDDF398D033}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileManager", "FileManager\FileManager.csproj", "{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager", "FileManager\FileManager.csproj", "{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileManager.Tests", "FileManager.Tests\FileManager.Tests.csproj", "{3B58450C-FBDA-4D48-8418-A3C750596D7D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleUtilities.Tests", "_Tests\AudibleUtilities.Tests\AudibleUtilities.Tests.csproj", "{788294BE-0D8E-40D4-9CEE-67896FBB52CE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator.Tests", "_Tests\FileLiberator.Tests\FileLiberator.Tests.csproj", "{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}"
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
@@ -102,10 +106,6 @@ Global
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.Build.0 = Release|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.Build.0 = Release|Any CPU
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -126,10 +126,22 @@ Global
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Release|Any CPU.Build.0 = Release|Any CPU
{3B58450C-FBDA-4D48-8418-A3C750596D7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B58450C-FBDA-4D48-8418-A3C750596D7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B58450C-FBDA-4D48-8418-A3C750596D7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B58450C-FBDA-4D48-8418-A3C750596D7D}.Release|Any CPU.Build.0 = Release|Any CPU
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Release|Any CPU.Build.0 = Release|Any CPU
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Release|Any CPU.Build.0 = Release|Any CPU
{F2E04270-4551-41C4-99FF-E7125BED708C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
@@ -144,13 +156,15 @@ Global
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{428163C3-D558-4914-B570-A92069521877} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{595E7C4D-506D-486D-98B7-5FDDF398D033} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
{3B58450C-FBDA-4D48-8418-A3C750596D7D} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{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

@@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using AudibleUtilities;
using CommandLine;
using InternalUtilities;
namespace LibationCli
{

View File

@@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using AudibleUtilities;
using CommandLine;
using InternalUtilities;
namespace LibationCli
{

View File

@@ -66,8 +66,26 @@ namespace LibationFileManager
#endregion
}
internal class AaxcFileStorage : AudibleFileStorage
{
internal AaxcFileStorage() : base(FileType.AAXC) { }
protected override string GetFilePathCustom(string productId)
{
var regex = GetBookSearchRegex(productId);
return Directory
.EnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
.FirstOrDefault(s => regex.IsMatch(s));
}
public bool Exists(string productId) => GetFilePath(productId) != null;
}
public class AudioFileStorage : AudibleFileStorage
{
internal AudioFileStorage() : base(FileType.Audio)
=> BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
private static BackgroundFileSystem BookDirectoryFiles { get; set; }
private static object bookDirectoryFilesLocker { get; } = new();
protected override string GetFilePathCustom(string productId)
@@ -81,26 +99,8 @@ namespace LibationFileManager
return BookDirectoryFiles.FindFile(regex);
}
internal AudioFileStorage() : base(FileType.Audio)
=> BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
public void Refresh() => BookDirectoryFiles.RefreshFiles();
public string GetPath(string productId) => GetFilePath(productId);
}
internal class AaxcFileStorage : AudibleFileStorage
{
protected override string GetFilePathCustom(string productId)
{
var regex = GetBookSearchRegex(productId);
return Directory
.EnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
.FirstOrDefault(s => regex.IsMatch(s));
}
internal AaxcFileStorage() : base(FileType.AAXC) { }
public bool Exists(string productId) => GetFilePath(productId) != null;
}
}

View File

@@ -145,8 +145,44 @@ 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)
{
var value = persistentDictionary.GetString(settingName).Trim();
return templ.IsValid(value) ? value : templ.DefaultTemplate;
}
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

@@ -0,0 +1,37 @@
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;
}
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 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);
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationFileManager
{
public abstract class Templates
{
public static Templates Folder { get; } = new FolderTemplate();
public static Templates File { get; } = new FileTemplate();
public static Templates ChapterFile { get; } = new ChapterFileTemplate();
public abstract string DefaultTemplate { get; }
public abstract bool IsValid(string template);
public abstract bool IsRecommended(string template);
public abstract int TagCount(string template);
public static bool ContainsChapterOnlyTags(string template)
=> TemplateTags.GetAll()
.Where(t => t.IsChapterOnly)
.Any(t => ContainsTag(template, t.TagName));
public static bool ContainsTag(string template, string tag) => template.Contains($"<{tag}>");
protected static bool fileIsValid(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
=> template is not null
&& !template.Contains(':')
&& !template.Contains(System.IO.Path.DirectorySeparatorChar)
&& !template.Contains(System.IO.Path.AltDirectorySeparatorChar);
protected bool isRecommended(string template, bool isChapter)
=> IsValid(template)
&& !string.IsNullOrWhiteSpace(template)
&& TagCount(template) > 0
&& ContainsChapterOnlyTags(template) == isChapter;
protected static int tagCount(string template, Func<TemplateTags, bool> func)
=> TemplateTags.GetAll()
.Where(func)
// for <id><id> == 1, use:
// .Count(t => template.Contains($"<{t.TagName}>"))
// .Sum() impl: <id><id> == 2
.Sum(t => template.Split($"<{t.TagName}>").Length - 1);
private class FolderTemplate : Templates
{
public override string DefaultTemplate { get; } = "<title short> [<id>]";
public override bool IsValid(string template)
// must be relative. no colons. 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
=> template is not null
&& !template.Contains(':');
public override bool IsRecommended(string template) => isRecommended(template, false);
public override int TagCount(string template) => tagCount(template, t => !t.IsChapterOnly);
}
private class FileTemplate : Templates
{
public override string DefaultTemplate { get; } = "<title> [<id>]";
public override bool IsValid(string template) => fileIsValid(template);
public override bool IsRecommended(string template) => isRecommended(template, false);
public override int TagCount(string template) => tagCount(template, t => !t.IsChapterOnly);
}
private class ChapterFileTemplate : Templates
{
public override string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
public override bool IsValid(string template) => fileIsValid(template);
public override bool IsRecommended(string template)
=> isRecommended(template, true)
// recommended to incl. <ch#> or <ch# 0>
&& (ContainsTag(template, TemplateTags.ChNumber.TagName) || ContainsTag(template, TemplateTags.ChNumber0.TagName));
public override int TagCount(string template) => tagCount(template, t => true);
}
}
}

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

@@ -1,9 +1,9 @@
using AudibleApi;
using InternalUtilities;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using AudibleApi;
using AudibleUtilities;
namespace LibationWinForms.Dialogs
{

View File

@@ -1,9 +1,8 @@
using ApplicationServices;
using InternalUtilities;
using LibationWinForms.Login;
using System;
using System.Linq;
using System;
using System.Windows.Forms;
using ApplicationServices;
using AudibleUtilities;
using LibationWinForms.Login;
namespace LibationWinForms.Dialogs
{

View File

@@ -1,7 +1,7 @@
using System;
using System.Windows.Forms;
using AudibleUtilities;
using Dinah.Core;
using InternalUtilities;
namespace LibationWinForms.Dialogs.Login
{

View File

@@ -1,7 +1,7 @@
using System;
using System.Windows.Forms;
using AudibleUtilities;
using Dinah.Core;
using InternalUtilities;
namespace LibationWinForms.Dialogs.Login
{

View File

@@ -1,7 +1,7 @@
using System;
using System.Windows.Forms;
using AudibleUtilities;
using Dinah.Core;
using InternalUtilities;
namespace LibationWinForms.Dialogs.Login
{

View File

@@ -1,6 +1,6 @@
using System;
using AudibleApi;
using InternalUtilities;
using AudibleUtilities;
using LibationWinForms.Dialogs.Login;
namespace LibationWinForms.Login

View File

@@ -1,6 +1,6 @@
using System;
using AudibleApi;
using InternalUtilities;
using AudibleUtilities;
using LibationWinForms.Dialogs.Login;
namespace LibationWinForms.Login

View File

@@ -1,16 +1,16 @@
using ApplicationServices;
using DataLayer;
using Dinah.Core.DataBinding;
using InternalUtilities;
using LibationFileManager;
using LibationWinForms.Login;
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Windows.Forms;
using ApplicationServices;
using AudibleUtilities;
using DataLayer;
using Dinah.Core.DataBinding;
using LibationFileManager;
using LibationWinForms.Login;
namespace LibationWinForms.Dialogs
{

View File

@@ -1,8 +1,8 @@
using InternalUtilities;
using System;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Windows.Forms;
using AudibleUtilities;
namespace LibationWinForms.Dialogs
{

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,64 @@ 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)
{
TEMP_TEMP_TEMP();
}
private void fileTemplateBtn_Click(object sender, EventArgs e)
{
TEMP_TEMP_TEMP();
}
private void chapterFileTemplateBtn_Click(object sender, EventArgs e)
{
TEMP_TEMP_TEMP();
}
private void TEMP_TEMP_TEMP()
=> MessageBox.Show("Sorry, not yet. Coming soon :)");
private void saveBtn_Click(object sender, EventArgs e)
{
var newBooks = booksSelectControl.SelectedDirectory;
#region validation
if (string.IsNullOrWhiteSpace(newBooks))
{
MessageBox.Show("Cannot set Books Location to blank", "Location is blank", MessageBoxButtons.OK, MessageBoxIcon.Error);
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);
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 (!Templates.Folder.IsValid(folderTemplateTb.Text))
{
MessageBox.Show($"Not saving change to folder naming template. Invalid format.", "Invalid folder template", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (!Templates.File.IsValid(fileTemplateTb.Text))
{
MessageBox.Show($"Not saving change to file naming template. Invalid format.", "Invalid file template", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (!Templates.ChapterFile.IsValid(chapterFileTemplateTb.Text))
{
MessageBox.Show($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
#endregion
if (!Directory.Exists(newBooks) && booksSelectControl.SelectedDirectoryIsKnown)
Directory.CreateDirectory(newBooks);
config.Books = newBooks;
{
@@ -145,6 +189,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

@@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using ApplicationServices;
using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.Drawing;
using Dinah.Core.Threading;
using InternalUtilities;
using LibationFileManager;
using LibationWinForms.Dialogs;

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.0.2.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,9 +4,9 @@ using System.IO;
using System.Linq;
using System.Windows.Forms;
using AudibleApi.Authorization;
using AudibleUtilities;
using DataLayer;
using Dinah.Core;
using InternalUtilities;
using LibationFileManager;
using LibationWinForms.Dialogs;
using Microsoft.EntityFrameworkCore;

View File

@@ -8,10 +8,10 @@ using System.Threading;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi.Authorization;
using AudibleUtilities;
using Dinah.Core;
using FluentAssertions;
using FluentAssertions.Common;
using InternalUtilities;
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

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" />
@@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\InternalUtilities\InternalUtilities.csproj" />
<ProjectReference Include="..\..\AudibleUtilities\AudibleUtilities.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dinah.Core;
using FileLiberator;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static AudioFileStorageExtTests.Shared;
namespace AudioFileStorageExtTests
{
public static class Shared
{
public static DataLayer.LibraryBook GetLibraryBook(string asin)
{
var book = new DataLayer.Book(new DataLayer.AudibleProductId(asin), "title", "desc", 1, DataLayer.ContentType.Product, new List<DataLayer.Contributor> { new DataLayer.Contributor("author") }, new List<DataLayer.Contributor> { new DataLayer.Contributor("narrator") }, new DataLayer.Category(new DataLayer.AudibleCategoryId("seriesId"), "name"), "us");
var libraryBook = new DataLayer.LibraryBook(book, DateTime.Now, "my us");
return libraryBook;
}
}
[TestClass]
public class MultipartRenamer_MultipartFilename
{
[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)
=> new AudioFileStorageExt.MultipartRenamer(GetLibraryBook(asin))
.MultipartFilename(template, dir, ext, pos, total, chapter)
.Should().Be(expected);
}
[TestClass]
public class GetFileTemplateSingle
{
[TestMethod]
[DataRow(null, "asin", @"C:\", "ext")]
[DataRow("f.txt", null, @"C:\", "ext")]
[DataRow("f.txt", "asin", null, "ext")]
[ExpectedException(typeof(ArgumentNullException))]
public void arg_null_exception(string template, string asin, string dirFullPath, string extension)
=> AudioFileStorageExt.GetFileTemplateSingle(template, GetLibraryBook(asin), dirFullPath, extension);
[TestMethod]
[DataRow("", "asin", @"C:\foo\bar", "ext")]
[DataRow(" ", "asin", @"C:\foo\bar", "ext")]
[DataRow("f.txt", "", @"C:\foo\bar", "ext")]
[DataRow("f.txt", " ", @"C:\foo\bar", "ext")]
[DataRow("f.txt", "asin", "", "ext")]
[DataRow("f.txt", "asin", " ", "ext")]
[ExpectedException(typeof(ArgumentException))]
public void arg_exception(string template, string asin, string dirFullPath, string extension)
=> AudioFileStorageExt.GetFileTemplateSingle(template, GetLibraryBook(asin), dirFullPath, extension);
[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)
=> AudioFileStorageExt.GetFileTemplateSingle(template, GetLibraryBook(asin), dirFullPath, extension)
.GetFilePath()
.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="..\..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>
</Project>

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">
@@ -18,7 +18,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FileManager\FileManager.csproj" />
<ProjectReference Include="..\..\FileManager\FileManager.csproj" />
</ItemGroup>
</Project>

View File

@@ -10,7 +10,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FileTemplateTests
{
[TestClass]
public class GetFilename
public class GetFilePath
{
[TestMethod]
public void equiv_GetValidFilename()
@@ -58,7 +58,7 @@ namespace FileTemplateTests
var fileTemplate = new FileTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
fileTemplate.AddParameterReplacement("title", filename);
fileTemplate.AddParameterReplacement("id", metadataSuffix);
return fileTemplate.GetFilename();
return fileTemplate.GetFilePath();
}
[TestMethod]
@@ -102,7 +102,7 @@ namespace FileTemplateTests
fileTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
fileTemplate.AddParameterReplacement("title", suffix);
return fileTemplate.GetFilename();
return fileTemplate.GetFilePath();
}
}
}

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));
}
@@ -55,76 +58,59 @@ namespace FileUtilityTests
}
[TestClass]
public class GetValidFilename
public class GetSequenceFormatted
{
[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)
=> FileUtility.GetValidFilename(dirFullPath, filename, extension, 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)
=> FileUtility.GetValidFilename(dirFullPath, filename, extension, metadataSuffix);
[TestMethod]
public void null_extension() => Tests(@"C:\foo\bar", "my file", null, "meta", @"C:\foo\bar\my file [meta]");
[TestMethod]
public void null_metadataSuffix() => Tests(@"C:\foo\bar", "my file", "txt", null, @"C:\foo\bar\my file [].txt");
[TestMethod]
[DataRow(@"C:\foo\bar", "my file", "txt", "my id", @"C:\foo\bar\my file [my id].txt")]
[DataRow(@"C:\foo\bar", "my file", "txt", "", @"C:\foo\bar\my file [].txt")]
public void Tests(string dirFullPath, string filename, string extension, string metadataSuffix, string expected)
=> FileUtility.GetValidFilename(dirFullPath, filename, extension, metadataSuffix).Should().Be(expected);
}
[TestClass]
public class GetMultipartFileName
{
[TestMethod]
public void null_path() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetMultipartFileName(null, 1, 1, ""));
[TestMethod]
public void null_suffix() => Tests(@"C:\foo\bar\my file.txt", 2, 100, null, @"C:\foo\bar\my file - 002 - .txt");
[TestMethod]
public void negative_partsPosition() => Assert.ThrowsException<ArgumentException>(()
=> FileUtility.GetMultipartFileName("foo", -1, 2, "")
=> FileUtility.GetSequenceFormatted(-1, 2)
);
[TestMethod]
public void zero_partsPosition() => Assert.ThrowsException<ArgumentException>(()
=> FileUtility.GetMultipartFileName("foo", 0, 2, "")
=> FileUtility.GetSequenceFormatted(0, 2)
);
[TestMethod]
public void negative_partsTotal() => Assert.ThrowsException<ArgumentException>(()
=> FileUtility.GetMultipartFileName("foo", 2, -1, "")
=> FileUtility.GetSequenceFormatted(2, -1)
);
[TestMethod]
public void zero_partsTotal() => Assert.ThrowsException<ArgumentException>(()
=> FileUtility.GetMultipartFileName("foo", 2, 0, "")
=> FileUtility.GetSequenceFormatted(2, 0)
);
[TestMethod]
public void partsPosition_greater_than_partsTotal() => Assert.ThrowsException<ArgumentException>(()
=> FileUtility.GetMultipartFileName("foo", 2, 1, "")
=> FileUtility.GetSequenceFormatted(2, 1)
);
[TestMethod]
// only part
[DataRow(@"C:\foo\bar\my file.txt", 1, 1, "title", @"C:\foo\bar\my file - 1 - title.txt")]
[DataRow(1, 1, "1")]
// 2 digits
[DataRow(2, 90, "02")]
// 3 digits
[DataRow(@"C:\foo\bar\my file.txt", 2, 100, "title", @"C:\foo\bar\my file - 002 - title.txt")]
// no suffix
[DataRow(@"C:\foo\bar\my file.txt", 2, 100, "", @"C:\foo\bar\my file - 002 - .txt")]
public void Tests(string originalPath, int partsPosition, int partsTotal, string suffix, string expected)
=> FileUtility.GetMultipartFileName(originalPath, partsPosition, partsTotal, suffix).Should().Be(expected);
[DataRow(2, 900, "002")]
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,189 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core;
using FluentAssertions;
using LibationFileManager;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TemplatesTests
{
[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);
}
}
namespace Templates_Folder_Tests
{
[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 IsRecommended
{
[TestMethod]
public void null_is_not_recommended() => Tests(null, false);
[TestMethod]
public void empty_is_not_recommended() => Tests("", false);
[TestMethod]
public void whitespace_is_not_recommended() => Tests(" ", false);
[TestMethod]
[DataRow(@"no tags", false)]
[DataRow(@"<id>\foo\bar", true)]
[DataRow("<ch#> <id>", false)]
[DataRow("<ch#> chapter tag", false)]
public void Tests(string template, bool expected) => Templates.Folder.IsRecommended(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", 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 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.IsRecommended
//[TestClass]
//public class IsRecommended { }
// same as Templates.Folder.TagCount
//[TestClass]
//public class TagCount { }
}
namespace Templates_ChapterFile_Tests
{
// same as Templates.File.IsValid
//[TestClass]
//public class IsValid { }
[TestClass]
public class IsRecommended
{
[TestMethod]
public void null_is_not_recommended() => Tests(null, false);
[TestMethod]
public void empty_is_not_recommended() => Tests("", false);
[TestMethod]
public void whitespace_is_not_recommended() => Tests(" ", false);
[TestMethod]
[DataRow(@"no tags", false)]
[DataRow(@"<id>\foo\bar", false)]
[DataRow("<ch#> <id>", true)]
[DataRow("<ch#> -- chapter tag", true)]
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", false)]
public void Tests(string template, bool expected) => Templates.ChapterFile.IsRecommended(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);
}
}

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" />