Merge pull request #1322 from Mbucari/master

Improve Libation's interaction with the file system & other minor fixes
This commit is contained in:
rmcrackan 2025-08-05 12:57:03 -04:00 committed by GitHub
commit 6d0c4a9b3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 829 additions and 401 deletions

View File

@ -126,8 +126,8 @@ namespace AaxDecrypter
if (DownloadOptions.SeriesName is string series) if (DownloadOptions.SeriesName is string series)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SERIES", series); AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SERIES", series);
if (DownloadOptions.SeriesNumber is float part) if (DownloadOptions.SeriesNumber is string part)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part.ToString()); AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part);
} }
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged); OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);

View File

@ -43,7 +43,7 @@ namespace AaxDecrypter
string? Publisher { get; } string? Publisher { get; }
string? Language { get; } string? Language { get; }
string? SeriesName { get; } string? SeriesName { get; }
float? SeriesNumber { get; } string? SeriesNumber { get; }
NAudio.Lame.LameConfig? LameConfig { get; } NAudio.Lame.LameConfig? LameConfig { get; }
bool Downsample { get; } bool Downsample { get; }
bool MatchSourceBitrate { get; } bool MatchSourceBitrate { get; }

View File

@ -121,7 +121,7 @@ namespace AppScaffolding
zipFileSink["Name"] = "File"; zipFileSink["Name"] = "File";
fileChanged = true; fileChanged = true;
} }
var hooks = $"{nameof(LibationFileManager)}.{nameof(FileSinkHook)}, {nameof(LibationFileManager)}"; var hooks = typeof(FileSinkHook).AssemblyQualifiedName;
if (serilog.SelectToken("$.WriteTo[?(@.Name == 'File')].Args", false) is JObject fileSinkArgs if (serilog.SelectToken("$.WriteTo[?(@.Name == 'File')].Args", false) is JObject fileSinkArgs
&& fileSinkArgs["hooks"]?.Value<string>() != hooks) && fileSinkArgs["hooks"]?.Value<string>() != hooks)
{ {
@ -158,7 +158,8 @@ namespace AppScaffolding
// - with class and method info: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception}"; // - with class and method info: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception}";
// output example: 2019-11-26 08:48:40.224 -05:00 [DBG] (at LibationWinForms.Program.init()) Begin Libation // output example: 2019-11-26 08:48:40.224 -05:00 [DBG] (at LibationWinForms.Program.init()) Begin Libation
// {Properties:j} needed for expanded exception logging // {Properties:j} needed for expanded exception logging
{ "outputTemplate", "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception} {Properties:j}" } { "outputTemplate", "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception} {Properties:j}" },
{ "hooks", typeof(FileSinkHook).AssemblyQualifiedName }, // for FileSinkHook
} }
} }
} }

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
#nullable enable
namespace DataLayer namespace DataLayer
{ {
// only library importing should use tracking. All else should be NoTracking. // only library importing should use tracking. All else should be NoTracking.
@ -24,13 +25,13 @@ namespace DataLayer
.Where(c => !c.Book.IsEpisodeParent() || includeParents) .Where(c => !c.Book.IsEpisodeParent() || includeParents)
.ToList(); .ToList();
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId) public static LibraryBook? GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
=> context => context
.LibraryBooks .LibraryBooks
.AsNoTrackingWithIdentityResolution() .AsNoTrackingWithIdentityResolution()
.GetLibraryBook(productId); .GetLibraryBook(productId);
public static LibraryBook GetLibraryBook(this IQueryable<LibraryBook> library, string productId) public static LibraryBook? GetLibraryBook(this IQueryable<LibraryBook> library, string productId)
=> library => library
.GetLibrary() .GetLibrary()
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId); .SingleOrDefault(lb => lb.Book.AudibleProductId == productId);

View File

@ -26,7 +26,7 @@ namespace FileLiberator
public string Language => LibraryBook.Book.Language; public string Language => LibraryBook.Book.Language;
public string? AudibleProductId => LibraryBookDto.AudibleProductId; public string? AudibleProductId => LibraryBookDto.AudibleProductId;
public string? SeriesName => LibraryBookDto.FirstSeries?.Name; public string? SeriesName => LibraryBookDto.FirstSeries?.Name;
public float? SeriesNumber => LibraryBookDto.FirstSeries?.Number; public string? SeriesNumber => LibraryBookDto.FirstSeries?.Number;
public NAudio.Lame.LameConfig? LameConfig { get; } public NAudio.Lame.LameConfig? LameConfig { get; }
public string UserAgent => AudibleApi.Resources.Download_User_Agent; public string UserAgent => AudibleApi.Resources.Download_User_Agent;
public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged; public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged;

View File

@ -83,7 +83,7 @@ namespace FileLiberator
.Select(sb .Select(sb
=> new SeriesDto( => new SeriesDto(
sb.Series.Name, sb.Series.Name,
sb.Book.IsEpisodeParent() ? null : sb.Index, sb.Book.IsEpisodeParent() ? null : sb.Order,
sb.Series.AudibleSeriesId) sb.Series.AudibleSeriesId)
).ToList(); ).ToList();
} }

View File

@ -0,0 +1,74 @@
using System;
using System.IO;
namespace FileManager
{
public static class FileSystemTest
{
/// <summary>
/// Additional characters which are illegal for filenames in Windows environments.
/// Double quotes and slashes are already illegal filename characters on all platforms,
/// so they are not included here.
/// </summary>
public static string AdditionalInvalidWindowsFilenameCharacters { get; } = "<>|:*?";
/// <summary>
/// Test if the directory supports filenames with characters that are invalid on Windows (:, *, ?, &lt;, &gt;, |).
/// </summary>
public static bool CanWriteWindowsInvalidChars(LongPath directoryName)
{
var testFile = Path.Combine(directoryName, AdditionalInvalidWindowsFilenameCharacters + Guid.NewGuid().ToString());
return CanWriteFile(testFile);
}
/// <summary>
/// Test if the directory supports filenames with 255 unicode characters.
/// </summary>
public static bool CanWrite255UnicodeChars(LongPath directoryName)
{
const char unicodeChar = 'ü';
var testFileName = new string(unicodeChar, 255);
var testFile = Path.Combine(directoryName, testFileName);
return CanWriteFile(testFile);
}
/// <summary>
/// Test if a directory has write access by attempting to create an empty file in it.
/// <para/>Returns true even if the temporary file can not be deleted.
/// </summary>
public static bool CanWriteDirectory(LongPath directoryName)
{
if (!Directory.Exists(directoryName))
return false;
Serilog.Log.Logger.Debug("Testing write permissions for directory: {@DirectoryName}", directoryName);
var testFilePath = Path.Combine(directoryName, Guid.NewGuid().ToString());
return CanWriteFile(testFilePath);
}
private static bool CanWriteFile(LongPath filename)
{
try
{
Serilog.Log.Logger.Debug("Testing ability to write filename: {@filename}", filename);
File.WriteAllBytes(filename, []);
Serilog.Log.Logger.Debug("Deleting test file after successful write: {@filename}", filename);
try
{
FileUtility.SaferDelete(filename);
}
catch (Exception ex)
{
//An error deleting the file doesn't constitute a write failure.
Serilog.Log.Logger.Debug(ex, "Error deleting test file: {@filename}", filename);
}
return true;
}
catch (Exception ex)
{
Serilog.Log.Logger.Debug(ex, "Error writing test file: {@filename}", filename);
return false;
}
}
}
}

View File

@ -56,7 +56,7 @@ namespace FileManager
{ {
ArgumentValidator.EnsureNotNull(name, nameof(name)); ArgumentValidator.EnsureNotNull(name, nameof(name));
name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name); name = ReplacementCharacters.Barebones(true).ReplaceFilenameChars(name);
return Task.Run(() => AddFileInternal(name, contents.Span, comment)); return Task.Run(() => AddFileInternal(name, contents.Span, comment));
} }

View File

