Custom illegal character replacement

This commit is contained in:
Michael Bucari-Tovo 2022-06-23 13:01:24 -06:00
parent 184ba84600
commit 2ab466c570
24 changed files with 838 additions and 218 deletions

View File

@ -141,7 +141,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea
private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties) private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
{ {
var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties); var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties);
fileName = FileUtility.GetValidFilename(fileName); fileName = FileUtility.GetValidFilename(fileName, DownloadOptions.ReplacementCharacters);
multiPartFilePaths.Add(fileName); multiPartFilePaths.Add(fileName);

View File

@ -103,7 +103,7 @@ namespace AaxDecrypter
try try
{ {
var path = Path.ChangeExtension(OutputFileName, ".cue"); var path = Path.ChangeExtension(OutputFileName, ".cue");
path = FileUtility.GetValidFilename(path); path = FileUtility.GetValidFilename(path, DownloadOptions.ReplacementCharacters);
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo)); File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo));
OnFileCreated(path); OnFileCreated(path);
} }

View File

@ -4,6 +4,7 @@ namespace AaxDecrypter
{ {
public interface IDownloadOptions public interface IDownloadOptions
{ {
FileManager.ReplacementCharacters ReplacementCharacters { get; }
string DownloadUrl { get; } string DownloadUrl { get; }
string UserAgent { get; } string UserAgent { get; }
string AudibleKey { get; } string AudibleKey { get; }

View File

@ -11,15 +11,5 @@ namespace AaxDecrypter
public int PartsTotal { get; set; } public int PartsTotal { get; set; }
public string Title { 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

@ -68,7 +68,7 @@ namespace AaxDecrypter
CloseInputFileStream(); CloseInputFileStream();
var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName); var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName, DownloadOptions.ReplacementCharacters);
SetOutputFileName(realOutputFileName); SetOutputFileName(realOutputFileName);
OnFileCreated(realOutputFileName); OnFileCreated(realOutputFileName);

View File

@ -117,6 +117,9 @@ namespace AppScaffolding
if (!config.Exists(nameof(config.DownloadEpisodes))) if (!config.Exists(nameof(config.DownloadEpisodes)))
config.DownloadEpisodes = true; config.DownloadEpisodes = true;
if (!config.Exists(nameof(config.ReplacementCharacters)))
config.ReplacementCharacters = FileManager.ReplacementCharacters.Default;
if (!config.Exists(nameof(config.FolderTemplate))) if (!config.Exists(nameof(config.FolderTemplate)))
config.FolderTemplate = Templates.Folder.DefaultTemplate; config.FolderTemplate = Templates.Folder.DefaultTemplate;

View File

@ -70,7 +70,7 @@ namespace FileLiberator
return new StatusHandler { "Cancelled" }; return new StatusHandler { "Cancelled" };
} }
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path); var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters);
OnFileCreated(libraryBook, realMp3Path); OnFileCreated(libraryBook, realMp3Path);
} }
return new StatusHandler(); return new StatusHandler();

View File

@ -258,7 +258,7 @@ namespace FileLiberator
{ {
var entry = entries[i]; var entry = entries[i];
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path))); var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters);
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest); FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
// propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) // propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)

View File

@ -3,6 +3,7 @@ using AAXClean;
using Dinah.Core; using Dinah.Core;
using DataLayer; using DataLayer;
using LibationFileManager; using LibationFileManager;
using FileManager;
namespace FileLiberator namespace FileLiberator
{ {
@ -23,6 +24,7 @@ namespace FileLiberator
public NAudio.Lame.LameConfig LameConfig { get; set; } public NAudio.Lame.LameConfig LameConfig { get; set; }
public bool Downsample { get; set; } public bool Downsample { get; set; }
public bool MatchSourceBitrate { get; set; } public bool MatchSourceBitrate { get; set; }
public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters;
public string GetMultipartFileName(MultiConvertFileProperties props) public string GetMultipartFileName(MultiConvertFileProperties props)
=> Templates.ChapterFile.GetFilename(LibraryBookDto, props); => Templates.ChapterFile.GetFilename(LibraryBookDto, props);

View File

@ -79,8 +79,13 @@ namespace FileManager
//Stop raising events //Stop raising events
fileSystemWatcher?.Dispose(); fileSystemWatcher?.Dispose();
try
{
//Calling CompleteAdding() will cause background scanner to terminate. //Calling CompleteAdding() will cause background scanner to terminate.
directoryChangesEvents?.CompleteAdding(); directoryChangesEvents?.CompleteAdding();
}
// if directoryChangesEvents is non-null and isDisposed, this exception is thrown. there's no other way to check >:(
catch (ObjectDisposedException) { }
//Wait for background scanner to terminate before reinitializing. //Wait for background scanner to terminate before reinitializing.
backgroundScanner?.Wait(); backgroundScanner?.Wait();

View File

@ -12,17 +12,13 @@ namespace FileManager
/// <param name="template">Proposed file name with optional html-styled template tags.</param> /// <param name="template">Proposed file name with optional html-styled template tags.</param>
public FileNamingTemplate(string template) : base(template) { } public FileNamingTemplate(string template) : base(template) { }
/// <summary>Optional step 2: Replace all illegal characters with this. Default=<see cref="string.Empty"/></summary>
public string IllegalCharacterReplacements { get; set; }
/// <summary>Generate a valid path for this file or directory</summary> /// <summary>Generate a valid path for this file or directory</summary>
public LongPath GetFilePath(bool returnFirstExisting = false) public LongPath GetFilePath(ReplacementCharacters replacements, bool returnFirstExisting = false)
{ {
string fileName = Template.EndsWith(Path.DirectorySeparatorChar) ? Template[..^1] : Template; string fileName = Template.EndsWith(Path.DirectorySeparatorChar) ? Template[..^1] : Template;
List<string> pathParts = new(); List<string> pathParts = new();
var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value)); var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, replacements));
while (!string.IsNullOrEmpty(fileName)) while (!string.IsNullOrEmpty(fileName))
{ {
@ -43,7 +39,7 @@ namespace FileManager
pathParts.Reverse(); pathParts.Reverse();
return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), IllegalCharacterReplacements, returnFirstExisting); return FileUtility.GetValidFilename(Path.Join(pathParts.ToArray()), replacements, returnFirstExisting);
} }
private string replaceFileName(string filename, Dictionary<string,string> paramReplacements) private string replaceFileName(string filename, Dictionary<string,string> paramReplacements)
@ -92,17 +88,14 @@ namespace FileManager
return string.Join("", filenameParts); return string.Join("", filenameParts);
} }
private string formatValue(object value) private string formatValue(object value, ReplacementCharacters replacements)
{ {
if (value is null) if (value is null)
return ""; return "";
// Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders. // 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. // Esp important for file templates.
return value return replacements.ReplaceInvalidFilenameChars(value.ToString());
.ToString()
.Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements)
.Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements);
} }
} }
} }

View File

