Add "Download split by chapters" context menu item (#1436)

All processables are now created with an instance of Configuration, and they use that instance's settings.

Added Configuration.CreateEphemeralCopy() to clone Configuration without persistence.
This commit is contained in:
MBucari
2025-12-01 23:23:47 -07:00
parent 4bd491f5b9
commit 4c5fdf05f5
21 changed files with 187 additions and 106 deletions

View File

@@ -16,9 +16,10 @@ namespace FileLiberator
/// Path: directory nested inside of Books directory
/// File name: n/a
/// </summary>
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook)
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook, Configuration config = null)
{
if (libraryBook.Book.IsEpisodeChild() && Configuration.Instance.SavePodcastsToParentFolder)
config ??= Configuration.Instance;
if (libraryBook.Book.IsEpisodeChild() && config.SavePodcastsToParentFolder)
{
var series = libraryBook.Book.SeriesLink.SingleOrDefault();
if (series is not null)

View File

@@ -13,7 +13,7 @@ using LibationFileManager;
namespace FileLiberator
{
public class ConvertToMp3 : AudioDecodable
public class ConvertToMp3 : AudioDecodable, IProcessable<ConvertToMp3>
{
public override string Name => "Convert to Mp3";
private Mp4Operation Mp4Operation;
@@ -72,15 +72,14 @@ namespace FileLiberator
OnNarratorsDiscovered(m4bBook.AppleTags.Narrator);
OnCoverImageDiscovered(m4bBook.AppleTags.Cover);
var config = Configuration.Instance;
var lameConfig = DownloadOptions.GetLameOptions(config);
var lameConfig = DownloadOptions.GetLameOptions(Configuration);
var chapters = m4bBook.GetChaptersFromMetadata();
//Finishing configuring lame encoder.
AaxDecrypter.MpegUtil.ConfigureLameOptions(
m4bBook,
lameConfig,
config.LameDownsampleMono,
config.LameMatchSourceBR,
Configuration.LameDownsampleMono,
Configuration.LameMatchSourceBR,
chapters);
if (m4bBook.AppleTags.Tracks is (int trackNum, int trackCount))
@@ -108,9 +107,9 @@ namespace FileLiberator
= FileUtility.SaferMoveToValidPath(
tempPath,
entry.proposedMp3Path,
Configuration.Instance.ReplacementCharacters,
Configuration.ReplacementCharacters,
extension: "mp3",
Configuration.Instance.OverwriteExisting);
Configuration.OverwriteExisting);
SetFileTime(libraryBook, realMp3Path);
SetDirectoryTime(libraryBook, Path.GetDirectoryName(realMp3Path));
@@ -169,5 +168,7 @@ namespace FileLiberator
TotalBytesToReceive = totalInputSize
});
}
public static ConvertToMp3 Create(Configuration config) => new() { Configuration = config };
private ConvertToMp3() { }
}
}

View File

