Merge pull request #631 from Mbucari/master

Add features #626 and #627 and Fix #628
This commit is contained in:
rmcrackan 2023-06-14 14:03:24 -04:00 committed by GitHub
commit 8221d7e202
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1195 additions and 1061 deletions

View File

@ -73,7 +73,14 @@ namespace FileLiberator
}
else
{
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters, "mp3");
var realMp3Path
= FileUtility.SaferMoveToValidPath(
mp3File.Name,
proposedMp3Path,
Configuration.Instance.ReplacementCharacters,
extension: "mp3",
Configuration.Instance.OverwriteExisting);
OnFileCreated(libraryBook, realMp3Path);
}
}

View File

@ -76,15 +76,32 @@ namespace FileLiberator
var finalStorageDir = getDestinationDirectory(libraryBook);
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
Task[] finalTasks = new[]
{
Task.Run(() => downloadCoverArt(libraryBook)),
Task.Run(() => moveFilesToBooksDir(libraryBook, entries)),
Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion)),
moveFilesTask,
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir))
};
try
{
await Task.WhenAll(finalTasks);
}
catch
{
//Swallow downloadCoverArt and SetCoverAsFolderIcon exceptions.
//Only fail if the downloaded audio files failed to move to Books directory
if (moveFilesTask.IsFaulted)
{
throw;
}
}
finally
{
if (moveFilesTask.IsCompletedSuccessfully)
await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion));
}
return new StatusHandler();
}
@ -343,7 +360,13 @@ namespace FileLiberator
{
var entry = entries[i];
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters);
var realDest
= FileUtility.SaferMoveToValidPath(
entry.Path,
Path.Combine(destinationDir, Path.GetFileName(entry.Path)),
Configuration.Instance.ReplacementCharacters,
overwrite: Configuration.Instance.OverwriteExisting);
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
// propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleUtilities;
using DataLayer;
using Dinah.Core;
using LibationFileManager;
@ -20,13 +21,22 @@ namespace FileLiberator
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
{
var apiExtended = await AudibleUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
var apiExtended = await ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
return apiExtended.Api;
}
public static LibraryBookDto ToDto(this LibraryBook libraryBook) => new()
public static LibraryBookDto ToDto(this LibraryBook libraryBook)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var nickname
= persister.AccountsSettings.Accounts
.FirstOrDefault(a => a.AccountId == libraryBook.Account)
?.AccountName;
return new()
{
Account = libraryBook.Account,
AccountNickname = nickname,
DateAdded = libraryBook.DateAdded,
AudibleProductId = libraryBook.Book.AudibleProductId,
@ -50,4 +60,5 @@ namespace FileLiberator
Language = libraryBook.Book.Language
};
}
}
}

View File

@ -6,7 +6,6 @@ using System.Text.RegularExpressions;
using Dinah.Core;
using Polly;
using Polly.Retry;
using Dinah.Core.Collections.Generic;
namespace FileManager
{
@ -147,14 +146,24 @@ namespace FileManager
/// <summary>
/// Move file.
/// <br/>- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length
/// <br/>- Ensure valid file name path: remove invalid chars, enforce max file length
/// <br/>- Perform <see cref="SaferMove"/>
/// <br/>- Return valid path
/// </summary>
public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements, string extension = null)
/// <param name="source">Name of the file to move</param>
/// <param name="destination">The new path and name for the file.</param>
/// <param name="replacements">Rules for replacing illegal file path characters</param>
/// <param name="extension">File extension override to use for <paramref name="destination"/></param>
/// <param name="overwrite">If <c>false</c> and <paramref name="destination"/> exists, append " (n)" to filename and try again.</param>
/// <returns>The actual destination filename</returns>
public static string SaferMoveToValidPath(
LongPath source,
LongPath destination,
ReplacementCharacters replacements,
string extension = null,
bool overwrite = false)
{
extension = extension ?? Path.GetExtension(source);
destination = GetValidFilename(destination, replacements, extension);
extension ??= Path.GetExtension(source);
destination = GetValidFilename(destination, replacements, extension, overwrite);
SaferMove(source, destination);
return destination;
}