@ -46,12 +46,12 @@ namespace FileManager
/// <br/>- ensure uniqueness /// <br/>- ensure uniqueness
/// <br/>- enforce max file length /// <br/>- enforce max file length
/// </summary> /// </summary>
public static LongPath GetValidFilename(LongPath path, string illegalCharacterReplacements = "", bool returnFirstExisting = false) public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, bool returnFirstExisting = false)
{ {
ArgumentValidator.EnsureNotNull(path, nameof(path)); ArgumentValidator.EnsureNotNull(path, nameof(path));
// remove invalid chars // remove invalid chars
path = GetSafePath(path, illegalCharacterReplacements); path = GetSafePath(path, replacements);
// ensure uniqueness and check lengths // ensure uniqueness and check lengths
var dir = Path.GetDirectoryName(path); var dir = Path.GetDirectoryName(path);
@ -77,36 +77,19 @@ namespace FileManager
return fullfilename; return fullfilename;
} }
// GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/'
/// <summary>Use with file name, not full path. Valid path characters which are invalid file name characters will be replaced: ':', '\\', '/'</summary>
public static string GetSafeFileName(string str, string illegalCharacterReplacements = "")
=> string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars()));
/// <summary>Use with full path, not file name. Valid path characters which are invalid file name characters will be retained: '\\', '/'</summary> /// <summary>Use with full path, not file name. Valid path characters which are invalid file name characters will be retained: '\\', '/'</summary>
public static LongPath GetSafePath(LongPath path, string illegalCharacterReplacements = "") public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements)
{ {
ArgumentValidator.EnsureNotNull(path, nameof(path)); ArgumentValidator.EnsureNotNull(path, nameof(path));
var pathNoPrefix = path.PathWithoutPrefix; var pathNoPrefix = path.PathWithoutPrefix;
pathNoPrefix = replaceColons(pathNoPrefix, ""); pathNoPrefix = replacements.ReplaceInvalidChars(pathNoPrefix);
pathNoPrefix = replaceIllegalWithUnicodeAnalog(pathNoPrefix);
pathNoPrefix = replaceInvalidChars(pathNoPrefix, illegalCharacterReplacements);
pathNoPrefix = removeDoubleSlashes(pathNoPrefix); pathNoPrefix = removeDoubleSlashes(pathNoPrefix);
return pathNoPrefix; return pathNoPrefix;
} }
private static char[] invalidChars { get; } = Path.GetInvalidPathChars().Union(new[] {
'*', '?',
// these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included.
// In live code, Path.GetInvalidPathChars() does not include them
'"', '<', '>'
}).ToArray();
private static string replaceInvalidChars(string path, string illegalCharacterReplacements)
=> string.Join(illegalCharacterReplacements ?? "", path.Split(invalidChars));
private static string removeDoubleSlashes(string path) private static string removeDoubleSlashes(string path)
{ {
if (path.Length < 2) if (path.Length < 2)
@ -122,60 +105,6 @@ namespace FileManager
return path[0] + remainder; return path[0] + remainder;
} }
private static string replaceIllegalWithUnicodeAnalog(string path)
{
char[] replaced = path.ToCharArray();
char GetQuote(int position)
{
if (
position == 0
|| (position > 0
&& position < replaced.Length
&& !char.IsLetter(replaced[position - 1])
&& !char.IsNumber(replaced[position - 1])
)
) return '“';
else if (
position == replaced.Length - 1
|| (position >= 0
&& position < replaced.Length - 1
&& !char.IsLetter(replaced[position + 1])
&& !char.IsNumber(replaced[position + 1])
)
) return '”';
else return '';
}
for (int i = 0; i < replaced.Length; i++)
{
replaced[i] = replaced[i] switch
{
'?' => '',
'*' => '✱',
'<' => '',
'>' => '',
'"' => GetQuote(i),
_ => replaced[i]
};
}
return new string(replaced);
}
private static string replaceColons(string path, string illegalCharacterReplacements)
{
// replace all colons except within the first 2 chars
var builder = new System.Text.StringBuilder();
for (var i = 0; i < path.Length; i++)
{
var c = path[i];
if (i >= 2 && c == ':')
builder.Append(illegalCharacterReplacements);
else
builder.Append(c);
}
return builder.ToString();
}
private static string removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*"; private static string removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*";
private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
@ -206,9 +135,9 @@ namespace FileManager
/// <br/>- Perform <see cref="SaferMove"/> /// <br/>- Perform <see cref="SaferMove"/>
/// <br/>- Return valid path /// <br/>- Return valid path
/// </summary> /// </summary>
public static string SaferMoveToValidPath(LongPath source, LongPath destination) public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements)
{ {
destination = GetValidFilename(destination); destination = GetValidFilename(destination, replacements);
SaferMove(source, destination); SaferMove(source, destination);
return destination; return destination;
} }

View File