@@ -17,7 +17,7 @@ using System.Threading.Tasks;
#nullable enable
namespace FileLiberator
{
public class DownloadDecryptBook : AudioDecodable
public class DownloadDecryptBook : AudioDecodable, IProcessable<DownloadDecryptBook>
{
public override string Name => "Download & Decrypt";
private CancellationTokenSource? cancellationTokenSource;
@@ -50,8 +50,8 @@ namespace FileLiberator
var api = await libraryBook.GetApiAsync();
LicenseInfo ??= await DownloadOptions.GetDownloadLicenseAsync(api, libraryBook, Configuration.Instance, cancellationToken);
using var downloadOptions = DownloadOptions.BuildDownloadOptions(libraryBook, Configuration.Instance, LicenseInfo);
LicenseInfo ??= await DownloadOptions.GetDownloadLicenseAsync(api, libraryBook, Configuration, cancellationToken);
using var downloadOptions = DownloadOptions.BuildDownloadOptions(libraryBook, Configuration, LicenseInfo);
var result = await DownloadAudiobookAsync(api, downloadOptions, cancellationToken);
if (!result.Success || getFirstAudioFile(result.ResultFiles) is not TempFile audioFile)
@@ -62,7 +62,7 @@ namespace FileLiberator
return new StatusHandler { "Decrypt failed" };
}
if (Configuration.Instance.RetainAaxFile)
if (Configuration.RetainAaxFile)
{
//Add the cached aaxc and key files to the entries list to be moved to the Books directory.
result.ResultFiles.AddRange(getAaxcFiles(result.CacheFiles));
@@ -256,7 +256,7 @@ namespace FileLiberator
private void AaxcDownloader_RetrievedCoverArt(object? sender, byte[]? e)
{
if (Configuration.Instance.AllowLibationFixup && sender is AaxcDownloadConvertBase downloader)
if (Configuration.AllowLibationFixup && sender is AaxcDownloadConvertBase downloader)
{
try
{
@@ -345,15 +345,15 @@ namespace FileLiberator
destinationDir,
entry.Extension,
entry.PartProperties,
Configuration.Instance.OverwriteExisting);
Configuration.OverwriteExisting);
var realDest
= FileUtility.SaferMoveToValidPath(
entry.FilePath,
destFileName,
Configuration.Instance.ReplacementCharacters,
Configuration.ReplacementCharacters,
entry.Extension,
Configuration.Instance.OverwriteExisting);
Configuration.OverwriteExisting);
#region File Move Progress
totalBytesMoved += new FileInfo(realDest).Length;
@@ -403,7 +403,7 @@ namespace FileLiberator
options.LibraryBook,
destinationDir,
extension: ".jpg",
returnFirstExisting: Configuration.Instance.OverwriteExisting);
returnFirstExisting: Configuration.OverwriteExisting);
if (File.Exists(coverPath))
FileUtility.SaferDelete(coverPath);
@@ -440,7 +440,7 @@ namespace FileLiberator
options.LibraryBook,
destinationDir,
extension: formatExtension,
returnFirstExisting: Configuration.Instance.OverwriteExisting);
returnFirstExisting: Configuration.OverwriteExisting);
if (File.Exists(recordsPath))
FileUtility.SaferDelete(recordsPath);
@@ -487,7 +487,7 @@ namespace FileLiberator
options.LibraryBook,
destinationDir,
extension: ".metadata.json",
returnFirstExisting: Configuration.Instance.OverwriteExisting);
returnFirstExisting: Configuration.OverwriteExisting);
if (File.Exists(metadataPath))
FileUtility.SaferDelete(metadataPath);
@@ -512,10 +512,10 @@ namespace FileLiberator
#endregion
#region Macros
private static string getDestinationDirectory(LibraryBook libraryBook)
private string getDestinationDirectory(LibraryBook libraryBook)
{
Serilog.Log.Verbose("Getting destination directory for {@Book}", libraryBook.LogFriendly());
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook, Configuration);
Serilog.Log.Verbose("Got destination directory for {@Book}. {@Directory}", libraryBook.LogFriendly(), destinationDir);
if (!Directory.Exists(destinationDir))
{
@@ -533,5 +533,8 @@ namespace FileLiberator
private static IEnumerable<TempFile> getAaxcFiles(IEnumerable<TempFile> entries)
=> entries.Where(f => File.Exists(f.FilePath) && (getFileType(f) is FileType.AAXC || f.Extension.Equals(".key", StringComparison.OrdinalIgnoreCase)));
#endregion
public static DownloadDecryptBook Create(Configuration config) => new() { Configuration = config };
private DownloadDecryptBook() { }
}
}

View File

@@ -13,7 +13,7 @@ using LibationFileManager;
namespace FileLiberator
{
public class DownloadPdf : Processable
public class DownloadPdf : Processable, IProcessable<DownloadPdf>
{
public override string Name => "Download Pdf";
public override bool Validate(LibraryBook libraryBook)
@@ -89,5 +89,8 @@ namespace FileLiberator
=> !File.Exists(actualDownloadedFilePath)
? new StatusHandler { "Downloaded PDF cannot be found" }
: new StatusHandler();
public static DownloadPdf Create(Configuration config) => new() { Configuration = config };
private DownloadPdf() { }
}
}

View File