View File

@ -28,6 +28,10 @@
<TextBlock Text="{CompiledBinding SavePodcastsToParentFolderText}" />
</CheckBox>
<CheckBox IsChecked="{CompiledBinding OverwriteExisting, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding OverwriteExistingText}" />
</CheckBox>
</StackPanel>
</controls:GroupBox>

View File

@ -21,6 +21,7 @@ namespace LibationAvalonia.ViewModels.Settings
{
BooksDirectory = config.Books.PathWithoutPrefix;
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
OverwriteExisting = config.OverwriteExisting;
LoggingLevel = config.LogLevel;
ThemeVariant = initialThemeVariant
= Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) is nameof(Avalonia.Styling.ThemeVariant.Dark)
@ -35,6 +36,7 @@ namespace LibationAvalonia.ViewModels.Settings
System.IO.Directory.CreateDirectory(lonNewBooks);
config.Books = lonNewBooks;
config.SavePodcastsToParentFolder = SavePodcastsToParentFolder;
config.OverwriteExisting = OverwriteExisting;
config.LogLevel = LoggingLevel;
Configuration.Instance.SetString(ThemeVariant, nameof(ThemeVariant));
}
@ -50,12 +52,14 @@ namespace LibationAvalonia.ViewModels.Settings
public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));
public string SavePodcastsToParentFolderText { get; } = Configuration.GetDescription(nameof(Configuration.SavePodcastsToParentFolder));
public string OverwriteExistingText { get; } = Configuration.GetDescription(nameof(Configuration.OverwriteExisting));
public Serilog.Events.LogEventLevel[] LoggingLevels { get; } = Enum.GetValues<Serilog.Events.LogEventLevel>();
public string BetaOptInText { get; } = Configuration.GetDescription(nameof(Configuration.BetaOptIn));
public string[] Themes { get; } = { nameof(Avalonia.Styling.ThemeVariant.Light), nameof(Avalonia.Styling.ThemeVariant.Dark) };
public string BooksDirectory { get; set; }
public bool SavePodcastsToParentFolder { get; set; }
public bool OverwriteExisting { get; set; }
public Serilog.Events.LogEventLevel LoggingLevel { get; set; }
public string ThemeVariant

View File

@ -18,11 +18,11 @@ namespace LibationCli
{
Environment.ExitCode = (int)ExitCode.RunTimeError;
Console.WriteLine("ERROR");
Console.WriteLine("=====");
Console.WriteLine(ex.Message);
Console.WriteLine();
Console.WriteLine(ex.StackTrace);
Console.Error.WriteLine("ERROR");
Console.Error.WriteLine("=====");
Console.Error.WriteLine(ex.Message);
Console.Error.WriteLine();
Console.Error.WriteLine(ex.StackTrace);
}
}

View File

@ -27,6 +27,7 @@ namespace LibationCli
}
catch (Exception ex)
{
Console.Error.WriteLine("CLI error. See log for more details.");
Serilog.Log.Logger.Error(ex, "CLI error");
}
};
@ -54,12 +55,15 @@ namespace LibationCli
return;
foreach (var errorMessage in statusHandler.Errors)
{
Console.Error.WriteLine(errorMessage);
Serilog.Log.Logger.Error(errorMessage);
}
}
catch (Exception ex)
{
var msg = "Error processing book. Skipping. This book will be tried again on next attempt. For options of skipping or marking as error, retry with main Libation app.";
Console.WriteLine(msg + ". See log for more details.");
Console.Error.WriteLine(msg + ". See log for more details.");
Serilog.Log.Logger.Error(ex, $"{msg} {{@DebugInfo}}", new { Book = libraryBook.LogFriendly() });
}
}

View File