@ -0,0 +1,269 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace FileManager
{
public class Replacement
{
public const int FIXED_COUNT = 6;
internal const char QUOTE_MARK = '"';
[JsonIgnore] public bool Mandatory { get; internal set; }
[JsonProperty] public char CharacterToReplace { get; private set; }
[JsonProperty] public string ReplacementString { get; private set; }
[JsonProperty] public string Description { get; private set; }
public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})";
public Replacement(char charToReplace, string replacementString, string description)
{
CharacterToReplace = charToReplace;
ReplacementString = replacementString;
Description = description;
}
private Replacement(char charToReplace, string replacementString, string description, bool mandatory)
: this(charToReplace, replacementString, description)
{
Mandatory = mandatory;
}
public void Update(char charToReplace, string replacementString, string description)
{
ReplacementString = replacementString;
if (!Mandatory)
{
CharacterToReplace = charToReplace;
Description = description;
}
}
public static Replacement OtherInvalid(string replacement) => new(default, replacement, "All other invalid characters", true);
public static Replacement FilenameForwardSlash(string replacement) => new('/', replacement, "Forward Slash (Filename Only)", true);
public static Replacement FilenameBackSlash(string replacement) => new('\\', replacement, "Back Slash (Filename Only)", true);
public static Replacement OpenQuote(string replacement) => new('"', replacement, "Open Quote", true);
public static Replacement CloseQuote(string replacement) => new('"', replacement, "Close Quote", true);
public static Replacement OtherQuote(string replacement) => new('"', replacement, "Other Quote", true);
public static Replacement Colon(string replacement) => new(':', replacement, "Colon");
public static Replacement Asterisk(string replacement) => new('*', replacement, "Asterisk");
public static Replacement QuestionMark(string replacement) => new('?', replacement, "Question Mark");
public static Replacement OpenAngleBracket(string replacement) => new('<', replacement, "Open Angle Bracket");
public static Replacement CloseAngleBracket(string replacement) => new('>', replacement, "Close Angle Bracket");
public static Replacement Pipe(string replacement) => new('|', replacement, "Vertical Line");
}
[JsonConverter(typeof(ReplacementCharactersConverter))]
public class ReplacementCharacters
{
public static readonly ReplacementCharacters Default = new()
{
Replacements = new List<Replacement>()
{
Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash(""),
Replacement.FilenameBackSlash(""),
Replacement.OpenQuote("“"),
Replacement.CloseQuote("”"),
Replacement.OtherQuote(""),
Replacement.OpenAngleBracket(""),
Replacement.CloseAngleBracket(""),
Replacement.Colon(""),
Replacement.Asterisk("✱"),
Replacement.QuestionMark(""),
Replacement.Pipe("⏐"),
}
};
public static readonly ReplacementCharacters LoFiDefault = new()
{
Replacements = new List<Replacement>()
{
Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("_"),
Replacement.OpenQuote("'"),
Replacement.CloseQuote("'"),
Replacement.OtherQuote("'"),
Replacement.OpenAngleBracket("{"),
Replacement.CloseAngleBracket("}"),
Replacement.Colon("-"),
}
};
public static readonly ReplacementCharacters Minimum = new()
{
Replacements = new List<Replacement>()
{
Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("_"),
Replacement.OpenQuote("_"),
Replacement.CloseQuote("_"),
Replacement.OtherQuote("_"),
}
};
private static readonly char[] invalidChars = Path.GetInvalidPathChars().Union(new[] {
'*', '?', ':',
// these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included.
// In live code, Path.GetInvalidPathChars() does not include them
'"', '<', '>'
}).ToArray();
public IReadOnlyList<Replacement> Replacements { get; init; }
private string DefaultReplacement => Replacements[0].ReplacementString;
private Replacement ForwardSlash => Replacements[1];
private Replacement BackSlash => Replacements[2];
private string OpenQuote => Replacements[3].ReplacementString;
private string CloseQuote => Replacements[4].ReplacementString;
private string OtherQuote => Replacements[5].ReplacementString;
private string GetFilenameCharReplacement(char toReplace, char preceding, char succeding)
{
if (toReplace == ForwardSlash.CharacterToReplace)
return ForwardSlash.ReplacementString;
else if (toReplace == BackSlash.CharacterToReplace)
return BackSlash.ReplacementString;
else return GetPathCharReplacement(toReplace, preceding, succeding);
}
private string GetPathCharReplacement(char toReplace, char preceding, char succeding)
{
if (toReplace == Replacement.QUOTE_MARK)
{
if (
preceding != default
&& !char.IsLetter(preceding)
&& !char.IsNumber(preceding)
&& (char.IsLetter(succeding) || char.IsNumber(succeding))
)
return OpenQuote;
else if (
succeding != default
&& !char.IsLetter(succeding)
&& !char.IsNumber(succeding)
&& (char.IsLetter(preceding) || char.IsNumber(preceding))
)
return CloseQuote;
else
return OtherQuote;
}
for (int i = Replacement.FIXED_COUNT; i < Replacements.Count; i++)
{
var r = Replacements[i];
if (r.CharacterToReplace == toReplace)
return r.ReplacementString;
}
return DefaultReplacement;
}
public static bool ContainsInvalid(string path)
=> path.Any(c => invalidChars.Contains(c));
public string ReplaceInvalidFilenameChars(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return string.Empty;
var builder = new System.Text.StringBuilder();
for (var i = 0; i < fileName.Length; i++)
{
var c = fileName[i];
if (invalidChars.Contains(c) || c == ForwardSlash.CharacterToReplace || c == BackSlash.CharacterToReplace)
{
char preceding = i > 0 ? fileName[i - 1] : default;
char succeeding = i < fileName.Length - 1 ? fileName[i + 1] : default;
builder.Append(GetFilenameCharReplacement(c, preceding, succeeding));
}
else
builder.Append(c);
}
return builder.ToString();
}
public string ReplaceInvalidChars(string pathStr)
{
if (string.IsNullOrEmpty(pathStr)) return string.Empty;
// replace all colons except within the first 2 chars
var builder = new System.Text.StringBuilder();
for (var i = 0; i < pathStr.Length; i++)
{
var c = pathStr[i];
if (!invalidChars.Contains(c) || (i <= 2 && Path.IsPathRooted(pathStr)))
builder.Append(c);
else
{
char preceding = i > 0 ? pathStr[i - 1] : default;
char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default;
builder.Append(GetPathCharReplacement(c, preceding, succeeding));
}
}
return builder.ToString();
}
}
#region JSON Converter
internal class ReplacementCharactersConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(ReplacementCharacters);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var replaceArr = jObj[nameof(Replacement)];
IReadOnlyList<Replacement> dict = replaceArr
.ToObject<Replacement[]>().ToList();
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
//If not, reset to default.
var default0 = Replacement.OtherInvalid("");
var default1 = Replacement.FilenameForwardSlash("");
var default2 = Replacement.FilenameBackSlash("");
var default3 = Replacement.OpenQuote("");
var default4 = Replacement.CloseQuote("");
var default5 = Replacement.OtherQuote("");
if (dict.Count < Replacement.FIXED_COUNT ||
dict[0].CharacterToReplace != default0.CharacterToReplace || dict[0].Description != default0.Description ||
dict[1].CharacterToReplace != default1.CharacterToReplace || dict[1].Description != default1.Description ||
dict[2].CharacterToReplace != default2.CharacterToReplace || dict[2].Description != default2.Description ||
dict[3].CharacterToReplace != default3.CharacterToReplace || dict[3].Description != default3.Description ||
dict[4].CharacterToReplace != default4.CharacterToReplace || dict[4].Description != default4.Description ||
dict[5].CharacterToReplace != default5.CharacterToReplace || dict[5].Description != default5.Description ||
dict.Any(r => ReplacementCharacters.ContainsInvalid(r.ReplacementString))
)
{
dict = ReplacementCharacters.Default.Replacements;
}
//First FIXED_COUNT are mandatory
for (int i = 0; i < Replacement.FIXED_COUNT; i++)
dict[i].Mandatory = true;
return new ReplacementCharacters { Replacements = dict };
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
ReplacementCharacters replacements = (ReplacementCharacters)value;
var propertyNames = replacements.Replacements
.Select(c => JObject.FromObject(c)).ToList();
var prop = new JProperty(nameof(Replacement), new JArray(propertyNames));
var obj = new JObject();
obj.AddFirst(prop);
obj.WriteTo(writer);
}
}
#endregion
}

View File

@ -284,6 +284,13 @@ namespace LibationFileManager
#region templates: custom file naming #region templates: custom file naming
[Description("Edit how illegal filename characters are replaced")]
public ReplacementCharacters ReplacementCharacters
{
get => persistentDictionary.GetNonString<ReplacementCharacters>(nameof(ReplacementCharacters));
set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value);
}
[Description("How to format the folders in which files will be saved")] [Description("How to format the folders in which files will be saved")]
public string FolderTemplate public string FolderTemplate
{ {

View File

@ -106,7 +106,7 @@ namespace LibationFileManager
=> string.IsNullOrWhiteSpace(template) => string.IsNullOrWhiteSpace(template)
? "" ? ""
: getFileNamingTemplate(libraryBookDto, template, null, null) : getFileNamingTemplate(libraryBookDto, template, null, null)
.GetFilePath().PathWithoutPrefix; .GetFilePath(Configuration.Instance.ReplacementCharacters).PathWithoutPrefix;
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@ -126,7 +126,7 @@ namespace LibationFileManager
var t = template + FileUtility.GetStandardizedExtension(extension); var t = template + FileUtility.GetStandardizedExtension(extension);
var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t); var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t);
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" }; var fileNamingTemplate = new FileNamingTemplate(fullfilename);
var title = libraryBookDto.Title ?? ""; var title = libraryBookDto.Title ?? "";
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':')); var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
@ -210,7 +210,7 @@ namespace LibationFileManager
/// <summary>USES LIVE CONFIGURATION VALUES</summary> /// <summary>USES LIVE CONFIGURATION VALUES</summary>
public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null) public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null)
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null) => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null)
.GetFilePath(); .GetFilePath(Configuration.Instance.ReplacementCharacters);
#endregion #endregion
} }
@ -233,7 +233,7 @@ namespace LibationFileManager
/// <summary>USES LIVE CONFIGURATION VALUES</summary> /// <summary>USES LIVE CONFIGURATION VALUES</summary>
public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false) public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false)
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension) => getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension)
.GetFilePath(returnFirstExisting); .GetFilePath(Configuration.Instance.ReplacementCharacters, returnFirstExisting);
#endregion #endregion
} }
@ -268,8 +268,9 @@ namespace LibationFileManager
public string GetFilename(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props) public string GetFilename(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props)
=> GetPortionFilename(libraryBookDto, Configuration.Instance.ChapterFileTemplate, props, AudibleFileStorage.DecryptInProgressDirectory); => GetPortionFilename(libraryBookDto, Configuration.Instance.ChapterFileTemplate, props, AudibleFileStorage.DecryptInProgressDirectory);
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath) public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath, ReplacementCharacters replacements = null)
{ {
replacements ??= Configuration.Instance.ReplacementCharacters;
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName)); var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName));
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal);
@ -277,7 +278,7 @@ namespace LibationFileManager
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal)); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? ""); fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
return fileNamingTemplate.GetFilePath().PathWithoutPrefix; return fileNamingTemplate.GetFilePath(replacements).PathWithoutPrefix;
} }
#endregion #endregion
} }