@ -74,12 +74,14 @@ namespace FileManager
} }
public override int GetHashCode() => Replacements.GetHashCode(); public override int GetHashCode() => Replacements.GetHashCode();
public static readonly ReplacementCharacters Default public static ReplacementCharacters Default(bool ntfs) => ntfs ? HiFi_NTFS : HiFi_Other;
= IsWindows public static ReplacementCharacters LoFiDefault(bool ntfs) => ntfs ? LoFi_NTFS : LoFi_Other;
? new() public static ReplacementCharacters Barebones(bool ntfs) => ntfs ? BareBones_NTFS : BareBones_Other;
{
Replacements = new Replacement[] #region Defaults
{ private static readonly ReplacementCharacters HiFi_NTFS = new()
{
Replacements = [
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash(""), Replacement.FilenameForwardSlash(""),
Replacement.FilenameBackSlash(""), Replacement.FilenameBackSlash(""),
@ -91,28 +93,23 @@ namespace FileManager
Replacement.Colon("_"), Replacement.Colon("_"),
Replacement.Asterisk("✱"), Replacement.Asterisk("✱"),
Replacement.QuestionMark(""), Replacement.QuestionMark(""),
Replacement.Pipe("⏐"), Replacement.Pipe("⏐")]
} };
}
: new() private static readonly ReplacementCharacters HiFi_Other = new()
{ {
Replacements = new Replacement[] Replacements = [
{
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash(""), Replacement.FilenameForwardSlash(""),
Replacement.FilenameBackSlash("\\"), Replacement.FilenameBackSlash("\\"),
Replacement.OpenQuote("“"), Replacement.OpenQuote("“"),
Replacement.CloseQuote("”"), Replacement.CloseQuote("”"),
Replacement.OtherQuote("\"") Replacement.OtherQuote("\"")]
} };
};
public static readonly ReplacementCharacters LoFiDefault private static readonly ReplacementCharacters LoFi_NTFS = new()
= IsWindows {
? new() Replacements = [
{
Replacements = new Replacement[]
{
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"), Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("_"), Replacement.FilenameBackSlash("_"),
@ -121,56 +118,54 @@ namespace FileManager
Replacement.OtherQuote("'"), Replacement.OtherQuote("'"),
Replacement.OpenAngleBracket("{"), Replacement.OpenAngleBracket("{"),
Replacement.CloseAngleBracket("}"), Replacement.CloseAngleBracket("}"),
Replacement.Colon("-"), Replacement.Colon("-")]
} };
}
: new () private static readonly ReplacementCharacters LoFi_Other = new()
{ {
Replacements = new Replacement[] Replacements = [
{
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"), Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("\\"), Replacement.FilenameBackSlash("\\"),
Replacement.OpenQuote("\""), Replacement.OpenQuote("\""),
Replacement.CloseQuote("\""), Replacement.CloseQuote("\""),
Replacement.OtherQuote("\"") Replacement.OtherQuote("\"")]
} };
};
public static readonly ReplacementCharacters Barebones private static readonly ReplacementCharacters BareBones_NTFS = new()
= IsWindows {
? new () Replacements = [
{
Replacements = new Replacement[]
{
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"), Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("_"), Replacement.FilenameBackSlash("_"),
Replacement.OpenQuote("_"), Replacement.OpenQuote("_"),
Replacement.CloseQuote("_"), Replacement.CloseQuote("_"),
Replacement.OtherQuote("_") Replacement.OtherQuote("_")]
} };
}
: new () private static readonly ReplacementCharacters BareBones_Other = new()
{ {
Replacements = new Replacement[] Replacements = [
{
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"), Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("\\"), Replacement.FilenameBackSlash("\\"),
Replacement.OpenQuote("\""), Replacement.OpenQuote("\""),
Replacement.CloseQuote("\""), Replacement.CloseQuote("\""),
Replacement.OtherQuote("\"") Replacement.OtherQuote("\"")]
} };
}; #endregion
/// <summary>
/// Characters to consider invalid in filenames in addition to those returned by <see cref="Path.GetInvalidFileNameChars()"/>
/// </summary>
public static char[] AdditionalInvalidFilenameCharacters { get; set; } = [];
private static bool IsWindows => Environment.OSVersion.Platform is PlatformID.Win32NT; internal static bool IsWindows => Environment.OSVersion.Platform is PlatformID.Win32NT;
private static readonly char[] invalidPathChars = Path.GetInvalidFileNameChars().Except(new[] { private static char[] invalidPathChars { get; } = Path.GetInvalidFileNameChars().Except(new[] {
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
}).ToArray(); }).ToArray();
private static readonly char[] invalidSlashes = Path.GetInvalidFileNameChars().Intersect(new[] { private static char[] invalidSlashes { get; } = Path.GetInvalidFileNameChars().Intersect(new[] {
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
}).ToArray(); }).ToArray();
@ -229,8 +224,11 @@ namespace FileManager
return DefaultReplacement; return DefaultReplacement;
} }
private static bool CharIsPathInvalid(char c)
=> invalidPathChars.Contains(c) || AdditionalInvalidFilenameCharacters.Contains(c);
public static bool ContainsInvalidPathChar(string path) public static bool ContainsInvalidPathChar(string path)
=> path.Any(c => invalidPathChars.Contains(c)); => path.Any(CharIsPathInvalid);
public static bool ContainsInvalidFilenameChar(string path) public static bool ContainsInvalidFilenameChar(string path)
=> ContainsInvalidPathChar(path) || path.Any(c => invalidSlashes.Contains(c)); => ContainsInvalidPathChar(path) || path.Any(c => invalidSlashes.Contains(c));
@ -242,7 +240,7 @@ namespace FileManager
{ {
var c = fileName[i]; var c = fileName[i];
if (invalidPathChars.Contains(c) if (CharIsPathInvalid(c)
|| invalidSlashes.Contains(c) || invalidSlashes.Contains(c)
|| Replacements.Any(r => r.CharacterToReplace == c) /* Replace any other legal characters that they user wants. */ ) || Replacements.Any(r => r.CharacterToReplace == c) /* Replace any other legal characters that they user wants. */ )
{ {
@ -267,14 +265,14 @@ namespace FileManager
if ( if (
( (
invalidPathChars.Contains(c) CharIsPathInvalid(c)
|| ( // Replace any other legal characters that they user wants. || ( // Replace any other legal characters that they user wants.
c != Path.DirectorySeparatorChar c != Path.DirectorySeparatorChar
&& c != Path.AltDirectorySeparatorChar && c != Path.AltDirectorySeparatorChar
&& Replacements.Any(r => r.CharacterToReplace == c) && Replacements.Any(r => r.CharacterToReplace == c)
) )
) )
&& !( // replace all colons except drive letter designator on Windows && !( // replace all colons except drive letter designator on Windows
c == ':' c == ':'
&& i == 1 && i == 1
&& Path.IsPathRooted(pathStr) && Path.IsPathRooted(pathStr)
@ -282,9 +280,9 @@ namespace FileManager
) )
) )
{ {
char preceding = i > 0 ? pathStr[i - 1] : default; char preceding = i > 0 ? pathStr[i - 1] : default;
char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default; char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default;
builder.Append(GetPathCharReplacement(c, preceding, succeeding)); builder.Append(GetPathCharReplacement(c, preceding, succeeding));
} }
else else
builder.Append(c); builder.Append(c);
@ -301,23 +299,21 @@ namespace FileManager
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var defaults = ReplacementCharacters.Default(ReplacementCharacters.IsWindows).Replacements;
var jObj = JObject.Load(reader); var jObj = JObject.Load(reader);
var replaceArr = jObj[nameof(Replacement)]; var replaceArr = jObj[nameof(Replacement)];
var dict var dict = replaceArr?.ToObject<Replacement[]>()?.ToList() ?? defaults;
= replaceArr?.ToObject<Replacement[]>()?.ToList()
?? ReplacementCharacters.Default.Replacements;
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid. //Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
//If not, reset to default. //If not, reset to default.
for (int i = 0; i < Replacement.FIXED_COUNT; i++) for (int i = 0; i < Replacement.FIXED_COUNT; i++)
{ {
if (dict.Count < Replacement.FIXED_COUNT if (dict.Count < Replacement.FIXED_COUNT
|| dict[i].CharacterToReplace != ReplacementCharacters.Barebones.Replacements[i].CharacterToReplace || dict[i].CharacterToReplace != defaults[i].CharacterToReplace
|| dict[i].Description != ReplacementCharacters.Barebones.Replacements[i].Description) || dict[i].Description != defaults[i].Description)
{ {
dict = ReplacementCharacters.Default.Replacements; dict = defaults;
break; break;
} }

View File

@ -120,7 +120,10 @@ namespace LibationAvalonia
ShowMainWindow(desktop); ShowMainWindow(desktop);
} }
else else
await CancelInstallation(); {
e.Cancel = true;
await CancelInstallation(setupDialog);
}
} }
else if (setupDialog.IsReturningUser) else if (setupDialog.IsReturningUser)
{ {
@ -128,7 +131,8 @@ namespace LibationAvalonia
} }
else else
{ {
await CancelInstallation(); e.Cancel = true;
await CancelInstallation(setupDialog);
return; return;
} }
@ -139,11 +143,11 @@ namespace LibationAvalonia
var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file."; var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
try try
{ {
await MessageBox.ShowAdminAlert(null, body, title, ex); await MessageBox.ShowAdminAlert(setupDialog, body, title, ex);
} }
catch catch
{ {
await MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error); await MessageBox.Show(setupDialog, $"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
return; return;
} }
@ -190,6 +194,7 @@ namespace LibationAvalonia
{ {
// path did not result in valid settings // path did not result in valid settings
var continueResult = await MessageBox.Show( var continueResult = await MessageBox.Show(
libationFilesDialog,
$"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}", $"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
"New install?", "New install?",
MessageBoxButtons.YesNo, MessageBoxButtons.YesNo,
@ -207,18 +212,18 @@ namespace LibationAvalonia
ShowMainWindow(desktop); ShowMainWindow(desktop);
} }
else else
await CancelInstallation(); await CancelInstallation(libationFilesDialog);
} }
else else
await CancelInstallation(); await CancelInstallation(libationFilesDialog);
} }
libationFilesDialog.Close(); libationFilesDialog.Close();
} }
static async Task CancelInstallation() static async Task CancelInstallation(Window window)
{ {
await MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning); await MessageBox.Show(window, "Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
Environment.Exit(0); Environment.Exit(0);
} }

View File

@ -92,13 +92,11 @@ namespace LibationAvalonia.Controls
base.UpdateDataValidation(property, state, error); base.UpdateDataValidation(property, state, error);
if (property == CommandProperty) if (property == CommandProperty)
{ {
if (state == BindingValueType.BindingError) var canExecure = !state.HasFlag(BindingValueType.HasError);
if (canExecure != _commandCanExecute)
{ {
if (_commandCanExecute) _commandCanExecute = canExecure;
{ UpdateIsEffectivelyEnabled();
_commandCanExecute = false;
UpdateIsEffectivelyEnabled();
}
} }
} }
} }

View File

@ -6,6 +6,8 @@
MinWidth="500" MinHeight="450" MinWidth="500" MinHeight="450"
Width="500" Height="450" Width="500" Height="450"
x:Class="LibationAvalonia.Dialogs.EditReplacementChars" x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
x:DataType="dialogs:EditReplacementChars"
Title="Illegal Character Replacement"> Title="Illegal Character Replacement">
<Grid <Grid
@ -23,31 +25,30 @@
BeginningEdit="ReplacementGrid_BeginningEdit" BeginningEdit="ReplacementGrid_BeginningEdit"
CellEditEnding="ReplacementGrid_CellEditEnding" CellEditEnding="ReplacementGrid_CellEditEnding"
KeyDown="ReplacementGrid_KeyDown" KeyDown="ReplacementGrid_KeyDown"
ItemsSource="{Binding replacements}"> ItemsSource="{CompiledBinding replacements}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTemplateColumn Header="Char to&#xa;Replace"> <DataGridTemplateColumn Header="Char to&#xa;Replace">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding CharacterToReplace, Mode=TwoWay}" /> <TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding CharacterToReplace, Mode=TwoWay}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTemplateColumn Header="Replacement&#xa;Text"> <DataGridTemplateColumn Header="Replacement&#xa;Text">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox Text="{Binding ReplacementText, Mode=TwoWay}" /> <TextBox Text="{CompiledBinding ReplacementText, Mode=TwoWay}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" Header="Description"> <DataGridTemplateColumn Width="*" Header="Description">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding Description, Mode=TwoWay}" /> <TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding Description, Mode=TwoWay}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@ -55,21 +56,31 @@
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<StackPanel <Grid
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
RowDefinitions="Auto,Auto"
Margin="5" Margin="5"
Orientation="Horizontal"> ColumnDefinitions="Auto,Auto,Auto,Auto">
<Button Margin="0,0,10,0" Command="{Binding Defaults}" Content="Defaults" /> <TextBlock IsVisible="{CompiledBinding !EnvironmentIsWindows}" Text="This System:" Margin="0,0,10,0" VerticalAlignment="Center" />
<Button Margin="0,0,10,0" Command="{Binding LoFiDefaults}" Content="LoFi Defaults" /> <TextBlock IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Text="NTFS:" Margin="0,0,10,0" VerticalAlignment="Center" />
<Button Command="{Binding Barebones}" Content="Barebones" />
</StackPanel> <Button Grid.Column="1" Margin="0,0,10,0" Command="{CompiledBinding Defaults}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="Defaults" />
<Button Grid.Column="2" Margin="0,0,10,0" Command="{CompiledBinding LoFiDefaults}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="LoFi Defaults" />
<Button Grid.Column="3" Command="{CompiledBinding Barebones}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="Barebones" />
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="1" Margin="0,10,10,0" Command="{CompiledBinding Defaults}" CommandParameter="True" Content="Defaults" />
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="2" Margin="0,10,10,0" Command="{CompiledBinding LoFiDefaults}" CommandParameter="True" Content="LoFi Defaults" />
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="3" Margin="0,10,0,0" Command="{CompiledBinding Barebones}" CommandParameter="True" Content="Barebones" />
</Grid>
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
Margin="5" Margin="5"
VerticalAlignment="Bottom"
Orientation="Horizontal"> Orientation="Horizontal">
<Button Margin="0,0,10,0" Command="{Binding Close}" Content="Cancel" /> <Button Margin="0,0,10,0" Command="{Binding Close}" Content="Cancel" />

View File