@ -70,7 +70,7 @@ namespace LibationCli
if (errorsList.Any(e => e.Tag.In(ErrorType.NoVerbSelectedError)))
{
Console.WriteLine("No verb selected");
Console.Error.WriteLine("No verb selected");
return;
}

View File

@ -82,6 +82,9 @@ namespace LibationFileManager
[Description("Location for book storage. Includes destination of newly liberated books")]
public LongPath Books { get => GetString(); set => SetString(value); }
[Description("Overwrite existing files if they already exist?")]
public bool OverwriteExisting { get => GetNonString(defaultValue: false); set => SetNonString(value); }
// temp/working dir(s) should be outside of dropbox
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
public string InProgress { get

View File

@ -37,5 +37,6 @@ namespace LibationFileManager
{
public DateTime? DateAdded { get; set; }
public string Account { get; set; }
public string AccountNickname { get; set; }
}
}

View File

@ -52,7 +52,8 @@ namespace LibationFileManager
private static readonly LibraryBookDto libraryBookDto
= new()
{
Account = "my account",
Account = "myaccount@example.co",
AccountNickname = "my account",
DateAdded = new DateTime(2022, 6, 9, 0, 0, 0),
DatePublished = new DateTime(2017, 2, 27, 0, 0, 0),
AudibleProductId = "123456789",

View File

@ -37,6 +37,7 @@ namespace LibationFileManager
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate");
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels");
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book");
public static TemplateTags Locale { get; } = new ("locale", "Region/country");
public static TemplateTags YearPublished { get; } = new("year", "Year published");
public static TemplateTags Language { get; } = new("language", "Book's language");

View File

@ -215,6 +215,7 @@ namespace LibationFileManager
{ TemplateTags.SampleRate, lb => (int?)(lb.IsPodcastParent ? null : lb.SampleRate) },
{ TemplateTags.Channels, lb => (int?)(lb.IsPodcastParent ? null : lb.Channels) },
{ TemplateTags.Account, lb => lb.Account },
{ TemplateTags.AccountNickname, lb => lb.AccountNickname },
{ TemplateTags.Locale, lb => lb.Locale },
{ TemplateTags.YearPublished, lb => lb.YearPublished },
{ TemplateTags.DatePublished, lb => lb.DatePublished },

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,8 @@ namespace LibationWinForms.Dialogs
booksLocationDescLbl.Text = desc(nameof(config.Books));
betaOptInCbox.Text = desc(nameof(config.BetaOptIn));
this.saveEpisodesToSeriesFolderCbox.Text = desc(nameof(config.SavePodcastsToParentFolder));
saveEpisodesToSeriesFolderCbox.Text = desc(nameof(config.SavePodcastsToParentFolder));
overwriteExistingCbox.Text = desc(nameof(config.OverwriteExisting));
booksSelectControl.SetSearchTitle("books location");
booksSelectControl.SetDirectoryItems(
@ -38,6 +39,8 @@ namespace LibationWinForms.Dialogs
booksSelectControl.SelectDirectory(config.Books.PathWithoutPrefix);
saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder;
overwriteExistingCbox.Checked = config.OverwriteExisting;
betaOptInCbox.Checked = config.BetaOptIn;
if (!betaOptInCbox.Checked)
@ -76,6 +79,7 @@ namespace LibationWinForms.Dialogs
}
config.SavePodcastsToParentFolder = saveEpisodesToSeriesFolderCbox.Checked;
config.OverwriteExisting = overwriteExistingCbox.Checked;
config.BetaOptIn = betaOptInCbox.Checked;
}

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:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@ -26,7 +26,8 @@ namespace TemplatesTests
public static LibraryBookDto GetLibraryBook(string seriesName = "Sherlock Holmes")
=> new()
{
Account = "my account",
Account = "myaccount@example.co",
AccountNickname = "my account",
DateAdded = new DateTime(2022, 6, 9, 0, 0, 0),
DatePublished = new DateTime(2017, 2, 27, 0, 0, 0),
FileDate = new DateTime(2023, 1, 28, 0, 0, 0),