@@ -9,24 +9,36 @@ using Dinah.Core.ErrorHandling;
using Dinah.Core.Net.Http;
using LibationFileManager;
#nullable enable
namespace FileLiberator
{
public abstract class Processable
public interface IProcessable<T> where T : IProcessable<T>
{
/// <summary>
/// Create a new instance of the Processable which uses a specific Configuration
/// </summary>
/// <param name="config">The <see cref="Configuration"/> this <typeparamref name="T"/> will use</param>
static abstract T Create(Configuration config);
}
public abstract class Processable
{
public abstract string Name { get; }
public event EventHandler<LibraryBook> Begin;
public event EventHandler<LibraryBook>? Begin;
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
public event EventHandler<string> StatusUpdate;
public event EventHandler<string>? StatusUpdate;
/// <summary>Fired when a file is successfully saved to disk</summary>
public event EventHandler<(string id, string path)> FileCreated;
public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<(string id, string path)>? FileCreated;
public event EventHandler<DownloadProgress>? StreamingProgressChanged;
public event EventHandler<TimeSpan>? StreamingTimeRemaining;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<LibraryBook>? Completed;
/// <returns>True == Valid</returns>
public abstract bool Validate(LibraryBook libraryBook);
public required Configuration Configuration{ get; init; }
protected Processable() { }
/// <returns>True == Valid</returns>
public abstract bool Validate(LibraryBook libraryBook);
/// <returns>True == success</returns>
public abstract Task<StatusHandler> ProcessAsync(LibraryBook libraryBook);
@@ -35,7 +47,7 @@ namespace FileLiberator
public IEnumerable<LibraryBook> GetValidLibraryBooks(IEnumerable<LibraryBook> library)
=> library.Where(libraryBook =>
Validate(libraryBook)
&& (!libraryBook.Book.IsEpisodeChild() || Configuration.Instance.DownloadEpisodes)
&& (!libraryBook.Book.IsEpisodeChild() || Configuration.DownloadEpisodes)
);
public async Task<StatusHandler> ProcessSingleAsync(LibraryBook libraryBook, bool validate)
@@ -86,12 +98,12 @@ namespace FileLiberator
protected void OnStreamingProgressChanged(DownloadProgress progress)
=> OnStreamingProgressChanged(null, progress);
protected void OnStreamingProgressChanged(object _, DownloadProgress progress)
protected void OnStreamingProgressChanged(object? _, DownloadProgress progress)
=> StreamingProgressChanged?.Invoke(this, progress);
protected void OnStreamingTimeRemaining(TimeSpan timeRemaining)
=> OnStreamingTimeRemaining(null, timeRemaining);
protected void OnStreamingTimeRemaining(object _, TimeSpan timeRemaining)
protected void OnStreamingTimeRemaining(object? _, TimeSpan timeRemaining)
=> StreamingTimeRemaining?.Invoke(this, timeRemaining);
protected void OnCompleted(LibraryBook libraryBook)
@@ -100,17 +112,17 @@ namespace FileLiberator
Completed?.Invoke(this, libraryBook);
}
protected static void SetFileTime(LibraryBook libraryBook, string file)
protected void SetFileTime(LibraryBook libraryBook, string file)
=> setFileSystemTime(libraryBook, new FileInfo(file));
protected static void SetDirectoryTime(LibraryBook libraryBook, string file)
protected void SetDirectoryTime(LibraryBook libraryBook, string file)
=> setFileSystemTime(libraryBook, new DirectoryInfo(file));
private static void setFileSystemTime(LibraryBook libraryBook, FileSystemInfo fileInfo)
private void setFileSystemTime(LibraryBook libraryBook, FileSystemInfo fileInfo)
{
if (!fileInfo.Exists) return;
fileInfo.CreationTimeUtc = getTimeValue(Configuration.Instance.CreationTime) ?? fileInfo.CreationTimeUtc;
fileInfo.LastWriteTimeUtc = getTimeValue(Configuration.Instance.LastWriteTime) ?? fileInfo.LastWriteTimeUtc;
fileInfo.CreationTimeUtc = getTimeValue(Configuration.CreationTime) ?? fileInfo.CreationTimeUtc;
fileInfo.LastWriteTimeUtc = getTimeValue(Configuration.LastWriteTime) ?? fileInfo.LastWriteTimeUtc;
DateTime? getTimeValue(Configuration.DateTimeSource source) => source switch
{

View File

@@ -7,6 +7,7 @@ namespace FileManager;
public interface IJsonBackedDictionary
{
JObject GetJObject();
bool Exists(string propertyName);
string? GetString(string propertyName, string? defaultValue = null);
T? GetNonString<T>(string propertyName, T? defaultValue = default);

View File

@@ -273,5 +273,7 @@ namespace FileManager
{
File.WriteAllText(Filepath, "{}");
}
}
public JObject GetJObject() => readFile();
}
}

View File

@@ -2,6 +2,7 @@ using Avalonia.Controls;
using DataLayer;
using Dinah.Core.ErrorHandling;
using LibationAvalonia.ViewModels;
using LibationFileManager;
using LibationUiBase.ProcessQueue;
using System.Collections.Generic;
using System.Linq;
@@ -32,11 +33,11 @@ public partial class ThemePreviewControl : UserControl
MainVM.Configure_NonUI();
}
QueuedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Queued };
WorkingBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Working };
CompletedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Completed };
CancelledBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Cancelled };
FailedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Failed };
QueuedBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Queued };
WorkingBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Working };
CompletedBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Completed };
CancelledBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Cancelled };
FailedBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Failed };
//Set the current processable so that the empty queue doesn't try to advance.
QueuedBook.AddDownloadPdf();