@ -13,6 +13,8 @@ namespace LibationAvalonia.Dialogs
{ {
Configuration config; Configuration config;
public bool EnvironmentIsWindows => Configuration.IsWindows;
private readonly List<ReplacementsExt> SOURCE = new(); private readonly List<ReplacementsExt> SOURCE = new();
public DataGridCollectionView replacements { get; } public DataGridCollectionView replacements { get; }
public EditReplacementChars() public EditReplacementChars()
@ -23,7 +25,7 @@ namespace LibationAvalonia.Dialogs
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
LoadTable(ReplacementCharacters.Default.Replacements); LoadTable(ReplacementCharacters.Default(true).Replacements);
} }
DataContext = this; DataContext = this;
@ -35,12 +37,12 @@ namespace LibationAvalonia.Dialogs
LoadTable(config.ReplacementCharacters.Replacements); LoadTable(config.ReplacementCharacters.Replacements);
} }
public void Defaults() public void Defaults(bool isNtfs)
=> LoadTable(ReplacementCharacters.Default.Replacements); => LoadTable(ReplacementCharacters.Default(isNtfs).Replacements);
public void LoFiDefaults() public void LoFiDefaults(bool isNtfs)
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements); => LoadTable(ReplacementCharacters.LoFiDefault(isNtfs).Replacements);
public void Barebones() public void Barebones(bool isNtfs)
=> LoadTable(ReplacementCharacters.Barebones.Replacements); => LoadTable(ReplacementCharacters.Barebones(isNtfs).Replacements);
protected override void SaveAndClose() protected override void SaveAndClose()
{ {

View File

@ -6,24 +6,28 @@
Width="800" Height="450" Width="800" Height="450"
x:Class="LibationAvalonia.Dialogs.EditTemplateDialog" x:Class="LibationAvalonia.Dialogs.EditTemplateDialog"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs" xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
x:DataType="dialogs:EditTemplateDialog+EditTemplateViewModel"
Title="EditTemplateDialog"> Title="EditTemplateDialog">
<Grid RowDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto" Margin="10">
<Grid <Grid
Grid.Row="0" Grid.Row="0"
RowDefinitions="Auto,Auto" RowDefinitions="Auto,Auto"
ColumnDefinitions="*,Auto" Margin="5"> ColumnDefinitions="*,Auto"
Margin="0,0,0,10">
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
Grid.Row="0" Grid.Row="0"
Text="{Binding Description}" /> Margin="0,0,0,10"
Text="{CompiledBinding Description}" />
<TextBox <TextBox
Grid.Column="0" Grid.Column="0"
Grid.Row="1" Grid.Row="1"
Name="userEditTbox" Name="userEditTbox"
Text="{Binding UserTemplateText, Mode=TwoWay}" /> Text="{CompiledBinding UserTemplateText, Mode=TwoWay}" />
<Button <Button
Grid.Column="1" Grid.Column="1"
@ -32,9 +36,10 @@
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
Content="Reset to Default" Content="Reset to Default"
Command="{Binding ResetToDefault}"/> Command="{CompiledBinding ResetToDefault}"/>
</Grid> </Grid>
<Grid Grid.Row="1" ColumnDefinitions="Auto,*"> <Grid Grid.Row="1" ColumnDefinitions="Auto,*"
Margin="0,0,0,10">
<DataGrid <DataGrid
Grid.Row="0" Grid.Row="0"
@ -44,13 +49,13 @@
GridLinesVisibility="All" GridLinesVisibility="All"
AutoGenerateColumns="False" AutoGenerateColumns="False"
DoubleTapped="EditTemplateViewModel_DoubleTapped" DoubleTapped="EditTemplateViewModel_DoubleTapped"
ItemsSource="{Binding ListItems}" > ItemsSource="{CompiledBinding ListItems}" >
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Tag"> <DataGridTemplateColumn Width="Auto" Header="Tag">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" /> <TextBlock Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{CompiledBinding Item1}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@ -61,7 +66,7 @@
<TextBlock <TextBlock
Height="18" Height="18"
Margin="10,0,10,0" Margin="10,0,10,0"
VerticalAlignment="Center" Text="{Binding Item2}" /> VerticalAlignment="Center" Text="{CompiledBinding Item2}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@ -71,23 +76,22 @@
<Grid <Grid
Grid.Column="1" Grid.Column="1"
Margin="5,0,5,0" Margin="10,0,0,0"
RowDefinitions="Auto,*,Auto" RowDefinitions="Auto,*,Auto"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch">
<TextBlock <TextBlock
Margin="5,5,5,10" Margin="0,0,0,5"
Text="Example:"/> Text="Example:"/>
<Border <Border
Grid.Row="1" Grid.Row="1"
Margin="5"
BorderThickness="1" BorderThickness="1"
BorderBrush="{DynamicResource DataGridGridLinesBrush}"> BorderBrush="{DynamicResource DataGridGridLinesBrush}">
<TextBlock <TextBlock
TextWrapping="WrapWithOverflow" TextWrapping="WrapWithOverflow"
Inlines="{Binding Inlines}" /> Inlines="{CompiledBinding Inlines}" />
</Border> </Border>
@ -95,13 +99,17 @@
Grid.Row="2" Grid.Row="2"
Margin="5" Margin="5"
Foreground="Firebrick" Foreground="Firebrick"
Text="{Binding WarningText}" Text="{CompiledBinding WarningText}"
IsVisible="{Binding WarningText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{CompiledBinding WarningText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</Grid> </Grid>
</Grid> </Grid>
<controls:LinkLabel
Grid.Row="2"
VerticalAlignment="Center"
Text="Read about naming templates on the Wiki"
Command="{Binding GoToNamingTemplateWiki}" />
<Button <Button
Grid.Row="2" Grid.Row="2"
Margin="5"
Padding="30,5,30,5" Padding="30,5,30,5"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Content="Save" Content="Save"

View File

@ -70,7 +70,7 @@ public partial class EditTemplateDialog : DialogWindow
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync(); => await SaveAndCloseAsync();
private class EditTemplateViewModel : ViewModels.ViewModelBase internal class EditTemplateViewModel : ViewModels.ViewModelBase
{ {
private readonly Configuration config; private readonly Configuration config;
public InlineCollection Inlines { get; } = new(); public InlineCollection Inlines { get; } = new();
@ -96,6 +96,9 @@ public partial class EditTemplateDialog : DialogWindow
} }
public void GoToNamingTemplateWiki()
=> Go.To.Url(@"ht" + "tps://github.com/rmcrackan/Libation/blob/master/Documentation/NamingTemplates.md");
// hold the work-in-progress value. not guaranteed to be valid // hold the work-in-progress value. not guaranteed to be valid
private string _userTemplateText; private string _userTemplateText;
public string UserTemplateText public string UserTemplateText

View File

@ -13,11 +13,12 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
public partial class LocateAudiobooksDialog : DialogWindow public partial class LocateAudiobooksDialog : DialogWindow
{ {
private event EventHandler<FilePathCache.CacheEntry> FileFound; private event EventHandler<FilePathCache.CacheEntry>? FileFound;
private readonly CancellationTokenSource tokenSource = new(); private readonly CancellationTokenSource tokenSource = new();
private readonly List<string> foundAsins = new(); private readonly List<string> foundAsins = new();
private readonly LocatedAudiobooksViewModel _viewModel; private readonly LocatedAudiobooksViewModel _viewModel;
@ -41,7 +42,7 @@ namespace LibationAvalonia.Dialogs
} }
} }
private void LocateAudiobooksDialog_Closing(object sender, System.ComponentModel.CancelEventArgs e) private void LocateAudiobooksDialog_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{ {
tokenSource.Cancel(); tokenSource.Cancel();
//If this dialog is closed before it's completed, Closing is fired //If this dialog is closed before it's completed, Closing is fired
@ -50,7 +51,7 @@ namespace LibationAvalonia.Dialogs
this.SaveSizeAndLocation(Configuration.Instance); this.SaveSizeAndLocation(Configuration.Instance);
} }
private void LocateAudiobooks_FileFound(object sender, FilePathCache.CacheEntry e) private void LocateAudiobooks_FileFound(object? sender, FilePathCache.CacheEntry e)
{ {
var newItem = new Tuple<string, string>($"[{e.Id}]", Path.GetFileName(e.Path)); var newItem = new Tuple<string, string>($"[{e.Id}]", Path.GetFileName(e.Path));
_viewModel.FoundFiles.Add(newItem); _viewModel.FoundFiles.Add(newItem);
@ -63,13 +64,13 @@ namespace LibationAvalonia.Dialogs
} }
} }
private async void LocateAudiobooksDialog_Opened(object sender, EventArgs e) private async void LocateAudiobooksDialog_Opened(object? sender, EventArgs e)
{ {
var folderPicker = new FolderPickerOpenOptions var folderPicker = new FolderPickerOpenOptions
{ {
Title = "Select the folder to search for audiobooks", Title = "Select the folder to search for audiobooks",
AllowMultiple = false, AllowMultiple = false,
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix) SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix ?? "")
}; };
var selectedFolder = (await StorageProvider.OpenFolderPickerAsync(folderPicker))?.SingleOrDefault()?.TryGetLocalPath(); var selectedFolder = (await StorageProvider.OpenFolderPickerAsync(folderPicker))?.SingleOrDefault()?.TryGetLocalPath();
@ -89,11 +90,13 @@ namespace LibationAvalonia.Dialogs
FilePathCache.Insert(book); FilePathCache.Insert(book);
var lb = context.GetLibraryBook_Flat_NoTracking(book.Id); var lb = context.GetLibraryBook_Flat_NoTracking(book.Id);
if (lb?.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated) if (lb is not null && lb.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated)); await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated));
tokenSource.Token.ThrowIfCancellationRequested();
FileFound?.Invoke(this, book); FileFound?.Invoke(this, book);
} }
catch (OperationCanceledException) { }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book); Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);

View File

@ -1,7 +1,9 @@
using Avalonia.Controls; using Avalonia.Controls;
using FileManager;
using LibationAvalonia.ViewModels.Settings; using LibationAvalonia.ViewModels.Settings;
using LibationFileManager; using LibationFileManager;
using LibationUiBase.Forms; using LibationUiBase.Forms;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
@ -39,6 +41,21 @@ namespace LibationAvalonia.Dialogs
} }
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync(); {
LongPath lonNewBooks = settingsDisp.ImportantSettings.GetBooksDirectory();
if (!System.IO.Directory.Exists(lonNewBooks))
{
try
{
System.IO.Directory.CreateDirectory(lonNewBooks);
}
catch (Exception ex)
{
await MessageBox.Show(this, $"Error creating Books Location:\n\n{ex.Message}", "Error creating directory", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
await SaveAndCloseAsync();
}
} }
} }

View File