View File

@ -0,0 +1,171 @@
namespace LibationWinForms.Dialogs
{
partial class EditReplacementChars
{
/// <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.dataGridView1 = new System.Windows.Forms.DataGridView();
this.defaultsBtn = new System.Windows.Forms.Button();
this.loFiDefaultsBtn = new System.Windows.Forms.Button();
this.saveBtn = new System.Windows.Forms.Button();
this.cancelBtn = new System.Windows.Forms.Button();
this.minDefaultBtn = new System.Windows.Forms.Button();
this.charToReplaceCol = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.replacementStringCol = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.descriptionCol = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.SuspendLayout();
//
// dataGridView1
//
this.dataGridView1.AllowUserToResizeColumns = false;
this.dataGridView1.AllowUserToResizeRows = false;
this.dataGridView1.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.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.charToReplaceCol,
this.replacementStringCol,
this.descriptionCol});
this.dataGridView1.Location = new System.Drawing.Point(12, 12);
this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.RowTemplate.Height = 25;
this.dataGridView1.Size = new System.Drawing.Size(498, 393);
this.dataGridView1.TabIndex = 0;
this.dataGridView1.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.dataGridView1_CellEndEdit);
this.dataGridView1.UserDeletingRow += new System.Windows.Forms.DataGridViewRowCancelEventHandler(this.dataGridView1_UserDeletingRow);
this.dataGridView1.Resize += new System.EventHandler(this.dataGridView1_Resize);
//
// defaultsBtn
//
this.defaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.defaultsBtn.Location = new System.Drawing.Point(12, 430);
this.defaultsBtn.Name = "defaultsBtn";
this.defaultsBtn.Size = new System.Drawing.Size(64, 25);
this.defaultsBtn.TabIndex = 1;
this.defaultsBtn.Text = "Defaults";
this.defaultsBtn.UseVisualStyleBackColor = true;
this.defaultsBtn.Click += new System.EventHandler(this.defaultsBtn_Click);
//
// loFiDefaultsBtn
//
this.loFiDefaultsBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.loFiDefaultsBtn.Location = new System.Drawing.Point(82, 430);
this.loFiDefaultsBtn.Name = "loFiDefaultsBtn";
this.loFiDefaultsBtn.Size = new System.Drawing.Size(84, 25);
this.loFiDefaultsBtn.TabIndex = 1;
this.loFiDefaultsBtn.Text = "LoFi Defaults";
this.loFiDefaultsBtn.UseVisualStyleBackColor = true;
this.loFiDefaultsBtn.Click += new System.EventHandler(this.loFiDefaultsBtn_Click);
//
// 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(428, 430);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(82, 25);
this.saveBtn.TabIndex = 1;
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.Location = new System.Drawing.Point(340, 430);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(82, 25);
this.cancelBtn.TabIndex = 1;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// minDefaultBtn
//
this.minDefaultBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.minDefaultBtn.Location = new System.Drawing.Point(172, 430);
this.minDefaultBtn.Name = "minDefaultBtn";
this.minDefaultBtn.Size = new System.Drawing.Size(80, 25);
this.minDefaultBtn.TabIndex = 1;
this.minDefaultBtn.Text = "Barebones";
this.minDefaultBtn.UseVisualStyleBackColor = true;
this.minDefaultBtn.Click += new System.EventHandler(this.minDefaultBtn_Click);
//
// charToReplaceCol
//
this.charToReplaceCol.HeaderText = "Char to Replace";
this.charToReplaceCol.MinimumWidth = 70;
this.charToReplaceCol.Name = "charToReplaceCol";
this.charToReplaceCol.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.charToReplaceCol.Width = 70;
//
// replacementStringCol
//
this.replacementStringCol.HeaderText = "Replacement Text";
this.replacementStringCol.MinimumWidth = 85;
this.replacementStringCol.Name = "replacementStringCol";
this.replacementStringCol.Width = 85;
//
// descriptionCol
//
this.descriptionCol.HeaderText = "Description";
this.descriptionCol.MinimumWidth = 100;
this.descriptionCol.Name = "descriptionCol";
this.descriptionCol.Width = 200;
//
// EditReplacementChars
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(522, 467);
this.Controls.Add(this.minDefaultBtn);
this.Controls.Add(this.loFiDefaultsBtn);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn);
this.Controls.Add(this.defaultsBtn);
this.Controls.Add(this.dataGridView1);
this.Name = "EditReplacementChars";
this.Text = "Character Replacements";
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.DataGridView dataGridView1;
private System.Windows.Forms.Button defaultsBtn;
private System.Windows.Forms.Button loFiDefaultsBtn;
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.Button minDefaultBtn;
private System.Windows.Forms.DataGridViewTextBoxColumn charToReplaceCol;
private System.Windows.Forms.DataGridViewTextBoxColumn replacementStringCol;
private System.Windows.Forms.DataGridViewTextBoxColumn descriptionCol;
}
}

View File