View File

@@ -32,13 +32,13 @@ namespace LibationAvalonia.ViewModels
setQueueCollapseState(collapseState);
}
public async void LiberateClicked(LibraryBook[] libraryBooks)
public async void LiberateClicked(System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config)
{
try
{
if (ProcessQueue.QueueDownloadDecrypt(libraryBooks))
if (ProcessQueue.QueueDownloadDecrypt(libraryBooks, config))
setQueueCollapseState(false);
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.AudioExists)
else if (libraryBooks.Count == 1 && libraryBooks[0].Book.AudioExists)
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(libraryBooks[0].Book.AudibleProductId);

View File

@@ -173,7 +173,7 @@ namespace LibationAvalonia.Views
await ViewModel.BindToGridTask;
}
public void ProductsDisplay_LiberateClicked(object _, LibraryBook[] libraryBook) => ViewModel.LiberateClicked(libraryBook);
public void ProductsDisplay_LiberateClicked(object _, IList<LibraryBook> libraryBook, Configuration config) => ViewModel.LiberateClicked(libraryBook, config);
public void ProductsDisplay_LiberateSeriesClicked(object _, SeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook[] libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);

View File

@@ -31,7 +31,7 @@ namespace LibationAvalonia.Views
if (Design.IsDesignMode)
{
ViewModels.MainVM.Configure_NonUI();
DataContext = new ProcessBookViewModel(MockLibraryBook.CreateBook());
DataContext = new ProcessBookViewModel(MockLibraryBook.CreateBook(), LibationFileManager.Configuration.Instance);
return;
}
}

View File