@ -20,6 +20,7 @@ namespace LibationAvalonia.ViewModels
private bool _removeButtonsVisible = Design.IsDesignMode; private bool _removeButtonsVisible = Design.IsDesignMode;
private int _numAccountsScanning = 2; private int _numAccountsScanning = 2;
private int _accountsCount = 0; private int _accountsCount = 0;
public string LocateAudiobooksTip => Configuration.GetHelpText("LocateAudiobooks");
/// <summary> Auto scanning accounts is enables </summary> /// <summary> Auto scanning accounts is enables </summary>
public bool AutoScanChecked { get => _autoScanChecked; set => Configuration.Instance.AutoScan = this.RaiseAndSetIfChanged(ref _autoScanChecked, value); } public bool AutoScanChecked { get => _autoScanChecked; set => Configuration.Instance.AutoScan = this.RaiseAndSetIfChanged(ref _autoScanChecked, value); }
@ -68,7 +69,8 @@ namespace LibationAvalonia.ViewModels
MainWindow.Loaded += (_, _) => MainWindow.Loaded += (_, _) =>
{ {
refreshImportMenu(); refreshImportMenu();
AccountsSettingsPersister.Saved += refreshImportMenu; AccountsSettingsPersister.Saved += (_, _)
=> Avalonia.Threading.Dispatcher.UIThread.Invoke(refreshImportMenu);
}; };
AutoScanChecked = Configuration.Instance.AutoScan; AutoScanChecked = Configuration.Instance.AutoScan;
@ -172,8 +174,19 @@ namespace LibationAvalonia.ViewModels
public async Task LocateAudiobooksAsync() public async Task LocateAudiobooksAsync()
{ {
var locateDialog = new LibationAvalonia.Dialogs.LocateAudiobooksDialog(); var result = await MessageBox.Show(
await locateDialog.ShowDialog(MainWindow); MainWindow,
Configuration.GetHelpText(nameof(LibationAvalonia.Dialogs.LocateAudiobooksDialog)),
"Locate Previously-Liberated Audiobook Files",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Information,
MessageBoxDefaultButton.Button1);
if (result is DialogResult.OK)
{
var locateDialog = new LibationAvalonia.Dialogs.LocateAudiobooksDialog();
await locateDialog.ShowDialog(MainWindow);
}
} }
private void setyNumScanningAccounts(int numScanning) private void setyNumScanningAccounts(int numScanning)
@ -222,7 +235,7 @@ namespace LibationAvalonia.ViewModels
} }
} }
private void refreshImportMenu(object? _ = null, EventArgs? __ = null) private void refreshImportMenu()
{ {
using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
AccountsCount = persister.AccountsSettings.Accounts.Count; AccountsCount = persister.AccountsSettings.Accounts.Count;

View File

@ -36,10 +36,7 @@ namespace LibationAvalonia.ViewModels.Settings
public void SaveSettings(Configuration config) public void SaveSettings(Configuration config)
{ {
LongPath lonNewBooks = Configuration.GetKnownDirectory(BooksDirectory) is Configuration.KnownDirectories.None ? BooksDirectory : System.IO.Path.Combine(BooksDirectory, "Books"); config.Books = GetBooksDirectory();
if (!System.IO.Directory.Exists(lonNewBooks))
System.IO.Directory.CreateDirectory(lonNewBooks);
config.Books = lonNewBooks;
config.SavePodcastsToParentFolder = SavePodcastsToParentFolder; config.SavePodcastsToParentFolder = SavePodcastsToParentFolder;
config.OverwriteExisting = OverwriteExisting; config.OverwriteExisting = OverwriteExisting;
config.CreationTime = CreationTime.Value; config.CreationTime = CreationTime.Value;
@ -47,6 +44,9 @@ namespace LibationAvalonia.ViewModels.Settings
config.LogLevel = LoggingLevel; config.LogLevel = LoggingLevel;
} }
public LongPath GetBooksDirectory()
=> Configuration.GetKnownDirectory(BooksDirectory) is Configuration.KnownDirectories.None ? BooksDirectory : System.IO.Path.Combine(BooksDirectory, "Books");
private static float scaleFactorToLinearRange(float scaleFactor) private static float scaleFactorToLinearRange(float scaleFactor)
=> float.Round(100 * MathF.Log2(scaleFactor)); => float.Round(100 * MathF.Log2(scaleFactor));
private static float linearRangeToScaleFactor(float value) private static float linearRangeToScaleFactor(float value)

View File

@ -110,7 +110,7 @@
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksSomeAsync}" Header="_Remove Books from Some Accounts" /> <MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksSomeAsync}" Header="_Remove Books from Some Accounts" />
<Separator /> <Separator />
<MenuItem Command="{CompiledBinding LocateAudiobooksAsync}" Header="L_ocate Audiobooks..." /> <MenuItem Command="{CompiledBinding LocateAudiobooksAsync}" Header="L_ocate Audiobooks..." ToolTip.Tip="{CompiledBinding LocateAudiobooksTip}" />
</MenuItem> </MenuItem>

View File

@ -43,6 +43,22 @@ namespace LibationAvalonia.Views
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ShowAccountsAsync), Gesture = new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Shift) }); KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ShowAccountsAsync), Gesture = new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Shift) });
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ExportLibraryAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Control) }); KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ExportLibraryAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Control) });
} }
Configuration.Instance.PropertyChanged += Settings_PropertyChanged;
Settings_PropertyChanged(this, null);
}
[Dinah.Core.PropertyChangeFilter(nameof(Configuration.Books))]
private void Settings_PropertyChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
{
if (!Configuration.IsWindows)
{
//The books directory does not support filenames with windows' invalid characters.
//Tell the ReplacementCharacters configuration to treat those characters as invalid.
ReplacementCharacters.AdditionalInvalidFilenameCharacters
= Configuration.Instance.BooksCanWriteWindowsInvalidChars ? []
: FileSystemTest.AdditionalInvalidWindowsFilenameCharacters.ToArray();
}
} }
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e) private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
@ -54,7 +70,7 @@ namespace LibationAvalonia.Views
FileUtility.SaferMoveToValidPath( FileUtility.SaferMoveToValidPath(
e.SettingsFilePath, e.SettingsFilePath,
e.SettingsFilePath, e.SettingsFilePath,
ReplacementCharacters.Barebones, Configuration.Instance.ReplacementCharacters,
"bak"); "bak");
AudibleApiStorage.EnsureAccountsSettingsFileExists(); AudibleApiStorage.EnsureAccountsSettingsFileExists();
e.Handled = true; e.Handled = true;
@ -103,6 +119,20 @@ namespace LibationAvalonia.Views
private async void MainWindow_Opened(object sender, EventArgs e) private async void MainWindow_Opened(object sender, EventArgs e)
{ {
if (AudibleFileStorage.BooksDirectory is null)
{
var result = await MessageBox.Show(
this,
"Please set a valid Books location in the settings dialog.",
"Books Directory Not Set",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button1);
if (result is DialogResult.OK)
await new SettingsDialog().ShowDialog(this);
}
if (Configuration.Instance.FirstLaunch) if (Configuration.Instance.FirstLaunch)
{ {
var result = await MessageBox.Show(this, "Would you like a guided tour to get started?", "Libation Walkthrough", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1); var result = await MessageBox.Show(this, "Would you like a guided tour to get started?", "Libation Walkthrough", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);

View File

@ -5,14 +5,15 @@ using DataLayer;
using LibationUiBase; using LibationUiBase;
using LibationUiBase.ProcessQueue; using LibationUiBase.ProcessQueue;
#nullable enable
namespace LibationAvalonia.Views namespace LibationAvalonia.Views
{ {
public delegate void QueueItemPositionButtonClicked(ProcessBookViewModel item, QueuePosition queueButton); public delegate void QueueItemPositionButtonClicked(ProcessBookViewModel? item, QueuePosition queueButton);
public delegate void QueueItemCancelButtonClicked(ProcessBookViewModel item); public delegate void QueueItemCancelButtonClicked(ProcessBookViewModel? item);
public partial class ProcessBookControl : UserControl public partial class ProcessBookControl : UserControl
{ {
public static event QueueItemPositionButtonClicked PositionButtonClicked; public static event QueueItemPositionButtonClicked? PositionButtonClicked;
public static event QueueItemCancelButtonClicked CancelButtonClicked; public static event QueueItemCancelButtonClicked? CancelButtonClicked;
public static readonly StyledProperty<ProcessBookStatus> ProcessBookStatusProperty = public static readonly StyledProperty<ProcessBookStatus> ProcessBookStatusProperty =
AvaloniaProperty.Register<ProcessBookControl, ProcessBookStatus>(nameof(ProcessBookStatus), enableDataValidation: true); AvaloniaProperty.Register<ProcessBookControl, ProcessBookStatus>(nameof(ProcessBookStatus), enableDataValidation: true);
@ -31,12 +32,13 @@ namespace LibationAvalonia.Views
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
ViewModels.MainVM.Configure_NonUI(); ViewModels.MainVM.Configure_NonUI();
DataContext = new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")); if (context.GetLibraryBook_Flat_NoTracking("B017V4IM1G") is LibraryBook book)
DataContext = new ProcessBookViewModel(book);
return; return;
} }
} }
private ProcessBookViewModel DataItem => DataContext is null ? null : DataContext as ProcessBookViewModel; private ProcessBookViewModel? DataItem => DataContext as ProcessBookViewModel;
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> CancelButtonClicked?.Invoke(DataItem); => CancelButtonClicked?.Invoke(DataItem);

View File

@ -34,44 +34,51 @@ namespace LibationAvalonia.Views
var vm = new ProcessQueueViewModel(); var vm = new ProcessQueueViewModel();
DataContext = vm; DataContext = vm;
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
var trialBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G") ?? context.GetLibrary_Flat_NoTracking().FirstOrDefault();
if (trialBook is null)
return;
List<ProcessBookViewModel> testList = new() List<ProcessBookViewModel> testList = new()
{ {
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.FailedAbort, Result = ProcessBookResult.FailedAbort,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.FailedSkip, Result = ProcessBookResult.FailedSkip,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.FailedRetry, Result = ProcessBookResult.FailedRetry,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.ValidationFail, Result = ProcessBookResult.ValidationFail,
Status = ProcessBookStatus.Failed, Status = ProcessBookStatus.Failed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.Cancelled, Result = ProcessBookResult.Cancelled,
Status = ProcessBookStatus.Cancelled, Status = ProcessBookStatus.Cancelled,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.Success, Result = ProcessBookResult.Success,
Status = ProcessBookStatus.Completed, Status = ProcessBookStatus.Completed,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.None, Result = ProcessBookResult.None,
Status = ProcessBookStatus.Working, Status = ProcessBookStatus.Working,
}, },
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")) new ProcessBookViewModel(trialBook)
{ {
Result = ProcessBookResult.None, Result = ProcessBookResult.None,
Status = ProcessBookStatus.Queued, Status = ProcessBookStatus.Queued,
@ -99,7 +106,7 @@ namespace LibationAvalonia.Views
#region Control event handlers #region Control event handlers
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item) private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel? item)
{ {
if (item is not null) if (item is not null)
{ {
@ -108,19 +115,20 @@ namespace LibationAvalonia.Views
} }
} }
private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel item, QueuePosition queueButton) private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel? item, QueuePosition queueButton)
{ {
Queue?.MoveQueuePosition(item, queueButton); if (item is not null)
Queue?.MoveQueuePosition(item, queueButton);
} }
public async void CancelAllBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void CancelAllBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
Queue?.ClearQueue(); Queue?.ClearQueue();
if (Queue?.Current is not null) if (Queue?.Current is not null)
await Queue.Current.CancelAsync(); await Queue.Current.CancelAsync();
} }
public void ClearFinishedBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void ClearFinishedBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
Queue?.ClearCompleted(); Queue?.ClearCompleted();
@ -128,12 +136,12 @@ namespace LibationAvalonia.Views
_viewModel.RunningTime = string.Empty; _viewModel.RunningTime = string.Empty;
} }
public void ClearLogBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void ClearLogBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
_viewModel?.LogEntries.Clear(); _viewModel?.LogEntries.Clear();
} }
private async void LogCopyBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) private async void LogCopyBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
if (_viewModel is ProcessQueueViewModel vm) if (_viewModel is ProcessQueueViewModel vm)
{ {
@ -143,14 +151,14 @@ namespace LibationAvalonia.Views
} }
} }
private async void cancelAllBtn_Click(object sender, EventArgs e) private async void cancelAllBtn_Click(object? sender, EventArgs e)
{ {
Queue?.ClearQueue(); Queue?.ClearQueue();
if (Queue?.Current is not null) if (Queue?.Current is not null)
await Queue.Current.CancelAsync(); await Queue.Current.CancelAsync();
} }
private void btnClearFinished_Click(object sender, EventArgs e) private void btnClearFinished_Click(object? sender, EventArgs e)
{ {
Queue?.ClearCompleted(); Queue?.ClearCompleted();

View File

@ -62,25 +62,22 @@ namespace LibationAvalonia.Views
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
List<LibraryBook> sampleEntries; LibraryBook?[] sampleEntries;
try try
{ {
sampleEntries = new() sampleEntries = [
{
//context.GetLibraryBook_Flat_NoTracking("B00DCD0OXU"),try{
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"), context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"),
context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"), context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"), context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"), context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"), context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"), context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6") context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")];
};
} }
catch { sampleEntries = new(); } catch { sampleEntries = []; }
var pdvm = new ProductsDisplayViewModel(); var pdvm = new ProductsDisplayViewModel();
_ = pdvm.BindToGridAsync(sampleEntries); _ = pdvm.BindToGridAsync(sampleEntries.OfType<LibraryBook>().ToList());
DataContext = pdvm; DataContext = pdvm;
setGridScale(1); setGridScale(1);

View File

@ -5,6 +5,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using Dinah.Core;
using Dinah.Core.StepRunner; using Dinah.Core.StepRunner;
using LibationAvalonia.Dialogs; using LibationAvalonia.Dialogs;
using LibationAvalonia.Views; using LibationAvalonia.Views;
@ -164,7 +165,8 @@ namespace LibationAvalonia
{ {
//if we imported new books, wait for the grid to update before proceeding. //if we imported new books, wait for the grid to update before proceeding.
if (newCount > 0) if (newCount > 0)
MainForm.ViewModel.ProductsDisplay.VisibleCountChanged += productsDisplay_VisibleCountChanged; Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
MainForm.ViewModel.ProductsDisplay.VisibleCountChanged += productsDisplay_VisibleCountChanged);
else else
tcs.SetResult(); tcs.SetResult();
} }
@ -176,7 +178,7 @@ namespace LibationAvalonia
var books = DbContexts.GetLibrary_Flat_NoTracking(); var books = DbContexts.GetLibrary_Flat_NoTracking();
if (books.Count == 0) return true; if (books.Count == 0) return true;
var firstAuthor = getFirstAuthor(); var firstAuthor = getFirstAuthor()?.SurroundWithQuotes();
if (firstAuthor == null) return true; if (firstAuthor == null) return true;
if (!await ProceedMessageBox("You can filter the grid entries by searching", "Searching")) if (!await ProceedMessageBox("You can filter the grid entries by searching", "Searching"))
@ -193,7 +195,7 @@ namespace LibationAvalonia
await displayControlAsync(MainForm.filterBtn); await displayControlAsync(MainForm.filterBtn);
MainForm.filterBtn.Command.Execute(null); MainForm.filterBtn.Command.Execute(firstAuthor);
await Task.Delay(1000); await Task.Delay(1000);
@ -209,8 +211,7 @@ namespace LibationAvalonia
private async Task<bool> ShowQuickFilters() private async Task<bool> ShowQuickFilters()
{ {
var firstAuthor = getFirstAuthor(); var firstAuthor = getFirstAuthor()?.SurroundWithQuotes();
if (firstAuthor == null) return true; if (firstAuthor == null) return true;
if (!await ProceedMessageBox("Queries that you perform regularly can be added to 'Quick Filters'", "Quick Filters")) if (!await ProceedMessageBox("Queries that you perform regularly can be added to 'Quick Filters'", "Quick Filters"))
@ -222,7 +223,7 @@ namespace LibationAvalonia
await Task.Delay(750); await Task.Delay(750);
await displayControlAsync(MainForm.addQuickFilterBtn); await displayControlAsync(MainForm.addQuickFilterBtn);
MainForm.addQuickFilterBtn.Command.Execute(null); MainForm.addQuickFilterBtn.Command.Execute(firstAuthor);
await displayControlAsync(MainForm.quickFiltersToolStripMenuItem); await displayControlAsync(MainForm.quickFiltersToolStripMenuItem);
await displayControlAsync(editQuickFiltersToolStripMenuItem); await displayControlAsync(editQuickFiltersToolStripMenuItem);

View File

@ -1,4 +1,6 @@
using CommandLine; using CommandLine;
using LibationFileManager;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationCli namespace LibationCli
@ -6,6 +8,15 @@ namespace LibationCli
[Verb("convert", HelpText = "Convert mp4 to mp3.")] [Verb("convert", HelpText = "Convert mp4 to mp3.")]
public class ConvertOptions : ProcessableOptionsBase public class ConvertOptions : ProcessableOptionsBase
{ {
protected override Task ProcessAsync() => RunAsync(CreateProcessable<FileLiberator.ConvertToMp3>()); protected override Task ProcessAsync()
{
if (AudibleFileStorage.BooksDirectory is null)
{
Console.Error.WriteLine("Error: Books directory is not set. Please configure the 'Books' setting in Settings.json.");
return Task.CompletedTask;
}
return RunAsync(CreateProcessable<FileLiberator.ConvertToMp3>());
}
} }
} }