@ -0,0 +1,144 @@
using FileManager;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs
{
public partial class EditReplacementChars : Form
{
Configuration config;
public EditReplacementChars()
{
InitializeComponent();
dataGridView1_Resize(this, EventArgs.Empty);
}
public EditReplacementChars(Configuration config) : this()
{
this.config = config;
LoadTable(config.ReplacementCharacters.Replacements);
}
private void LoadTable(IReadOnlyList<Replacement> replacements)
{
dataGridView1.Rows.Clear();
for (int i = 0; i < replacements.Count; i++)
{
var r = replacements[i];
int row = dataGridView1.Rows.Add(r.CharacterToReplace, r.ReplacementString, r.Description);
dataGridView1.Rows[row].Tag = r;
if (r.Mandatory)
{
dataGridView1.Rows[row].Cells[charToReplaceCol.Index].ReadOnly = true;
dataGridView1.Rows[row].Cells[descriptionCol.Index].ReadOnly = true;
dataGridView1.Rows[row].Cells[charToReplaceCol.Index].Style.BackColor = System.Drawing.Color.LightGray;
dataGridView1.Rows[row].Cells[descriptionCol.Index].Style.BackColor = System.Drawing.Color.LightGray;
}
}
}
private void dataGridView1_UserDeletingRow(object sender, DataGridViewRowCancelEventArgs e)
{
if (e.Row?.Tag is Replacement r && r.Mandatory)
e.Cancel = true;
}
private void loFiDefaultsBtn_Click(object sender, EventArgs e)
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements);
private void defaultsBtn_Click(object sender, EventArgs e)
=> LoadTable(ReplacementCharacters.Default.Replacements);
private void minDefaultBtn_Click(object sender, EventArgs e)
=> LoadTable(ReplacementCharacters.Minimum.Replacements);
private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0) return;
dataGridView1.Rows[e.RowIndex].ErrorText = string.Empty;
var charToReplaceStr = dataGridView1.Rows[e.RowIndex].Cells[charToReplaceCol.Index].Value?.ToString();
var replacement = dataGridView1.Rows[e.RowIndex].Cells[replacementStringCol.Index].Value?.ToString() ?? string.Empty;
var description = dataGridView1.Rows[e.RowIndex].Cells[descriptionCol.Index].Value?.ToString() ?? string.Empty;
//Validate the whole row. If it passes all validation, add or update the row's tag.
if (string.IsNullOrEmpty(charToReplaceStr) && replacement == string.Empty && description == string.Empty)
{
//Invalid entry, so delete row
var row = dataGridView1.Rows[e.RowIndex];
if (!row.IsNewRow)
{
BeginInvoke(new MethodInvoker(delegate
{
dataGridView1.Rows.Remove(row);
}));
}
}
else if (string.IsNullOrEmpty(charToReplaceStr))
{
dataGridView1.Rows[e.RowIndex].ErrorText = $"You must choose a character to replace";
}
else if (charToReplaceStr.Length > 1)
{
dataGridView1.Rows[e.RowIndex].ErrorText = $"Only 1 {charToReplaceCol.HeaderText} per entry";
}
else if (e.RowIndex >= Replacement.FIXED_COUNT &&
dataGridView1.Rows
.Cast<DataGridViewRow>()
.Where(r => r.Index != e.RowIndex)
.Select(r => r.Tag)
.OfType<Replacement>()
.Any(r => r.CharacterToReplace == charToReplaceStr[0])
)
{
dataGridView1.Rows[e.RowIndex].ErrorText = $"The {charToReplaceStr[0]} character is already being replaced";
}
else if (ReplacementCharacters.ContainsInvalid(replacement))
{
dataGridView1.Rows[e.RowIndex].ErrorText = $"Your {replacementStringCol.HeaderText} contains illegal characters";
}
else
{
//valid entry. Add or update Replacement in row's Tag
var charToReplace = charToReplaceStr[0];
if (dataGridView1.Rows[e.RowIndex].Tag is Replacement existing)
existing.Update(charToReplace, replacement, description);
else
dataGridView1.Rows[e.RowIndex].Tag = new Replacement(charToReplace, replacement, description);
}
}
private void saveBtn_Click(object sender, EventArgs e)
{
var replacements = dataGridView1.Rows
.Cast<DataGridViewRow>()
.Select(r => r.Tag)
.OfType<Replacement>()
.ToList();
config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements };
DialogResult = DialogResult.OK;
Close();
}
private void cancelBtn_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
private void dataGridView1_Resize(object sender, EventArgs e)
{
dataGridView1.Columns[^1].Width = dataGridView1.Width - dataGridView1.Columns.Cast<DataGridViewColumn>().Sum(c => c == dataGridView1.Columns[^1] ? 0 : c.Width) - dataGridView1.RowHeadersWidth - 2;
}
}
}

View File

@ -0,0 +1,69 @@
<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>
<metadata name="charToReplaceCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="replacementStringCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="descriptionCol.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
</root>

View File