@@ -38,42 +38,42 @@ namespace LibationAvalonia.Views
var trialBook = MockLibraryBook.CreateBook();
List<ProcessBookViewModel> testList = new()
{
new ProcessBookViewModel(trialBook)
new ProcessBookViewModel(trialBook, Configuration.Instance)
{
Result = ProcessBookResult.FailedAbort,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(trialBook)
new ProcessBookViewModel(trialBook, Configuration.Instance)
{
Result = ProcessBookResult.FailedSkip,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(trialBook)
new ProcessBookViewModel(trialBook, Configuration.Instance)
{
Result = ProcessBookResult.FailedRetry,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(trialBook)
new ProcessBookViewModel(trialBook, Configuration.Instance)
{
Result = ProcessBookResult.ValidationFail,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(trialBook)
new ProcessBookViewModel(trialBook, Configuration.Instance)
{
Result = ProcessBookResult.Cancelled,
Status = ProcessBookStatus.Cancelled,
},
new ProcessBookViewModel(trialBook)
new ProcessBookViewModel(trialBook, Configuration.Instance)
{
Result = ProcessBookResult.Success,
Status = ProcessBookStatus.Completed,
},
new ProcessBookViewModel(trialBook)
new ProcessBookViewModel(trialBook, Configuration.Instance)
{
Result = ProcessBookResult.None,
Status = ProcessBookStatus.Working,
},
new ProcessBookViewModel(trialBook)
new ProcessBookViewModel(trialBook, Configuration.Instance)
{
Result = ProcessBookResult.None,
Status = ProcessBookStatus.Queued,

View File

@@ -26,7 +26,7 @@ namespace LibationAvalonia.Views
{
public partial class ProductsDisplay : UserControl
{
public event EventHandler<LibraryBook[]>? LiberateClicked;
public event LiberateClickedHandler? LiberateClicked;
public event EventHandler<SeriesEntry>? LiberateSeriesClicked;
public event EventHandler<LibraryBook[]>? ConvertToMp3Clicked;
public event EventHandler<LibraryBook>? TagsButtonClicked;
@@ -298,10 +298,29 @@ namespace LibationAvalonia.Views
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.DownloadSelectedText,
Command = ReactiveCommand.Create(() => LiberateClicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray()))
Command = ReactiveCommand.Create(() => LiberateClicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray(), Configuration.Instance))
});
}
#endregion
#region Download split by chapters
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry3_a)
{
args.ContextMenuItems.Add(new MenuItem()
{
Header = ctx.DownloadAsChapters,
IsEnabled = ctx.DownloadAsChaptersEnabled,
Command = ReactiveCommand.Create(() =>
{
var config = Configuration.Instance.CreateEphemeralCopy();
config.AllowLibationFixup = config.SplitFilesByChapter = true;
var books = ctx.LibraryBookEntries.Select(e => e.LibraryBook).Where(lb => lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error).ToList();
//No need to persist BookStatus changes. They only needs to last long for the files to start downloading
books.ForEach(b => b.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated);
LiberateClicked?.Invoke(this, [entry3_a.LibraryBook], config);
})
});
}
#endregion
#region Convert to Mp3
@@ -329,7 +348,7 @@ namespace LibationAvalonia.Views
entry4.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
if (entry4.Book.HasPdf)
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
LiberateClicked?.Invoke(this, [entry4.LibraryBook]);
LiberateClicked?.Invoke(this, [entry4.LibraryBook], Configuration.Instance);
})
});
}
@@ -512,7 +531,7 @@ namespace LibationAvalonia.Views
}
else if (button.DataContext is LibraryBookEntry lbEntry)
{
LiberateClicked?.Invoke(this, [lbEntry.LibraryBook]);
LiberateClicked?.Invoke(this, [lbEntry.LibraryBook], Configuration.Instance);
}
}

View File

@@ -18,10 +18,10 @@ namespace LibationCli
public IEnumerable<string>? Asins { get; set; }
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook>? completedAction = null)
where TProcessable : Processable, new()
where TProcessable : Processable, IProcessable<TProcessable>
{
var progressBar = new ConsoleProgressBar(Console.Out);
var strProc = new TProcessable();
var strProc = TProcessable.Create(Configuration.Instance);
LibraryBook? currentLibraryBook = null;
strProc.Begin += (o, e) =>

View File

@@ -32,7 +32,14 @@ namespace LibationFileManager
}
private static readonly Configuration s_SingletonInstance = new();
public static Configuration Instance { get; private set; } = s_SingletonInstance;
public bool IsEphemeralInstance => JsonBackedDictionary is EphemeralDictionary;
public Configuration CreateEphemeralCopy()
{
var copy = new Configuration();
copy.LoadEphemeralSettings(Settings.GetJObject());
return copy;
}
private Configuration() { }
#endregion

View File

@@ -17,6 +17,7 @@ internal class EphemeralDictionary : IJsonBackedDictionary
JsonObject = dataStore;
}
public JObject GetJObject() => (JObject)JsonObject.DeepClone();
public bool Exists(string propertyName)
=> JsonObject.ContainsKey(propertyName);
public string? GetString(string propertyName, string? defaultValue = null)

View File

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
namespace LibationUiBase.GridView;
public delegate void LiberateClickedHandler(object sender, System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config);
public class GridContextMenu
{
public string CopyCellText => $"{Accelerator}Copy Cell Contents";
@@ -20,6 +21,7 @@ public class GridContextMenu
public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntries[0].Book.TitleWithSubtitle}'";
public string LocateFileErrorMessage => "Error saving book's location";
public string ConvertToMp3Text => $"{Accelerator}Convert to Mp3";
public string DownloadAsChapters => $"Download {Accelerator}split by chapters";
public string ReDownloadText => "Re-download this audiobook";
public string DownloadSelectedText => "Download selected audiobooks";
public string EditTemplatesText => "Edit Templates";
@@ -33,6 +35,7 @@ public class GridContextMenu
public bool SetDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || ge.Liberate.IsSeries);
public bool SetNotDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || ge.Liberate.IsSeries);
public bool ConvertToMp3Enabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
public bool DownloadAsChaptersEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error);
public bool ReDownloadEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
private GridEntry[] GridEntries { get; }