View File

@ -1,6 +1,8 @@
using CommandLine; using CommandLine;
using DataLayer; using DataLayer;
using FileLiberator; using FileLiberator;
using LibationFileManager;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationCli namespace LibationCli
@ -13,9 +15,17 @@ namespace LibationCli
public bool PdfOnly { get; set; } public bool PdfOnly { get; set; }
protected override Task ProcessAsync() protected override Task ProcessAsync()
=> PdfOnly {
if (AudibleFileStorage.BooksDirectory is null)
{
Console.Error.WriteLine("Error: Books directory is not set. Please configure the 'Books' setting in Settings.json.");
return Task.CompletedTask;
}
return PdfOnly
? RunAsync(CreateProcessable<DownloadPdf>()) ? RunAsync(CreateProcessable<DownloadPdf>())
: RunAsync(CreateBackupBook()); : RunAsync(CreateBackupBook());
}
private static Processable CreateBackupBook() private static Processable CreateBackupBook()
{ {

View File

@ -45,13 +45,24 @@ namespace LibationFileManager
public static AudioFileStorage Audio { get; } = new AudioFileStorage(); public static AudioFileStorage Audio { get; } = new AudioFileStorage();
public static LongPath BooksDirectory /// <summary>
/// The fully-qualified Books durectory path if the directory exists, otherwise null.
/// </summary>
public static LongPath? BooksDirectory
{ {
get get
{ {
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books)) if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
Configuration.Instance.Books = Configuration.DefaultBooksDirectory; return null;
return Directory.CreateDirectory(Configuration.Instance.Books).FullName; try
{
return Directory.CreateDirectory(Configuration.Instance.Books)?.FullName;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error creating Books directory: {@BooksDirectory}", Configuration.Instance.Books);
return null;
}
} }
} }
#endregion #endregion
@ -129,8 +140,9 @@ namespace LibationFileManager
protected override LongPath? GetFilePathCustom(string productId) protected override LongPath? GetFilePathCustom(string productId)
=> GetFilePathsCustom(productId).FirstOrDefault(); => GetFilePathsCustom(productId).FirstOrDefault();
private static BackgroundFileSystem newBookDirectoryFiles() private static BackgroundFileSystem? newBookDirectoryFiles()
=> new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); => BooksDirectory is LongPath books ? new BackgroundFileSystem(books, "*.*", SearchOption.AllDirectories)
: null;
protected override List<LongPath> GetFilePathsCustom(string productId) protected override List<LongPath> GetFilePathsCustom(string productId)
{ {
@ -140,6 +152,7 @@ namespace LibationFileManager
BookDirectoryFiles = newBookDirectoryFiles(); BookDirectoryFiles = newBookDirectoryFiles();
var regex = GetBookSearchRegex(productId); var regex = GetBookSearchRegex(productId);
var diskFiles = BookDirectoryFiles?.FindFiles(regex) ?? [];
//Find all extant files matching the productId //Find all extant files matching the productId
//using both the file system and the file path cache //using both the file system and the file path cache
@ -148,17 +161,17 @@ namespace LibationFileManager
.GetFiles(productId) .GetFiles(productId)
.Where(c => c.fileType == FileType.Audio && File.Exists(c.path)) .Where(c => c.fileType == FileType.Audio && File.Exists(c.path))
.Select(c => c.path) .Select(c => c.path)
.Union(BookDirectoryFiles.FindFiles(regex)) .Union(diskFiles)
.ToList(); .ToList();
} }
public void Refresh() public void Refresh()
{ {
if (BookDirectoryFiles is null) if (BookDirectoryFiles is null && BooksDirectory is not null)
lock (bookDirectoryFilesLocker) lock (bookDirectoryFilesLocker)
BookDirectoryFiles = newBookDirectoryFiles(); BookDirectoryFiles = newBookDirectoryFiles();
else
BookDirectoryFiles?.RefreshFiles(); BookDirectoryFiles?.RefreshFiles();
} }
public LongPath? GetPath(string productId) => GetFilePath(productId); public LongPath? GetPath(string productId) => GetFilePath(productId);

View File

@ -107,8 +107,27 @@ namespace LibationFileManager
don't have a spatial audio version will be download don't have a spatial audio version will be download
as usual based on your other file quality settings. as usual based on your other file quality settings.
""" }, """ },
} {"LocateAudiobooks","""
.AsReadOnly(); Scan the contents a folder to find audio files that
match books in Libation's database. This is useful
if you moved your Books folder or re-installed
Libation and want it to be able to find your
already downloaded audiobooks.
Prerequisite: An audiobook must already exist in
Libation's database (through an Audible account
scan) for a matching audio file to be found.
""" },
{"LocateAudiobooksDialog","""
Libation will search all .m4b and .mp3 files in a folder, looking for audio files belonging to library books in Libation's database.
If an audiobook file is found that matches one of Libation's library books, Libation will mark that book as "Liberated" (green stoplight).
For an audio file to be identified, Libation must have that library book in its database. If you're on a fresh installation of Libation, be sure to add and scan all of your Audible accounts before running this action.
This may take a while, depending on the number of audio files in the folder and the speed of your storage device.
""" }
}.AsReadOnly();
public static string GetHelpText(string? settingName) public static string GetHelpText(string? settingName)
=> settingName != null && HelpText.TryGetValue(settingName, out var value) ? value : ""; => settingName != null && HelpText.TryGetValue(settingName, out var value) ? value : "";

View File

@ -84,7 +84,7 @@ namespace LibationFileManager
ProcessDirectory, ProcessDirectory,
LocalAppData, LocalAppData,
UserProfile, UserProfile,
Path.Combine(Path.GetTempPath(), "Libation") WinTemp,
}; };
//Try to find and validate appsettings.json in each folder //Try to find and validate appsettings.json in each folder
@ -181,7 +181,7 @@ namespace LibationFileManager
} }
catch (Exception e) catch (Exception e)
{ {
Serilog.Log.Error(e, "Failed to run shell command. {Arguments}", psi.ArgumentList); Serilog.Log.Error(e, "Failed to run shell command. {@Arguments}", psi.ArgumentList);
return null; return null;
} }
} }

View File