@ -60,6 +60,7 @@
this.tab3DownloadDecrypt = new System.Windows.Forms.TabPage(); this.tab3DownloadDecrypt = new System.Windows.Forms.TabPage();
this.inProgressFilesGb = new System.Windows.Forms.GroupBox(); this.inProgressFilesGb = new System.Windows.Forms.GroupBox();
this.customFileNamingGb = new System.Windows.Forms.GroupBox(); this.customFileNamingGb = new System.Windows.Forms.GroupBox();
this.editCharreplacementBtn = new System.Windows.Forms.Button();
this.chapterFileTemplateBtn = new System.Windows.Forms.Button(); this.chapterFileTemplateBtn = new System.Windows.Forms.Button();
this.chapterFileTemplateTb = new System.Windows.Forms.TextBox(); this.chapterFileTemplateTb = new System.Windows.Forms.TextBox();
this.chapterFileTemplateLbl = new System.Windows.Forms.Label(); this.chapterFileTemplateLbl = new System.Windows.Forms.Label();
@ -148,7 +149,7 @@
// saveBtn // saveBtn
// //
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(667, 441); this.saveBtn.Location = new System.Drawing.Point(667, 461);
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.saveBtn.Name = "saveBtn"; this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(88, 27); this.saveBtn.Size = new System.Drawing.Size(88, 27);
@ -161,7 +162,7 @@
// //
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); 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.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(785, 441); this.cancelBtn.Location = new System.Drawing.Point(785, 461);
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.cancelBtn.Name = "cancelBtn"; this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(88, 27); this.cancelBtn.Size = new System.Drawing.Size(88, 27);
@ -365,7 +366,7 @@
this.tabControl.Location = new System.Drawing.Point(12, 12); this.tabControl.Location = new System.Drawing.Point(12, 12);
this.tabControl.Name = "tabControl"; this.tabControl.Name = "tabControl";
this.tabControl.SelectedIndex = 0; this.tabControl.SelectedIndex = 0;
this.tabControl.Size = new System.Drawing.Size(862, 423); this.tabControl.Size = new System.Drawing.Size(862, 443);
this.tabControl.TabIndex = 100; this.tabControl.TabIndex = 100;
// //
// tab1ImportantSettings // tab1ImportantSettings
@ -377,7 +378,7 @@
this.tab1ImportantSettings.Location = new System.Drawing.Point(4, 24); this.tab1ImportantSettings.Location = new System.Drawing.Point(4, 24);
this.tab1ImportantSettings.Name = "tab1ImportantSettings"; this.tab1ImportantSettings.Name = "tab1ImportantSettings";
this.tab1ImportantSettings.Padding = new System.Windows.Forms.Padding(3); this.tab1ImportantSettings.Padding = new System.Windows.Forms.Padding(3);
this.tab1ImportantSettings.Size = new System.Drawing.Size(854, 395); this.tab1ImportantSettings.Size = new System.Drawing.Size(854, 415);
this.tab1ImportantSettings.TabIndex = 0; this.tab1ImportantSettings.TabIndex = 0;
this.tab1ImportantSettings.Text = "Important settings"; this.tab1ImportantSettings.Text = "Important settings";
this.tab1ImportantSettings.UseVisualStyleBackColor = true; this.tab1ImportantSettings.UseVisualStyleBackColor = true;
@ -416,7 +417,7 @@
this.tab2ImportLibrary.Location = new System.Drawing.Point(4, 24); this.tab2ImportLibrary.Location = new System.Drawing.Point(4, 24);
this.tab2ImportLibrary.Name = "tab2ImportLibrary"; this.tab2ImportLibrary.Name = "tab2ImportLibrary";
this.tab2ImportLibrary.Padding = new System.Windows.Forms.Padding(3); this.tab2ImportLibrary.Padding = new System.Windows.Forms.Padding(3);
this.tab2ImportLibrary.Size = new System.Drawing.Size(854, 395); this.tab2ImportLibrary.Size = new System.Drawing.Size(854, 415);
this.tab2ImportLibrary.TabIndex = 1; this.tab2ImportLibrary.TabIndex = 1;
this.tab2ImportLibrary.Text = "Import library"; this.tab2ImportLibrary.Text = "Import library";
this.tab2ImportLibrary.UseVisualStyleBackColor = true; this.tab2ImportLibrary.UseVisualStyleBackColor = true;
@ -459,7 +460,7 @@
this.tab3DownloadDecrypt.Location = new System.Drawing.Point(4, 24); this.tab3DownloadDecrypt.Location = new System.Drawing.Point(4, 24);
this.tab3DownloadDecrypt.Name = "tab3DownloadDecrypt"; this.tab3DownloadDecrypt.Name = "tab3DownloadDecrypt";
this.tab3DownloadDecrypt.Padding = new System.Windows.Forms.Padding(3); this.tab3DownloadDecrypt.Padding = new System.Windows.Forms.Padding(3);
this.tab3DownloadDecrypt.Size = new System.Drawing.Size(854, 395); this.tab3DownloadDecrypt.Size = new System.Drawing.Size(854, 415);
this.tab3DownloadDecrypt.TabIndex = 2; this.tab3DownloadDecrypt.TabIndex = 2;
this.tab3DownloadDecrypt.Text = "Download/Decrypt"; this.tab3DownloadDecrypt.Text = "Download/Decrypt";
this.tab3DownloadDecrypt.UseVisualStyleBackColor = true; this.tab3DownloadDecrypt.UseVisualStyleBackColor = true;
@ -470,7 +471,7 @@
| System.Windows.Forms.AnchorStyles.Right))); | System.Windows.Forms.AnchorStyles.Right)));
this.inProgressFilesGb.Controls.Add(this.inProgressDescLbl); this.inProgressFilesGb.Controls.Add(this.inProgressDescLbl);
this.inProgressFilesGb.Controls.Add(this.inProgressSelectControl); this.inProgressFilesGb.Controls.Add(this.inProgressSelectControl);
this.inProgressFilesGb.Location = new System.Drawing.Point(7, 251); this.inProgressFilesGb.Location = new System.Drawing.Point(6, 281);
this.inProgressFilesGb.Name = "inProgressFilesGb"; this.inProgressFilesGb.Name = "inProgressFilesGb";
this.inProgressFilesGb.Size = new System.Drawing.Size(841, 128); this.inProgressFilesGb.Size = new System.Drawing.Size(841, 128);
this.inProgressFilesGb.TabIndex = 21; this.inProgressFilesGb.TabIndex = 21;
@ -481,6 +482,7 @@
// //
this.customFileNamingGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) this.customFileNamingGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right))); | System.Windows.Forms.AnchorStyles.Right)));
this.customFileNamingGb.Controls.Add(this.editCharreplacementBtn);
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateBtn); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateBtn);
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateTb); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateTb);
this.customFileNamingGb.Controls.Add(this.chapterFileTemplateLbl); this.customFileNamingGb.Controls.Add(this.chapterFileTemplateLbl);
@ -492,11 +494,22 @@
this.customFileNamingGb.Controls.Add(this.folderTemplateLbl); this.customFileNamingGb.Controls.Add(this.folderTemplateLbl);
this.customFileNamingGb.Location = new System.Drawing.Point(7, 88); this.customFileNamingGb.Location = new System.Drawing.Point(7, 88);
this.customFileNamingGb.Name = "customFileNamingGb"; this.customFileNamingGb.Name = "customFileNamingGb";
this.customFileNamingGb.Size = new System.Drawing.Size(841, 157); this.customFileNamingGb.Size = new System.Drawing.Size(841, 187);
this.customFileNamingGb.TabIndex = 20; this.customFileNamingGb.TabIndex = 20;
this.customFileNamingGb.TabStop = false; this.customFileNamingGb.TabStop = false;
this.customFileNamingGb.Text = "Custom file naming"; this.customFileNamingGb.Text = "Custom file naming";
// //
// editCharreplacementBtn
//
this.editCharreplacementBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.editCharreplacementBtn.Location = new System.Drawing.Point(5, 158);
this.editCharreplacementBtn.Name = "editCharreplacementBtn";
this.editCharreplacementBtn.Size = new System.Drawing.Size(387, 23);
this.editCharreplacementBtn.TabIndex = 8;
this.editCharreplacementBtn.Text = "[edit char replacement desc]";
this.editCharreplacementBtn.UseVisualStyleBackColor = true;
this.editCharreplacementBtn.Click += new System.EventHandler(this.editCharreplacementBtn_Click);
//
// chapterFileTemplateBtn // chapterFileTemplateBtn
// //
this.chapterFileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this.chapterFileTemplateBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
@ -603,7 +616,7 @@
this.tab4AudioFileOptions.Location = new System.Drawing.Point(4, 24); this.tab4AudioFileOptions.Location = new System.Drawing.Point(4, 24);
this.tab4AudioFileOptions.Name = "tab4AudioFileOptions"; this.tab4AudioFileOptions.Name = "tab4AudioFileOptions";
this.tab4AudioFileOptions.Padding = new System.Windows.Forms.Padding(3); this.tab4AudioFileOptions.Padding = new System.Windows.Forms.Padding(3);
this.tab4AudioFileOptions.Size = new System.Drawing.Size(854, 395); this.tab4AudioFileOptions.Size = new System.Drawing.Size(854, 415);
this.tab4AudioFileOptions.TabIndex = 3; this.tab4AudioFileOptions.TabIndex = 3;
this.tab4AudioFileOptions.Text = "Audio File Options"; this.tab4AudioFileOptions.Text = "Audio File Options";
this.tab4AudioFileOptions.UseVisualStyleBackColor = true; this.tab4AudioFileOptions.UseVisualStyleBackColor = true;
@ -1017,7 +1030,7 @@
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn; this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(886, 484); this.ClientSize = new System.Drawing.Size(886, 504);
this.Controls.Add(this.tabControl); this.Controls.Add(this.tabControl);
this.Controls.Add(this.cancelBtn); this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.saveBtn); this.Controls.Add(this.saveBtn);
@ -1141,5 +1154,6 @@
private System.Windows.Forms.GroupBox chapterTitleTemplateGb; private System.Windows.Forms.GroupBox chapterTitleTemplateGb;
private System.Windows.Forms.Button chapterTitleTemplateBtn; private System.Windows.Forms.Button chapterTitleTemplateBtn;
private System.Windows.Forms.TextBox chapterTitleTemplateTb; private System.Windows.Forms.TextBox chapterTitleTemplateTb;
private System.Windows.Forms.Button editCharreplacementBtn;
} }
} }

View File