View File

@@ -43,6 +43,7 @@ public enum ProcessBookStatus
public class ProcessBookViewModel : ReactiveObject
{
public LibraryBook LibraryBook { get; protected set; }
public Configuration Configuration { get; }
#region Properties exposed to the view
public ProcessBookResult Result { get => field; set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(StatusText)); } }
@@ -95,9 +96,10 @@ public class ProcessBookViewModel : ReactiveObject
/// <summary> A series of Processable actions to perform on this book </summary>
protected Queue<Func<Processable>> Processes { get; } = new();
public ProcessBookViewModel(LibraryBook libraryBook)
public ProcessBookViewModel(LibraryBook libraryBook, Configuration configuration)
{
LibraryBook = libraryBook;
Configuration = configuration;
Title = LibraryBook.Book.TitleWithSubtitle;
Author = LibraryBook.Book.AuthorNames;
@@ -203,9 +205,9 @@ public class ProcessBookViewModel : ReactiveObject
public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
public ProcessBookViewModel AddConvertToMp3() => AddProcessable<ConvertToMp3>();
private ProcessBookViewModel AddProcessable<T>() where T : Processable, new()
private ProcessBookViewModel AddProcessable<T>() where T : Processable, IProcessable<T>
{
Processes.Enqueue(() => new T());
Processes.Enqueue(() => T.Create(Configuration));
return this;
}
@@ -260,7 +262,7 @@ public class ProcessBookViewModel : ReactiveObject
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
{
var quality
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
= Configuration.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
@@ -345,7 +347,7 @@ public class ProcessBookViewModel : ReactiveObject
const DialogResult SkipResult = DialogResult.Ignore;
LogError($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
DialogResult? dialogResult = Configuration.Instance.BadBook switch
DialogResult? dialogResult = Configuration.BadBook switch
{
Configuration.BadBookAction.Abort => DialogResult.Abort,
Configuration.BadBookAction.Retry => DialogResult.Retry,

View File

@@ -27,7 +27,7 @@ public class ProcessQueueViewModel : ReactiveObject
{
Queue.QueuedCountChanged += Queue_QueuedCountChanged;
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
SpeedLimit = LibationFileManager.Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
}
public int CompletedCount { get => field; private set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
@@ -48,7 +48,7 @@ public class ProcessQueueViewModel : ReactiveObject
set
{
var newValue = Math.Min(999 * 1024 * 1024, (long)Math.Ceiling(value * 1024 * 1024));
var config = LibationFileManager.Configuration.Instance;
var config = Configuration.Instance;
config.DownloadSpeedLimit = newValue;
_speedLimit
@@ -57,6 +57,8 @@ public class ProcessQueueViewModel : ReactiveObject
: 0;
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
if (Queue.Current is ProcessBookViewModel currentBook)
currentBook.Configuration.DownloadSpeedLimit = config.DownloadSpeedLimit;
SpeedLimitIncrement = _speedLimit > 100 ? 10
: _speedLimit > 10 ? 1
@@ -89,24 +91,26 @@ public class ProcessQueueViewModel : ReactiveObject
#region Add Books to Queue
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks, Configuration? config = null)
{
if (!IsBooksDirectoryValid())
config ??= Configuration.Instance;
if (!IsBooksDirectoryValid(config))
return false;
var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray();
if (needsPdf.Length > 0)
{
Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
AddDownloadPdf(needsPdf);
AddDownloadPdf(needsPdf, config);
return true;
}
return false;
}
public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks)
public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks, Configuration? config = null)
{
if (!IsBooksDirectoryValid())
config ??= Configuration.Instance;
if (!IsBooksDirectoryValid(config))
return false;
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
@@ -116,15 +120,16 @@ public class ProcessQueueViewModel : ReactiveObject
if (preLiberated.Length == 1)
RemoveCompleted(preLiberated[0]);
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
AddConvertMp3(preLiberated);
AddConvertMp3(preLiberated, config);
return true;
}
return false;
}
public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks)
public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks, Configuration? config = null)
{
if (!IsBooksDirectoryValid())
config ??= Configuration.Instance;
if (!IsBooksDirectoryValid(config))
return false;
if (libraryBooks.Count == 1)
@@ -137,14 +142,14 @@ public class ProcessQueueViewModel : ReactiveObject
{
RemoveCompleted(item);
Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item);
AddDownloadDecrypt([item]);
AddDownloadDecrypt([item], config);
return true;
}
else if (item.NeedsPdfDownload())
{
RemoveCompleted(item);
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
AddDownloadPdf([item]);
AddDownloadPdf([item], config);
return true;
}
}
@@ -155,16 +160,16 @@ public class ProcessQueueViewModel : ReactiveObject
if (toLiberate.Length > 0)
{
Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length);
AddDownloadDecrypt(toLiberate);
AddDownloadDecrypt(toLiberate, config);
return true;
}
}
return false;
}
private bool IsBooksDirectoryValid()
private bool IsBooksDirectoryValid(Configuration config)
{
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
if (string.IsNullOrWhiteSpace(config.Books))
{
Serilog.Log.Logger.Error("Books location is not set in configuration.");
MessageBoxBase.Show(
@@ -176,9 +181,9 @@ public class ProcessQueueViewModel : ReactiveObject
}
else if (AudibleFileStorage.BooksDirectory is null)
{
Serilog.Log.Logger.Error("Failed to create books directory: {@booksDir}", Configuration.Instance.Books);
Serilog.Log.Logger.Error("Failed to create books directory: {@booksDir}", config.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.",
$"Libation was unable to create the \"Books location\" folder at:\n{config.Books}\n\nPlease change the Books location in the settings menu.",
"Failed to Create Books Directory",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
@@ -186,9 +191,9 @@ public class ProcessQueueViewModel : ReactiveObject
}
else if (AudibleFileStorage.DownloadsInProgressDirectory is null)
{
Serilog.Log.Logger.Error("Failed to create DownloadsInProgressDirectory in {@InProgress}", Configuration.Instance.InProgress);
Serilog.Log.Logger.Error("Failed to create DownloadsInProgressDirectory in {@InProgress}", config.InProgress);
MessageBoxBase.Show(
$"Libation was unable to create the \"Downloads In Progress\" folder in:\n{Configuration.Instance.InProgress}\n\nPlease change the In Progress location in the settings menu.",
$"Libation was unable to create the \"Downloads In Progress\" folder in:\n{config.InProgress}\n\nPlease change the In Progress location in the settings menu.",
"Failed to Create Downloads In Progress Directory",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
@@ -196,9 +201,9 @@ public class ProcessQueueViewModel : ReactiveObject
}
else if (AudibleFileStorage.DecryptInProgressDirectory is null)
{
Serilog.Log.Logger.Error("Failed to create DecryptInProgressDirectory in {@InProgress}", Configuration.Instance.InProgress);
Serilog.Log.Logger.Error("Failed to create DecryptInProgressDirectory in {@InProgress}", config.InProgress);
MessageBoxBase.Show(
$"Libation was unable to create the \"Decrypt In Progress\" folder in:\n{Configuration.Instance.InProgress}\n\nPlease change the In Progress location in the settings menu.",
$"Libation was unable to create the \"Decrypt In Progress\" folder in:\n{config.InProgress}\n\nPlease change the In Progress location in the settings menu.",
"Failed to Create Decrypt In Progress Directory",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
@@ -218,34 +223,34 @@ public class ProcessQueueViewModel : ReactiveObject
&& entry.Status is ProcessBookStatus.Completed
&& Queue.RemoveCompleted(entry);
private void AddDownloadPdf(IList<LibraryBook> entries)
private void AddDownloadPdf(IList<LibraryBook> entries, Configuration config)
{
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
AddToQueue(procs);
ProcessBookViewModel Create(LibraryBook entry)
=> new ProcessBookViewModel(entry).AddDownloadPdf();
=> new ProcessBookViewModel(entry, config).AddDownloadPdf();
}
private void AddDownloadDecrypt(IList<LibraryBook> entries)
private void AddDownloadDecrypt(IList<LibraryBook> entries, Configuration config)
{
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
AddToQueue(procs);
ProcessBookViewModel Create(LibraryBook entry)
=> new ProcessBookViewModel(entry).AddDownloadDecryptBook().AddDownloadPdf();
=> new ProcessBookViewModel(entry, config).AddDownloadDecryptBook().AddDownloadPdf();
}
private void AddConvertMp3(IList<LibraryBook> entries)
private void AddConvertMp3(IList<LibraryBook> entries, Configuration config)
{
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
AddToQueue(procs);
ProcessBookViewModel Create(LibraryBook entry)
=> new ProcessBookViewModel(entry).AddConvertToMp3();
=> new ProcessBookViewModel(entry, config).AddConvertToMp3();
}
private void AddToQueue(IList<ProcessBookViewModel> pbook)
@@ -282,7 +287,7 @@ public class ProcessQueueViewModel : ReactiveObject
}
Serilog.Log.Logger.Information("Begin processing queued item: '{item_LibraryBook}'", nextBook.LibraryBook);
SpeedLimit = nextBook.Configuration.DownloadSpeedLimit / 1024m / 1024;
var result = await nextBook.ProcessOneAsync();
Serilog.Log.Logger.Information("Completed processing queued item: '{item_LibraryBook}' with result: {result}", nextBook.LibraryBook, result);

View File

@@ -25,13 +25,13 @@ namespace LibationWinForms
this.Width = width;
}
private void ProductsDisplay_LiberateClicked(object sender, LibraryBook[] libraryBooks)
private void ProductsDisplay_LiberateClicked(object sender, System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config)
{
try
{
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks))
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks, config))
SetQueueCollapseState(false);
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.AudioExists)
else if (libraryBooks.Count == 1 && libraryBooks[0].Book.AudioExists)
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(libraryBooks[0].Book.AudibleProductId);

View File

@@ -22,7 +22,7 @@ namespace LibationWinForms.GridView
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged;
public event EventHandler<LibraryBook[]> LiberateClicked;
public event LiberateClickedHandler LiberateClicked;
public event EventHandler<SeriesEntry> LiberateSeriesClicked;
public event EventHandler<LibraryBook[]> ConvertToMp3Clicked;
public event EventHandler InitialLoaded;
@@ -202,10 +202,31 @@ namespace LibationWinForms.GridView
ctxMenu.Items.Add(downloadSelectedMenuItem);
downloadSelectedMenuItem.Click += (s, _) =>
{
LiberateClicked?.Invoke(s, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray());
LiberateClicked?.Invoke(s, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray(), Configuration.Instance);
};
}
#endregion
#region Download split by chapters
if (ctx.LibraryBookEntries.Length > 0)
{
var downloadChaptersMenuItem = new ToolStripMenuItem
{
Text = ctx.DownloadAsChapters,
Enabled = ctx.DownloadAsChaptersEnabled
};
downloadChaptersMenuItem.Click += (_, e) =>
{
var config = Configuration.Instance.CreateEphemeralCopy();
config.AllowLibationFixup = config.SplitFilesByChapter = true;
var books = ctx.LibraryBookEntries.Select(e => e.LibraryBook).Where(lb => lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error).ToList();
books.ForEach(b => b.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated);
LiberateClicked?.Invoke(this, books, config);
};
ctxMenu.Items.Add(downloadChaptersMenuItem);
}
#endregion
#region Convert to Mp3
@@ -218,7 +239,6 @@ namespace LibationWinForms.GridView
};
convertToMp3MenuItem.Click += (_, e) => ConvertToMp3Clicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray());
ctxMenu.Items.Add(convertToMp3MenuItem);
}
#endregion
@@ -239,7 +259,7 @@ namespace LibationWinForms.GridView
if (entry4.Book.HasPdf)
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
LiberateClicked?.Invoke(s, [entry4.LibraryBook]);
LiberateClicked?.Invoke(s, [entry4.LibraryBook], Configuration.Instance);
};
}
@@ -415,7 +435,7 @@ namespace LibationWinForms.GridView
{
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error
&& !liveGridEntry.Liberate.IsUnavailable)
LiberateClicked?.Invoke(this, [liveGridEntry.LibraryBook]);
LiberateClicked?.Invoke(this, [liveGridEntry.LibraryBook], Configuration.Instance);
}
private void productsGrid_RemovableCountChanged(object sender, EventArgs e)