@ -27,6 +27,7 @@ namespace LibationFileManager
//https://github.com/serilog/serilog-settings-configuration/issues/406 //https://github.com/serilog/serilog-settings-configuration/issues/406
var readerOptions = new ConfigurationReaderOptions( var readerOptions = new ConfigurationReaderOptions(
typeof(ILogger).Assembly, // Serilog typeof(ILogger).Assembly, // Serilog
typeof(LoggerCallerEnrichmentConfiguration).Assembly, // Dinah.Core
typeof(LoggerEnrichmentConfigurationExtensions).Assembly, // Serilog.Exceptions typeof(LoggerEnrichmentConfigurationExtensions).Assembly, // Serilog.Exceptions
typeof(ConsoleLoggerConfigurationExtensions).Assembly, // Serilog.Sinks.Console typeof(ConsoleLoggerConfigurationExtensions).Assembly, // Serilog.Sinks.Console
typeof(FileLoggerConfigurationExtensions).Assembly); // Serilog.Sinks.File typeof(FileLoggerConfigurationExtensions).Assembly); // Serilog.Sinks.File

View File

@ -36,12 +36,12 @@ namespace LibationFileManager
[return: NotNullIfNotNull(nameof(defaultValue))] [return: NotNullIfNotNull(nameof(defaultValue))]
public T? GetNonString<T>(T defaultValue, [CallerMemberName] string propertyName = "") public T? GetNonString<T>(T defaultValue, [CallerMemberName] string propertyName = "")
=> Settings.GetNonString(propertyName, defaultValue); => Settings is null ? default : Settings.GetNonString(propertyName, defaultValue);
[return: NotNullIfNotNull(nameof(defaultValue))] [return: NotNullIfNotNull(nameof(defaultValue))]
public string? GetString(string? defaultValue = null, [CallerMemberName] string propertyName = "") public string? GetString(string? defaultValue = null, [CallerMemberName] string propertyName = "")
=> Settings.GetString(propertyName, defaultValue); => Settings?.GetString(propertyName, defaultValue);
public object? GetObject([CallerMemberName] string propertyName = "") => Settings.GetObject(propertyName); public object? GetObject([CallerMemberName] string propertyName = "") => Settings.GetObject(propertyName);
@ -111,7 +111,34 @@ namespace LibationFileManager
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); } public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Location for book storage. Includes destination of newly liberated books")] [Description("Location for book storage. Includes destination of newly liberated books")]
public LongPath? Books { get => GetString(); set => SetString(value); } public LongPath? Books {
get => GetString();
set
{
if (value != Books)
{
OnPropertyChanging(nameof(Books), Books, value);
Settings.SetString(nameof(Books), value);
m_BooksCanWrite255UnicodeChars = null;
m_BooksCanWriteWindowsInvalidChars = null;
OnPropertyChanged(nameof(Books), value);
}
}
}
private bool? m_BooksCanWrite255UnicodeChars;
private bool? m_BooksCanWriteWindowsInvalidChars;
/// <summary>
/// True if the Books directory can be written to with 255 unicode character filenames
/// <para/> Does not persist. Check and set this value at runtime and whenever Books is changed.
/// </summary>
public bool BooksCanWrite255UnicodeChars => m_BooksCanWrite255UnicodeChars ??= FileSystemTest.CanWrite255UnicodeChars(AudibleFileStorage.BooksDirectory);
/// <summary>
/// True if the Books directory can be written to with filenames containing characters invalid on Windows (:, *, ?, &lt;, &gt;, |)
/// <para/> Always false on Windows platforms.
/// <para/> Does not persist. Check and set this value at runtime and whenever Books is changed.
/// </summary>
public bool BooksCanWriteWindowsInvalidChars => !IsWindows && (m_BooksCanWriteWindowsInvalidChars ??= FileSystemTest.CanWriteWindowsInvalidChars(AudibleFileStorage.BooksDirectory));
[Description("Overwrite existing files if they already exist?")] [Description("Overwrite existing files if they already exist?")]
public bool OverwriteExisting { get => GetNonString(defaultValue: false); set => SetNonString(value); } public bool OverwriteExisting { get => GetNonString(defaultValue: false); set => SetNonString(value); }
@ -319,7 +346,7 @@ namespace LibationFileManager
#region templates: custom file naming #region templates: custom file naming
[Description("Edit how filename characters are replaced")] [Description("Edit how filename characters are replaced")]
public ReplacementCharacters ReplacementCharacters { get => GetNonString(defaultValue: ReplacementCharacters.Default); set => SetNonString(value); } public ReplacementCharacters ReplacementCharacters { get => GetNonString(defaultValue: ReplacementCharacters.Default(IsWindows)); set => SetNonString(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

@ -3,48 +3,58 @@ using System.IO;
using System.Linq; using System.Linq;
using Dinah.Core; using Dinah.Core;
using FileManager; using FileManager;
using Newtonsoft.Json.Linq;
#nullable enable #nullable enable
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration : PropertyChangeFilter public partial class Configuration : PropertyChangeFilter
{ {
/// <summary>
/// Returns true if <see cref="SettingsFilePath"/> exists and the <see cref="Books"/> property has a non-null, non-empty value.
/// Does not verify the existence of the <see cref="Books"/> directory.
/// </summary>
public bool LibationSettingsAreValid => SettingsFileIsValid(SettingsFilePath); public bool LibationSettingsAreValid => SettingsFileIsValid(SettingsFilePath);
/// <summary>
/// Returns true if <paramref name="settingsFile"/> exists and the <see cref="Books"/> property has a non-null, non-empty value.
/// Does not verify the existence of the <see cref="Books"/> directory.
/// </summary>
/// <param name="settingsFile">File path to the settings JSON file</param>
public static bool SettingsFileIsValid(string settingsFile) public static bool SettingsFileIsValid(string settingsFile)
{ {
if (!Directory.Exists(Path.GetDirectoryName(settingsFile)) || !File.Exists(settingsFile)) if (!Directory.Exists(Path.GetDirectoryName(settingsFile)) || !File.Exists(settingsFile))
return false; return false;
var pDic = new PersistentDictionary(settingsFile, isReadOnly: false); try
if (pDic.GetString(nameof(Books)) is not string booksDir)
return false;
if (!Directory.Exists(booksDir))
{ {
if (Path.GetDirectoryName(settingsFile) is not string dir) var settingsJson = JObject.Parse(File.ReadAllText(settingsFile));
throw new DirectoryNotFoundException(settingsFile); return !string.IsNullOrWhiteSpace(settingsJson[nameof(Books)]?.Value<string>());
}
//"Books" is not null, so setup has already been run. catch (Exception ex)
//Since Books can't be found, try to create it {
//and then revert to the default books directory Serilog.Log.Logger.Error(ex, "Failed to load settings file: {@SettingsFile}", settingsFile);
foreach (string d in new string[] { booksDir, DefaultBooksDirectory }) try
{ {
Serilog.Log.Logger.Information("Deleting invalid settings file: {@SettingsFile}", settingsFile);
FileUtility.SaferDelete(settingsFile);
Serilog.Log.Logger.Information("Creating a new, empty setting file: {@SettingsFile}", settingsFile);
try try
{ {
Directory.CreateDirectory(d); File.WriteAllText(settingsFile, "{}");
}
pDic.SetString(nameof(Books), d); catch (Exception createEx)
{
return Directory.Exists(d); Serilog.Log.Logger.Error(createEx, "Failed to create new settings file: {@SettingsFile}", settingsFile);
} }
catch { /* Do Nothing */ }
} }
catch (Exception deleteEx)
{
Serilog.Log.Logger.Error(deleteEx, "Failed to delete the invalid settings file: {@SettingsFile}", settingsFile);
}
return false; return false;
} }
return true;
} }
#region singleton stuff #region singleton stuff

View File

@ -7,9 +7,9 @@ public record SeriesDto : IFormattable
{ {
public string Name { get; } public string Name { get; }
public float? Number { get; } public string? Number { get; }
public string AudibleSeriesId { get; } public string AudibleSeriesId { get; }
public SeriesDto(string name, float? number, string audibleSeriesId) public SeriesDto(string name, string? number, string audibleSeriesId)
{ {
Name = name; Name = name;
Number = number; Number = number;

View File

@ -68,7 +68,7 @@ namespace LibationFileManager.Templates
YearPublished = 2017, YearPublished = 2017,
Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")], Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")],
Narrators = [new("Stephen Fry", null)], Narrators = [new("Stephen Fry", null)],
Series = [new("Sherlock Holmes", 1, "B08376S3R2"), new("Some Other Series", 1, "B000000000")], Series = [new("Sherlock Holmes", "1-6", "B08376S3R2"), new("Book Collection", "1", "B000000000")],
Codec = "AAC-LC", Codec = "AAC-LC",
LibationVersion = Configuration.LibationVersion?.ToVersionString(), LibationVersion = Configuration.LibationVersion?.ToVersionString(),
FileVersion = "36217811", FileVersion = "36217811",

View File

@ -157,7 +157,7 @@ namespace LibationFileManager.Templates
var maxFilenameLength = LongPath.MaxFilenameLength - var maxFilenameLength = LongPath.MaxFilenameLength -
(i < pathParts.Count - 1 || string.IsNullOrEmpty(fileExtension) ? 0 : fileExtension.Length + 5); (i < pathParts.Count - 1 || string.IsNullOrEmpty(fileExtension) ? 0 : fileExtension.Length + 5);
while (part.Sum(LongPath.GetFilesystemStringLength) > maxFilenameLength) while (part.Sum(GetFilenameLength) > maxFilenameLength)
{ {
int maxLength = part.Max(p => p.Length); int maxLength = part.Max(p => p.Length);
var maxEntry = part.First(p => p.Length == maxLength); var maxEntry = part.First(p => p.Length == maxLength);
@ -173,6 +173,10 @@ namespace LibationFileManager.Templates
return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting); return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting);
} }
private static int GetFilenameLength(string filename)
=> Configuration.Instance.BooksCanWrite255UnicodeChars ? filename.Length
: System.Text.Encoding.UTF8.GetByteCount(filename);
/// <summary> /// <summary>
/// Organize template parts into directories. Any Extra slashes will be /// Organize template parts into directories. Any Extra slashes will be
/// returned as empty directories and are taken care of by Path.Combine() /// returned as empty directories and are taken care of by Path.Combine()

View File

@ -1,5 +1,6 @@
using ApplicationServices; using ApplicationServices;
using DataLayer; using DataLayer;
using LibationFileManager;
using LibationUiBase.Forms; using LibationUiBase.Forms;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -95,6 +96,9 @@ public class ProcessQueueViewModel : ReactiveObject
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks) public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
{ {
if (!IsBooksDirectoryValid())
return false;
var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray(); var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray();
if (needsPdf.Length > 0) if (needsPdf.Length > 0)
{ {
@ -107,6 +111,9 @@ public class ProcessQueueViewModel : ReactiveObject
public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks) public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks)
{ {
if (!IsBooksDirectoryValid())
return false;
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing. //Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray(); var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray();
if (preLiberated.Length > 0) if (preLiberated.Length > 0)
@ -122,6 +129,9 @@ public class ProcessQueueViewModel : ReactiveObject
public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks) public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks)
{ {
if (!IsBooksDirectoryValid())
return false;
if (libraryBooks.Count == 1) if (libraryBooks.Count == 1)
{ {
var item = libraryBooks[0]; var item = libraryBooks[0];
@ -157,6 +167,32 @@ public class ProcessQueueViewModel : ReactiveObject
return false; return false;
} }
private bool IsBooksDirectoryValid()
{
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
{
Serilog.Log.Logger.Error("Books location is not set in configuration.");
MessageBoxBase.Show(
"Please choose a \"Books location\" folder in the Settings menu.",
"Books Directory Not Set",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return false;
}
else if (AudibleFileStorage.BooksDirectory is null)
{
Serilog.Log.Logger.Error("Failed to create books directory: {@booksDir}", Configuration.Instance.Books);
MessageBoxBase.Show(
$"Libation was unable to create the \"Books location\" folder at:\n{Configuration.Instance.Books}\n\nPlease change the Books location in the settings menu.",
"Failed to Create Books Directory",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return false;
}
return true;
}
private bool IsBookInQueue(LibraryBook libraryBook) private bool IsBookInQueue(LibraryBook libraryBook)
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModel entry ? false => Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModel entry ? false
: entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry) : entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry)

View File

@ -50,13 +50,13 @@ namespace LibationWinForms.Dialogs
} }
private void loFiDefaultsBtn_Click(object sender, EventArgs e) private void loFiDefaultsBtn_Click(object sender, EventArgs e)
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements); => LoadTable(ReplacementCharacters.LoFiDefault(ntfs: true).Replacements);
private void defaultsBtn_Click(object sender, EventArgs e) private void defaultsBtn_Click(object sender, EventArgs e)
=> LoadTable(ReplacementCharacters.Default.Replacements); => LoadTable(ReplacementCharacters.Default(ntfs: true).Replacements);
private void minDefaultBtn_Click(object sender, EventArgs e) private void minDefaultBtn_Click(object sender, EventArgs e)
=> LoadTable(ReplacementCharacters.Barebones.Replacements); => LoadTable(ReplacementCharacters.Barebones(ntfs: true).Replacements);
private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)

View File

@ -28,161 +28,168 @@
/// </summary> /// </summary>
private void InitializeComponent() private void InitializeComponent()
{ {
this.saveBtn = new System.Windows.Forms.Button(); saveBtn = new System.Windows.Forms.Button();
this.cancelBtn = new System.Windows.Forms.Button(); cancelBtn = new System.Windows.Forms.Button();
this.templateTb = new System.Windows.Forms.TextBox(); templateTb = new System.Windows.Forms.TextBox();
this.templateLbl = new System.Windows.Forms.Label(); templateLbl = new System.Windows.Forms.Label();
this.resetToDefaultBtn = new System.Windows.Forms.Button(); resetToDefaultBtn = new System.Windows.Forms.Button();
this.listView1 = new System.Windows.Forms.ListView(); listView1 = new System.Windows.Forms.ListView();
this.columnHeader1 = new System.Windows.Forms.ColumnHeader(); columnHeader1 = new System.Windows.Forms.ColumnHeader();
this.columnHeader2 = new System.Windows.Forms.ColumnHeader(); columnHeader2 = new System.Windows.Forms.ColumnHeader();
this.richTextBox1 = new System.Windows.Forms.RichTextBox(); richTextBox1 = new System.Windows.Forms.RichTextBox();
this.warningsLbl = new System.Windows.Forms.Label(); warningsLbl = new System.Windows.Forms.Label();
this.exampleLbl = new System.Windows.Forms.Label(); exampleLbl = new System.Windows.Forms.Label();
this.SuspendLayout(); llblGoToWiki = new System.Windows.Forms.LinkLabel();
SuspendLayout();
// //
// saveBtn // saveBtn
// //
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); saveBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
this.saveBtn.Location = new System.Drawing.Point(714, 345); saveBtn.Location = new System.Drawing.Point(714, 345);
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.saveBtn.Name = "saveBtn"; saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(88, 27); saveBtn.Size = new System.Drawing.Size(88, 27);
this.saveBtn.TabIndex = 98; saveBtn.TabIndex = 98;
this.saveBtn.Text = "Save"; saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true; saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click); saveBtn.Click += saveBtn_Click;
// //
// cancelBtn // cancelBtn
// //
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); cancelBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel; cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(832, 345); cancelBtn.Location = new System.Drawing.Point(832, 345);
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.cancelBtn.Name = "cancelBtn"; cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(88, 27); cancelBtn.Size = new System.Drawing.Size(88, 27);
this.cancelBtn.TabIndex = 99; cancelBtn.TabIndex = 99;
this.cancelBtn.Text = "Cancel"; cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true; cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click); cancelBtn.Click += cancelBtn_Click;
// //
// templateTb // templateTb
// //
this.templateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) templateTb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
| System.Windows.Forms.AnchorStyles.Right))); templateTb.Location = new System.Drawing.Point(12, 27);
this.templateTb.Location = new System.Drawing.Point(12, 27); templateTb.Name = "templateTb";
this.templateTb.Name = "templateTb"; templateTb.Size = new System.Drawing.Size(779, 23);
this.templateTb.Size = new System.Drawing.Size(779, 23); templateTb.TabIndex = 1;
this.templateTb.TabIndex = 1; templateTb.TextChanged += templateTb_TextChanged;
this.templateTb.TextChanged += new System.EventHandler(this.templateTb_TextChanged);
// //
// templateLbl // templateLbl
// //
this.templateLbl.AutoSize = true; templateLbl.AutoSize = true;
this.templateLbl.Location = new System.Drawing.Point(12, 9); templateLbl.Location = new System.Drawing.Point(12, 9);
this.templateLbl.Name = "templateLbl"; templateLbl.Name = "templateLbl";
this.templateLbl.Size = new System.Drawing.Size(89, 15); templateLbl.Size = new System.Drawing.Size(89, 15);
this.templateLbl.TabIndex = 0; templateLbl.TabIndex = 0;
this.templateLbl.Text = "[template desc]"; templateLbl.Text = "[template desc]";
// //
// resetToDefaultBtn // resetToDefaultBtn
// //
this.resetToDefaultBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); resetToDefaultBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
this.resetToDefaultBtn.Location = new System.Drawing.Point(797, 26); resetToDefaultBtn.Location = new System.Drawing.Point(797, 26);
this.resetToDefaultBtn.Name = "resetToDefaultBtn"; resetToDefaultBtn.Name = "resetToDefaultBtn";
this.resetToDefaultBtn.Size = new System.Drawing.Size(124, 23); resetToDefaultBtn.Size = new System.Drawing.Size(124, 23);
this.resetToDefaultBtn.TabIndex = 2; resetToDefaultBtn.TabIndex = 2;
this.resetToDefaultBtn.Text = "Reset to default"; resetToDefaultBtn.Text = "Reset to default";
this.resetToDefaultBtn.UseVisualStyleBackColor = true; resetToDefaultBtn.UseVisualStyleBackColor = true;
this.resetToDefaultBtn.Click += new System.EventHandler(this.resetToDefaultBtn_Click); resetToDefaultBtn.Click += resetToDefaultBtn_Click;
// //
// listView1 // listView1
// //
this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) listView1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
| System.Windows.Forms.AnchorStyles.Left))); listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { columnHeader1, columnHeader2 });
this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { listView1.FullRowSelect = true;
this.columnHeader1, listView1.GridLines = true;
this.columnHeader2}); listView1.Location = new System.Drawing.Point(12, 56);
this.listView1.FullRowSelect = true; listView1.MultiSelect = false;
this.listView1.GridLines = true; listView1.Name = "listView1";
this.listView1.Location = new System.Drawing.Point(12, 56); listView1.Size = new System.Drawing.Size(328, 283);
this.listView1.MultiSelect = false; listView1.TabIndex = 3;
this.listView1.Name = "listView1"; listView1.UseCompatibleStateImageBehavior = false;
this.listView1.Size = new System.Drawing.Size(328, 283); listView1.View = System.Windows.Forms.View.Details;
this.listView1.TabIndex = 3; listView1.DoubleClick += listView1_DoubleClick;
this.listView1.UseCompatibleStateImageBehavior = false;
this.listView1.View = System.Windows.Forms.View.Details;
this.listView1.DoubleClick += new System.EventHandler(this.listView1_DoubleClick);
// //
// columnHeader1 // columnHeader1
// //
this.columnHeader1.Text = "Tag"; columnHeader1.Text = "Tag";
this.columnHeader1.Width = 137; columnHeader1.Width = 137;
// //
// columnHeader2 // columnHeader2
// //
this.columnHeader2.Text = "Description"; columnHeader2.Text = "Description";
this.columnHeader2.Width = 170; columnHeader2.Width = 170;
// //
// richTextBox1 // richTextBox1
// //
this.richTextBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) richTextBox1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
| System.Windows.Forms.AnchorStyles.Left) richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
| System.Windows.Forms.AnchorStyles.Right))); richTextBox1.Location = new System.Drawing.Point(346, 74);
this.richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; richTextBox1.Name = "richTextBox1";
this.richTextBox1.Location = new System.Drawing.Point(346, 74); richTextBox1.ReadOnly = true;
this.richTextBox1.Name = "richTextBox1"; richTextBox1.Size = new System.Drawing.Size(574, 185);
this.richTextBox1.ReadOnly = true; richTextBox1.TabIndex = 5;
this.richTextBox1.Size = new System.Drawing.Size(574, 185); richTextBox1.Text = "";
this.richTextBox1.TabIndex = 5;
this.richTextBox1.Text = "";
// //
// warningsLbl // warningsLbl
// //
this.warningsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); warningsLbl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
this.warningsLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); warningsLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
this.warningsLbl.ForeColor = System.Drawing.Color.Firebrick; warningsLbl.ForeColor = System.Drawing.Color.Firebrick;
this.warningsLbl.Location = new System.Drawing.Point(346, 262); warningsLbl.Location = new System.Drawing.Point(346, 262);
this.warningsLbl.Name = "warningsLbl"; warningsLbl.Name = "warningsLbl";
this.warningsLbl.Size = new System.Drawing.Size(574, 77); warningsLbl.Size = new System.Drawing.Size(574, 77);
this.warningsLbl.TabIndex = 6; warningsLbl.TabIndex = 6;
this.warningsLbl.Text = "[warnings]"; warningsLbl.Text = "[warnings]";
// //
// exampleLbl // exampleLbl
// //
this.exampleLbl.AutoSize = true; exampleLbl.AutoSize = true;
this.exampleLbl.Location = new System.Drawing.Point(346, 56); exampleLbl.Location = new System.Drawing.Point(346, 56);
this.exampleLbl.Name = "exampleLbl"; exampleLbl.Name = "exampleLbl";
this.exampleLbl.Size = new System.Drawing.Size(55, 15); exampleLbl.Size = new System.Drawing.Size(54, 15);
this.exampleLbl.TabIndex = 4; exampleLbl.TabIndex = 4;
this.exampleLbl.Text = "Example:"; exampleLbl.Text = "Example:";
//
// llblGoToWiki
//
llblGoToWiki.AutoSize = true;
llblGoToWiki.Location = new System.Drawing.Point(12, 357);
llblGoToWiki.Name = "llblGoToWiki";
llblGoToWiki.Size = new System.Drawing.Size(229, 15);
llblGoToWiki.TabIndex = 100;
llblGoToWiki.TabStop = true;
llblGoToWiki.Text = "Read about naming templates on the Wiki";
llblGoToWiki.LinkClicked += llblGoToWiki_LinkClicked;
// //
// EditTemplateDialog // EditTemplateDialog
// //
this.AcceptButton = this.saveBtn; AcceptButton = saveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.CancelButton = this.cancelBtn; CancelButton = cancelBtn;
this.ClientSize = new System.Drawing.Size(933, 388); ClientSize = new System.Drawing.Size(933, 388);
this.Controls.Add(this.exampleLbl); Controls.Add(llblGoToWiki);
this.Controls.Add(this.warningsLbl); Controls.Add(exampleLbl);
this.Controls.Add(this.richTextBox1); Controls.Add(warningsLbl);
this.Controls.Add(this.listView1); Controls.Add(richTextBox1);
this.Controls.Add(this.resetToDefaultBtn); Controls.Add(listView1);
this.Controls.Add(this.templateLbl); Controls.Add(resetToDefaultBtn);
this.Controls.Add(this.templateTb); Controls.Add(templateLbl);
this.Controls.Add(this.cancelBtn); Controls.Add(templateTb);
this.Controls.Add(this.saveBtn); Controls.Add(cancelBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; Controls.Add(saveBtn);
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false; Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MinimizeBox = false; MaximizeBox = false;
this.Name = "EditTemplateDialog"; MinimizeBox = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; Name = "EditTemplateDialog";
this.Text = "Edit Template"; StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Load += new System.EventHandler(this.EditTemplateDialog_Load); Text = "Edit Template";
this.ResumeLayout(false); Load += EditTemplateDialog_Load;
this.PerformLayout(); ResumeLayout(false);
PerformLayout();
} }
@ -198,5 +205,6 @@
private System.Windows.Forms.RichTextBox richTextBox1; private System.Windows.Forms.RichTextBox richTextBox1;
private System.Windows.Forms.Label warningsLbl; private System.Windows.Forms.Label warningsLbl;
private System.Windows.Forms.Label exampleLbl; private System.Windows.Forms.Label exampleLbl;
private System.Windows.Forms.LinkLabel llblGoToWiki;
} }
} }