@ -11,9 +11,19 @@ namespace LibationWinForms.Dialogs
private void fileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.File, fileTemplateTb); 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 void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb);
private void editCharreplacementBtn_Click(object sender, EventArgs e)
{
var form = new EditReplacementChars(config);
form.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
form.ShowDialog(this);
}
private void Load_DownloadDecrypt(Configuration config) private void Load_DownloadDecrypt(Configuration config)
{ {
inProgressDescLbl.Text = desc(nameof(config.InProgress)); inProgressDescLbl.Text = desc(nameof(config.InProgress));
editCharreplacementBtn.Text = desc(nameof(config.ReplacementCharacters));
badBookGb.Text = desc(nameof(config.BadBook)); badBookGb.Text = desc(nameof(config.BadBook));
badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription(); badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription();
badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription(); badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription();

View File

@ -33,7 +33,18 @@ namespace LibationWinForms
.ToArray(); .ToArray();
// in autoScan, new books SHALL NOT show dialog // in autoScan, new books SHALL NOT show dialog
await Invoke(async () => await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts)); try
{
Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts);
if (InvokeRequired)
await Invoke(importAsync);
else
await importAsync();
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error invoking auto-scan");
}
}; };
// load init state to menu checkbox // load init state to menu checkbox

View File

@ -12,6 +12,8 @@ namespace FileNamingTemplateTests
[TestClass] [TestClass]
public class GetFilePath public class GetFilePath
{ {
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
[TestMethod] [TestMethod]
public void equiv_GetValidFilename() public void equiv_GetValidFilename()
{ {
@ -19,81 +21,33 @@ namespace FileNamingTemplateTests
sb.Append('0', 300); sb.Append('0', 300);
var longText = sb.ToString(); var longText = sb.ToString();
var expectedOld = "C:\\foo\\bar\\my_ book 00000000000000000000000000000000000000000 [ID123456].txt";
var expectedNew = "C:\\foo\\bar\\my book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt"; var expectedNew = "C:\\foo\\bar\\my book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt";
var f1 = OLD_GetValidFilename(@"C:\foo\bar", "my: book " + longText, "txt", "ID123456");
var f2 = NEW_GetValidFilename_FileNamingTemplate(@"C:\foo\bar", "my: book " + longText, "txt", "ID123456"); var f2 = NEW_GetValidFilename_FileNamingTemplate(@"C:\foo\bar", "my: book " + longText, "txt", "ID123456");
f1.Should().Be(expectedOld);
f2.Should().Be(expectedNew); f2.Should().Be(expectedNew);
} }
private static string OLD_GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
filename ??= "";
// sanitize. omit invalid characters. exception: colon => underscore
filename = filename.Replace(":", "_");
filename = FileUtility.GetSafeFileName(filename);
if (filename.Length > 50)
filename = filename.Substring(0, 50);
if (!string.IsNullOrWhiteSpace(metadataSuffix))
filename += $" [{metadataSuffix}]";
// extension is null when this method is used for directory names
extension = FileUtility.GetStandardizedExtension(extension);
// ensure uniqueness
var fullfilename = Path.Combine(dirFullPath, filename + extension);
var i = 0;
while (File.Exists(fullfilename))
fullfilename = Path.Combine(dirFullPath, filename + $" ({++i})" + extension);
return fullfilename;
}
private static string NEW_GetValidFilename_FileNamingTemplate(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 template = $"<title> [<id>]";
var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension)); var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension));
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" }; var fileNamingTemplate = new FileNamingTemplate(fullfilename);
fileNamingTemplate.AddParameterReplacement("title", filename); fileNamingTemplate.AddParameterReplacement("title", filename);
fileNamingTemplate.AddParameterReplacement("id", metadataSuffix); fileNamingTemplate.AddParameterReplacement("id", metadataSuffix);
return fileNamingTemplate.GetFilePath().PathWithoutPrefix; return fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix;
} }
[TestMethod] [TestMethod]
public void equiv_GetMultipartFileName() public void equiv_GetMultipartFileName()
{ {
var expected = @"C:\foo\bar\my file - 002 - title.txt"; 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_FileNamingTemplate(@"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); f2.Should().Be(expected);
f1.Should().Be(f2);
} }
private static string OLD_GetMultipartFileName(string originalPath, int partsPosition, int partsTotal, string suffix)
{
// 1-9 => 1-9
// 10-99 => 01-99
// 100-999 => 001-999
var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
string extension = Path.GetExtension(originalPath);
var filenameBase = $"{Path.GetFileNameWithoutExtension(originalPath)} - {chapterCountLeadingZeros}";
if (!string.IsNullOrWhiteSpace(suffix))
filenameBase += $" - {suffix}";
// Replace illegal path characters with spaces
var fileName = FileUtility.GetSafeFileName(filenameBase, " ");
var path = Path.Combine(Path.GetDirectoryName(originalPath), fileName + extension);
return path;
}
private static string NEW_GetMultipartFileName_FileNamingTemplate(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 // 1-9 => 1-9
@ -103,10 +57,10 @@ namespace FileNamingTemplateTests
var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath); var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath);
var fileNamingTemplate = new FileNamingTemplate(t) { IllegalCharacterReplacements = " " }; var fileNamingTemplate = new FileNamingTemplate(t);
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros); fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
fileNamingTemplate.AddParameterReplacement("title", suffix); fileNamingTemplate.AddParameterReplacement("title", suffix);
return fileNamingTemplate.GetFilePath().PathWithoutPrefix; return fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix;
} }
[TestMethod] [TestMethod]
@ -114,7 +68,7 @@ namespace FileNamingTemplateTests
{ {
var fileNamingTemplate = new FileNamingTemplate(@"\foo\<title>.txt"); var fileNamingTemplate = new FileNamingTemplate(@"\foo\<title>.txt");
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s"); fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
fileNamingTemplate.GetFilePath().PathWithoutPrefix.Should().Be(@"\foo\slashes.txt"); fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix.Should().Be(@"\foo\slashes.txt");
} }
} }
} }

View File