View File

@ -12,7 +12,7 @@ namespace LibationWinForms.Dialogs
{ {
private void resetTextBox(string value) => this.templateTb.Text = value; private void resetTextBox(string value) => this.templateTb.Text = value;
private Configuration config { get; } = Configuration.Instance; private Configuration config { get; } = Configuration.Instance;
private ITemplateEditor templateEditor { get;} private ITemplateEditor templateEditor { get; }
public EditTemplateDialog() public EditTemplateDialog()
{ {
@ -150,5 +150,11 @@ namespace LibationWinForms.Dialogs
templateTb.Text = text.Insert(selStart, itemText); templateTb.Text = text.Insert(selStart, itemText);
templateTb.SelectionStart = selStart + itemText.Length; templateTb.SelectionStart = selStart + itemText.Length;
} }
private void llblGoToWiki_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
Go.To.Url(@"ht" + "tps://github.com/rmcrackan/Libation/blob/master/Documentation/NamingTemplates.md");
e.Link.Visited = true;
}
} }
} }

View File

@ -1,4 +1,64 @@
<root> <?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <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:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">

View File

@ -83,9 +83,11 @@ namespace LibationWinForms.Dialogs
if (lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated) if (lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated)); await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated));
tokenSource.Token.ThrowIfCancellationRequested();
this.Invoke(FileFound, this, book); this.Invoke(FileFound, this, book);
} }
catch(Exception ex) catch (OperationCanceledException) { }
catch (Exception ex)
{ {
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book); Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);
} }

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
#nullable enable
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
{ {
public partial class SettingsDialog public partial class SettingsDialog
@ -55,7 +56,7 @@ namespace LibationWinForms.Dialogs
}, },
Configuration.KnownDirectories.UserProfile, Configuration.KnownDirectories.UserProfile,
"Books"); "Books");
booksSelectControl.SelectDirectory(config.Books.PathWithoutPrefix); booksSelectControl.SelectDirectory(config.Books?.PathWithoutPrefix ?? "");
saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder; saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder;
overwriteExistingCbox.Checked = config.OverwriteExisting; overwriteExistingCbox.Checked = config.OverwriteExisting;
@ -63,7 +64,7 @@ namespace LibationWinForms.Dialogs
gridFontScaleFactorTbar.Value = scaleFactorToLinearRange(config.GridFontScaleFactor); gridFontScaleFactorTbar.Value = scaleFactorToLinearRange(config.GridFontScaleFactor);
} }
private void Save_Important(Configuration config) private bool Save_Important(Configuration config)
{ {
var newBooks = booksSelectControl.SelectedDirectory; var newBooks = booksSelectControl.SelectedDirectory;
@ -73,19 +74,29 @@ namespace LibationWinForms.Dialogs
if (string.IsNullOrWhiteSpace(newBooks)) if (string.IsNullOrWhiteSpace(newBooks))
{ {
validationError("Cannot set Books Location to blank", "Location is blank"); validationError("Cannot set Books Location to blank", "Location is blank");
return; return false;
}
LongPath lonNewBooks = newBooks;
if (!Directory.Exists(lonNewBooks))
{
try
{
Directory.CreateDirectory(lonNewBooks);
}
catch (Exception ex)
{
validationError($"Error creating Books Location:\r\n{ex.Message}", "Error creating directory");
return false;
}
} }
#endregion #endregion
LongPath lonNewBooks = newBooks;
if (!Directory.Exists(lonNewBooks))
Directory.CreateDirectory(lonNewBooks);
config.Books = newBooks; config.Books = newBooks;
{ {
var logLevelOld = config.LogLevel; var logLevelOld = config.LogLevel;
var logLevelNew = (Serilog.Events.LogEventLevel)loggingLevelCb.SelectedItem; var logLevelNew = (loggingLevelCb.SelectedItem as Serilog.Events.LogEventLevel?) ?? Serilog.Events.LogEventLevel.Information;
config.LogLevel = logLevelNew; config.LogLevel = logLevelNew;
@ -97,9 +108,9 @@ namespace LibationWinForms.Dialogs
config.SavePodcastsToParentFolder = saveEpisodesToSeriesFolderCbox.Checked; config.SavePodcastsToParentFolder = saveEpisodesToSeriesFolderCbox.Checked;
config.OverwriteExisting = overwriteExistingCbox.Checked; config.OverwriteExisting = overwriteExistingCbox.Checked;
config.CreationTime = (creationTimeCb.SelectedItem as EnumDisplay<Configuration.DateTimeSource>)?.Value ?? Configuration.DateTimeSource.File;
config.CreationTime = ((EnumDisplay<Configuration.DateTimeSource>)creationTimeCb.SelectedItem).Value; config.LastWriteTime = (lastWriteTimeCb.SelectedItem as EnumDisplay<Configuration.DateTimeSource>)?.Value ?? Configuration.DateTimeSource.File;
config.LastWriteTime = ((EnumDisplay<Configuration.DateTimeSource>)lastWriteTimeCb.SelectedItem).Value; return true;
} }
private static int scaleFactorToLinearRange(float scaleFactor) private static int scaleFactorToLinearRange(float scaleFactor)

View File

@ -43,7 +43,7 @@ namespace LibationWinForms.Dialogs
private void saveBtn_Click(object sender, EventArgs e) private void saveBtn_Click(object sender, EventArgs e)
{ {
Save_Important(config); if (!Save_Important(config)) return;
Save_ImportLibrary(config); Save_ImportLibrary(config);
Save_DownloadDecrypt(config); Save_DownloadDecrypt(config);
Save_AudioSettings(config); Save_AudioSettings(config);

View File

@ -16,7 +16,8 @@ namespace LibationWinForms
private void Configure_ScanManual() private void Configure_ScanManual()
{ {
this.Load += refreshImportMenu; this.Load += refreshImportMenu;
AccountsSettingsPersister.Saved += refreshImportMenu; AccountsSettingsPersister.Saved += (_, _) => Invoke(refreshImportMenu, null, null);
locateAudiobooksToolStripMenuItem.ToolTipText = Configuration.GetHelpText("LocateAudiobooks");
} }
private void refreshImportMenu(object _, EventArgs __) private void refreshImportMenu(object _, EventArgs __)
@ -96,7 +97,16 @@ namespace LibationWinForms
private void locateAudiobooksToolStripMenuItem_Click(object sender, EventArgs e) private void locateAudiobooksToolStripMenuItem_Click(object sender, EventArgs e)
{ {
new LocateAudiobooksDialog().ShowDialog(); var result = MessageBox.Show(
this,
Configuration.GetHelpText(nameof(LocateAudiobooksDialog)),
"Locate Previously-Liberated Audiobook Files",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Information,
MessageBoxDefaultButton.Button1);
if (result is DialogResult.OK)
new LocateAudiobooksDialog().ShowDialog();
} }
} }
} }