@ -12,49 +12,88 @@ namespace FileUtilityTests
[TestClass] [TestClass]
public class GetSafePath public class GetSafePath
{ {
[TestMethod] static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null)); static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault;
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Minimum;
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
[TestMethod] [TestMethod]
[DataRow("http://test.com/a/b/c", @"http\test.com\a\b\c")] public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default));
public void null_replacement(string inStr, string outStr) => Tests(inStr, null, outStr);
[TestMethod]
// non-empty replacement
[DataRow("abc*abc.txt", "abc✱abc.txt")]
// standardize slashes
[DataRow(@"a/b\c/d", @"a\b\c\d")]
// remove illegal chars
[DataRow("a*?:z.txt", "a✱z.txt")]
// retain drive letter path colon
[DataRow(@"C:\az.txt", @"C:\az.txt")]
// replace all other colons
[DataRow(@"a\b:c\d.txt", @"a\bc\d.txt")]
// remove empty directories
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")]
[DataRow(@"C:\""foo\<id>", @"C:\“foo\id")]
public void DefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, Default).PathWithoutPrefix);
[TestMethod]
// non-empty replacement
[DataRow("abc*abc.txt", "abc_abc.txt")]
// standardize slashes
[DataRow(@"a/b\c/d", @"a\b\c\d")]
// remove illegal chars
[DataRow("a*?:z.txt", "a__-z.txt")]
// retain drive letter path colon
[DataRow(@"C:\az.txt", @"C:\az.txt")]
// replace all other colons
[DataRow(@"a\b:c\d.txt", @"a\b-c\d.txt")]
// remove empty directories
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")]
[DataRow(@"C:\""foo\<id>", @"C:\'foo\{id}")]
public void LoFiDefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, LoFiDefault).PathWithoutPrefix);
[TestMethod] [TestMethod]
// empty replacement // empty replacement
[DataRow("abc*abc.txt", "", "abc✱abc.txt")] [DataRow("abc*abc.txt", "abc_abc.txt")]
// non-empty replacement
[DataRow("abc*abc.txt", "ZZZ", "abc✱abc.txt")]
// standardize slashes // standardize slashes
[DataRow(@"a/b\c/d", "Z", @"a\b\c\d")] [DataRow(@"a/b\c/d", @"a\b\c\d")]
// remove illegal chars // remove illegal chars
[DataRow("a*?:z.txt", "Z", "a✱z.txt")] [DataRow("a*?:z.txt", "a___z.txt")]
// retain drive letter path colon // retain drive letter path colon
[DataRow(@"C:\az.txt", "Z", @"C:\az.txt")] [DataRow(@"C:\az.txt", @"C:\az.txt")]
// replace all other colons // replace all other colons
[DataRow(@"a\b:c\d.txt", "ZZZ", @"a\bc\d.txt")] [DataRow(@"a\b:c\d.txt", @"a\b_c\d.txt")]
// remove empty directories // remove empty directories
[DataRow(@"C:\a\\\b\c\\\d.txt", "ZZZ", @"C:\a\b\c\d.txt")] [DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")]
[DataRow(@"C:\""foo\<id>", "ZZZ", @"C:\“foo\id")] [DataRow(@"C:\""foo\<id>", @"C:\_foo\_id_")]
public void Tests(string inStr, string replacement, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, replacement).PathWithoutPrefix); public void BarebonesDefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, Barebones).PathWithoutPrefix);
} }
[TestClass] [TestClass]
public class GetSafeFileName public class GetSafeFileName
{ {
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault;
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Minimum;
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow() // needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
[TestMethod] [TestMethod]
[DataRow("http://test.com/a/b/c", "httptest.comabc")] [DataRow("http://test.com/a/b/c", "httptest.comabc")]
public void url_null_replacement(string inStr, string outStr) => ReplacementTests(inStr, null, outStr); public void url_null_replacement(string inStr, string outStr) => DefaultReplacementTest(inStr, outStr);
[TestMethod] [TestMethod]
// empty replacement // empty replacement
[DataRow("http://test.com/a/b/c", "", "httptest.comabc")] [DataRow("http://test.com/a/b/c", "httptest.comabc")]
// single char replace public void DefaultReplacementTest(string inStr, string outStr) => Default.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr);
[DataRow("http://test.com/a/b/c", "_", "http___test.com_a_b_c")]
// multi char replace [TestMethod]
[DataRow("http://test.com/a/b/c", "!!!", "http!!!!!!!!!test.com!!!a!!!b!!!c")] // empty replacement
public void ReplacementTests(string inStr, string replacement, string outStr) => FileUtility.GetSafeFileName(inStr, replacement).Should().Be(outStr); [DataRow("http://test.com/a/b/c", "http-__test.com_a_b_c")]
public void LoFiDefaultReplacementTest(string inStr, string outStr) => LoFiDefault.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr);
[TestMethod]
// empty replacement
[DataRow("http://test.com/a/b/c", "http___test.com_a_b_c")]
public void BarebonesDefaultReplacementTest(string inStr, string outStr) => Barebones.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr);
} }
[TestClass] [TestClass]
@ -117,6 +156,8 @@ namespace FileUtilityTests
[TestClass] [TestClass]
public class GetValidFilename public class GetValidFilename
{ {
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
[TestMethod] [TestMethod]
// dot-files // dot-files
[DataRow(@"C:\a bc\x y z\.f i l e.txt")] [DataRow(@"C:\a bc\x y z\.f i l e.txt")]
@ -134,7 +175,7 @@ namespace FileUtilityTests
// file end dots // file end dots
[DataRow(@"C:\a bc\x y z\f i l e.txt . . .", @"C:\a bc\x y z\f i l e.txt")] [DataRow(@"C:\a bc\x y z\f i l e.txt . . .", @"C:\a bc\x y z\f i l e.txt")]
public void Tests(string input, string expected) public void Tests(string input, string expected)
=> FileUtility.GetValidFilename(input).PathWithoutPrefix.Should().Be(expected); => FileUtility.GetValidFilename(input, Replacements).PathWithoutPrefix.Should().Be(expected);
} }
[TestClass] [TestClass]

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dinah.Core; using Dinah.Core;
using FileManager;
using FluentAssertions; using FluentAssertions;
using LibationFileManager; using LibationFileManager;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -51,6 +52,9 @@ namespace TemplatesTests
[TestClass] [TestClass]
public class getFileNamingTemplate public class getFileNamingTemplate
{ {
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
[TestMethod] [TestMethod]
[DataRow(null, "asin", @"C:\", "ext")] [DataRow(null, "asin", @"C:\", "ext")]
[ExpectedException(typeof(ArgumentNullException))] [ExpectedException(typeof(ArgumentNullException))]
@ -73,28 +77,28 @@ namespace TemplatesTests
[DataRow("<id>", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\asin.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) public void Tests(string template, string asin, string dirFullPath, string extension, string expected)
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension) => Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension)
.GetFilePath() .GetFilePath(Replacements)
.PathWithoutPrefix .PathWithoutPrefix
.Should().Be(expected); .Should().Be(expected);
[TestMethod] [TestMethod]
public void IfSeries_empty() public void IfSeries_empty()
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series-><-if series>bar", @"C:\a\b", "ext") => Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series-><-if series>bar", @"C:\a\b", "ext")
.GetFilePath() .GetFilePath(Replacements)
.PathWithoutPrefix .PathWithoutPrefix
.Should().Be(@"C:\a\b\foobar.ext"); .Should().Be(@"C:\a\b\foobar.ext");
[TestMethod] [TestMethod]
public void IfSeries_no_series() public void IfSeries_no_series()
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", ""), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext") => Templates.getFileNamingTemplate(GetLibraryBook("asin", ""), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
.GetFilePath() .GetFilePath(Replacements)
.PathWithoutPrefix .PathWithoutPrefix
.Should().Be(@"C:\a\b\foobar.ext"); .Should().Be(@"C:\a\b\foobar.ext");
[TestMethod] [TestMethod]
public void IfSeries_with_series() public void IfSeries_with_series()
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext") => Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
.GetFilePath() .GetFilePath(Replacements)
.PathWithoutPrefix .PathWithoutPrefix
.Should().Be(@"C:\a\b\foo-Sherlock Holmes-asin-bar.ext"); .Should().Be(@"C:\a\b\foo-Sherlock Holmes-asin-bar.ext");
} }
@ -387,11 +391,13 @@ namespace Templates_ChapterFile_Tests
[TestClass] [TestClass]
public class GetPortionFilename public class GetPortionFilename
{ {
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
[TestMethod] [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", "[<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")] [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) 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) => Templates.ChapterFile.GetPortionFilename(GetLibraryBook(asin), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, Default)
.Should().Be(expected); .Should().Be(expected);
} }
} }