View File

@ -6,9 +6,29 @@ namespace LibationWinForms
{ {
public partial class Form1 public partial class Form1
{ {
private void Configure_Settings() { } private void Configure_Settings()
{
Shown += FormShown_Settings;
}
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog().ShowDialog(); private void FormShown_Settings(object sender, EventArgs e)
{
if (LibationFileManager.AudibleFileStorage.BooksDirectory is null)
{
var result = MessageBox.Show(
this,
"Please set a valid Books location in the settings dialog.",
"Books Directory Not Set",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button1);
if (result is DialogResult.OK)
new SettingsDialog().ShowDialog(this);
}
}
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog().ShowDialog();
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog(); private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();

View File

@ -47,7 +47,7 @@ namespace LibationWinForms
FileUtility.SaferMoveToValidPath( FileUtility.SaferMoveToValidPath(
e.SettingsFilePath, e.SettingsFilePath,
e.SettingsFilePath, e.SettingsFilePath,
ReplacementCharacters.Barebones, Configuration.Instance.ReplacementCharacters,
"bak"); "bak");
AudibleApiStorage.EnsureAccountsSettingsFileExists(); AudibleApiStorage.EnsureAccountsSettingsFileExists();

View File

@ -148,7 +148,10 @@ namespace LibationWinForms
} }
if (setupDialog.IsNewUser) if (setupDialog.IsNewUser)
{
Configuration.SetLibationFiles(defaultLibationFilesDir); Configuration.SetLibationFiles(defaultLibationFilesDir);
config.Books = Configuration.DefaultBooksDirectory;
}
else if (setupDialog.IsReturningUser) else if (setupDialog.IsReturningUser)
{ {
var libationFilesDialog = new LibationFilesDialog(); var libationFilesDialog = new LibationFilesDialog();
@ -175,16 +178,11 @@ namespace LibationWinForms
CancelInstallation(); CancelInstallation();
return; return;
} }
config.Books = Configuration.DefaultBooksDirectory;
} }
// INIT DEFAULT SETTINGS if (!config.LibationSettingsAreValid)
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog CancelInstallation();
config.Books ??= Configuration.DefaultBooksDirectory;
if (config.LibationSettingsAreValid)
return;
CancelInstallation();
} }
/// <summary>migrations which require Forms or are long-running</summary> /// <summary>migrations which require Forms or are long-running</summary>

View File

@ -1,5 +1,6 @@
using ApplicationServices; using ApplicationServices;
using AudibleUtilities; using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.StepRunner; using Dinah.Core.StepRunner;
using LibationFileManager; using LibationFileManager;
using LibationWinForms.Dialogs; using LibationWinForms.Dialogs;
@ -163,7 +164,7 @@ namespace LibationWinForms
var books = DbContexts.GetLibrary_Flat_NoTracking(); var books = DbContexts.GetLibrary_Flat_NoTracking();
if (books.Count == 0) return true; if (books.Count == 0) return true;
var firstAuthor = getFirstAuthor(); var firstAuthor = getFirstAuthor()?.SurroundWithQuotes();
if (firstAuthor == null) return true; if (firstAuthor == null) return true;
if (!ProceedMessageBox("You can filter the grid entries by searching", "Searching")) if (!ProceedMessageBox("You can filter the grid entries by searching", "Searching"))
@ -196,7 +197,7 @@ namespace LibationWinForms
private async Task<bool> ShowQuickFilters() private async Task<bool> ShowQuickFilters()
{ {
var firstAuthor = getFirstAuthor(); var firstAuthor = getFirstAuthor()?.SurroundWithQuotes();
if (firstAuthor == null) return true; if (firstAuthor == null) return true;
if (!ProceedMessageBox("Queries that you perform regularly can be added to 'Quick Filters'", "Quick Filters")) if (!ProceedMessageBox("Queries that you perform regularly can be added to 'Quick Filters'", "Quick Filters"))

View File

@ -12,9 +12,9 @@ namespace FileUtilityTests
[TestClass] [TestClass]
public class GetSafePath public class GetSafePath
{ {
static readonly ReplacementCharacters Default = ReplacementCharacters.Default; static readonly ReplacementCharacters Default = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault; static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault(Environment.OSVersion.Platform == PlatformID.Win32NT);
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones; static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod] [TestMethod]
public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default)); public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default));
@ -98,9 +98,9 @@ namespace FileUtilityTests
[TestClass] [TestClass]
public class GetSafeFileName public class GetSafeFileName
{ {
static readonly ReplacementCharacters Default = ReplacementCharacters.Default; static readonly ReplacementCharacters Default = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault; static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault(Environment.OSVersion.Platform == PlatformID.Win32NT);
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones; static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones(Environment.OSVersion.Platform == PlatformID.Win32NT);
// 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]
@ -193,7 +193,7 @@ namespace FileUtilityTests
[TestClass] [TestClass]
public class GetValidFilename public class GetValidFilename
{ {
static ReplacementCharacters Replacements = ReplacementCharacters.Default; static ReplacementCharacters Replacements = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod] [TestMethod]
// dot-files // dot-files

View File

@ -23,8 +23,17 @@ namespace TemplatesTests
public static class Shared public static class Shared
{ {
[System.Runtime.CompilerServices.ModuleInitializer]
public static void Init()
{
var thisDir = Path.GetDirectoryName(Environment.ProcessPath);
LibationFileManager.Configuration.SetLibationFiles(thisDir);
if (!LibationFileManager.Configuration.Instance.LibationSettingsAreValid)
LibationFileManager.Configuration.Instance.Books = Path.Combine(thisDir, "Books");
}
public static LibraryBookDto GetLibraryBook() public static LibraryBookDto GetLibraryBook()
=> GetLibraryBook([new SeriesDto("Sherlock Holmes", 1, "B08376S3R2")]); => GetLibraryBook([new SeriesDto("Sherlock Holmes", "1", "B08376S3R2")]);
public static LibraryBookDto GetLibraryBook(IEnumerable<SeriesDto> series) public static LibraryBookDto GetLibraryBook(IEnumerable<SeriesDto> series)
=> new() => new()
@ -66,7 +75,7 @@ namespace TemplatesTests
[TestClass] [TestClass]
public class getFileNamingTemplate public class getFileNamingTemplate
{ {
static ReplacementCharacters Replacements = ReplacementCharacters.Default; static ReplacementCharacters Replacements = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod] [TestMethod]
[DataRow(null)] [DataRow(null)]
@ -367,12 +376,13 @@ namespace TemplatesTests
[TestMethod] [TestMethod]
[DataRow("<series>", "Series A, Series B, Series C")] [DataRow("<series>", "Series A, Series B, Series C, Series D")]
[DataRow("<series[]>", "Series A, Series B, Series C")] [DataRow("<series[]>", "Series A, Series B, Series C, Series D")]
[DataRow("<series[max(1)]>", "Series A")] [DataRow("<series[max(1)]>", "Series A")]
[DataRow("<series[max(2)]>", "Series A, Series B")] [DataRow("<series[max(2)]>", "Series A, Series B")]
[DataRow("<series[max(3)]>", "Series A, Series B, Series C")] [DataRow("<series[max(3)]>", "Series A, Series B, Series C")]
[DataRow("<series[format({N}, {#}, {ID}) separator(; )]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3")] [DataRow("<series[max(4)]>", "Series A, Series B, Series C, Series D")]
[DataRow("<series[format({N}, {#}, {ID}) separator(; )]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3; Series D, 1-5, B4")]
[DataRow("<series[format({N}, {#}, {ID}) separator(; ) max(3)]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3")] [DataRow("<series[format({N}, {#}, {ID}) separator(; ) max(3)]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3")]
[DataRow("<series[format({N}, {#}, {ID}) separator(; ) max(2)]>", "Series A, 1, B1; Series B, 6, B2")] [DataRow("<series[format({N}, {#}, {ID}) separator(; ) max(2)]>", "Series A, 1, B1; Series B, 6, B2")]
[DataRow("<first series>", "Series A")] [DataRow("<first series>", "Series A")]
@ -383,9 +393,10 @@ namespace TemplatesTests
var bookDto = GetLibraryBook(); var bookDto = GetLibraryBook();
bookDto.Series = bookDto.Series =
[ [
new("Series A", 1, "B1"), new("Series A", "1", "B1"),
new("Series B", 6, "B2"), new("Series B", "6", "B2"),
new("Series C", 2, "B3") new("Series C", "2", "B3"),
new("Series D", "1-5", "B4"),
]; ];
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue(); Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
@ -451,7 +462,7 @@ namespace Templates_Other
[TestClass] [TestClass]
public class GetFilePath public class GetFilePath
{ {
static ReplacementCharacters Replacements = ReplacementCharacters.Default; static ReplacementCharacters Replacements = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod] [TestMethod]
[DataRow(@"C:\foo\bar", @"\\Folder\<title>\[<id>]\\", @"C:\foo\bar\Folder\my_ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\[ID123456].txt", PlatformID.Win32NT)] [DataRow(@"C:\foo\bar", @"\\Folder\<title>\[<id>]\\", @"C:\foo\bar\Folder\my_ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\[ID123456].txt", PlatformID.Win32NT)]
@ -887,7 +898,7 @@ namespace Templates_ChapterFile_Tests
[TestClass] [TestClass]
public class GetFilename public class GetFilename
{ {
static readonly ReplacementCharacters Default = ReplacementCharacters.Default; static readonly ReplacementCharacters Default = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod] [TestMethod]
[DataRow("[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt", PlatformID.Win32NT)] [DataRow("[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt", PlatformID.Win32NT)]