diff --git a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs index ff708f97..4504a50c 100644 --- a/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs +++ b/Source/AaxDecrypter/AaxcDownloadMultiConverter.cs @@ -43,7 +43,21 @@ namespace AaxDecrypter return false; } + //Step 3 + if (DownloadOptions.DownloadClipsBookmarks) + { + Serilog.Log.Information("Begin Downloading Clips and Bookmarks"); + if (await Task.Run(Step_DownloadClipsBookmarks)) + Serilog.Log.Information("Completed Downloading Clips and Bookmarks"); + else + { + Serilog.Log.Information("Failed to Download Clips and Bookmarks"); + return false; + } + } + + //Step 4 Serilog.Log.Information("Begin Cleanup"); if (await Task.Run(Step_Cleanup)) Serilog.Log.Information("Completed Cleanup"); diff --git a/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs b/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs index 85dc62a1..04188885 100644 --- a/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs +++ b/Source/AaxDecrypter/AaxcDownloadSingleConverter.cs @@ -49,6 +49,19 @@ namespace AaxDecrypter } //Step 4 + if (DownloadOptions.DownloadClipsBookmarks) + { + Serilog.Log.Information("Begin Downloading Clips and Bookmarks"); + if (await Task.Run(Step_DownloadClipsBookmarks)) + Serilog.Log.Information("Completed Downloading Clips and Bookmarks"); + else + { + Serilog.Log.Information("Failed to Download Clips and Bookmarks"); + return false; + } + } + + //Step 5 Serilog.Log.Information("Begin Step 4: Cleanup"); if (await Task.Run(Step_Cleanup)) Serilog.Log.Information("Completed Step 4: Cleanup"); diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 89a94f2f..69a69407 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -48,11 +48,18 @@ namespace AaxDecrypter TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc"); DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions)); + DownloadOptions.PropertyChanged += DownloadOptions_PropertyChanged; // delete file after validation is complete FileUtility.SaferDelete(OutputFileName); } + private void DownloadOptions_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(DownloadOptions.DownloadSpeedBps)) + InputFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps; + } + public abstract Task CancelAsync(); public virtual void SetCoverArt(byte[] coverArt) @@ -132,14 +139,27 @@ namespace AaxDecrypter return success; } + protected async Task Step_DownloadClipsBookmarks() + { + if (!IsCanceled && DownloadOptions.DownloadClipsBookmarks) + { + var recordsFile = await DownloadOptions.SaveClipsAndBookmarks(OutputFileName); + + if (File.Exists(recordsFile)) + OnFileCreated(recordsFile); + } + return !IsCanceled; + } + private NetworkFileStreamPersister OpenNetworkFileStream() { - if (!File.Exists(jsonDownloadState)) - return NewNetworkFilePersister(); - + NetworkFileStreamPersister nfsp = default; try { - var nfsp = new NetworkFileStreamPersister(jsonDownloadState); + if (!File.Exists(jsonDownloadState)) + return nfsp = NewNetworkFilePersister(); + + nfsp = new NetworkFileStreamPersister(jsonDownloadState); // If More than ~1 hour has elapsed since getting the download url, it will expire. // The new url will be to the same file. nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl)); @@ -149,7 +169,12 @@ namespace AaxDecrypter { FileUtility.SaferDelete(jsonDownloadState); FileUtility.SaferDelete(TempFilePath); - return NewNetworkFilePersister(); + return nfsp = NewNetworkFilePersister(); + } + finally + { + if (nfsp is not null) + nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps; } } diff --git a/Source/AaxDecrypter/IDownloadOptions.cs b/Source/AaxDecrypter/IDownloadOptions.cs index 2e098af6..ff24c1b7 100644 --- a/Source/AaxDecrypter/IDownloadOptions.cs +++ b/Source/AaxDecrypter/IDownloadOptions.cs @@ -1,8 +1,10 @@ using AAXClean; +using System.ComponentModel; +using System.Threading.Tasks; namespace AaxDecrypter { - public interface IDownloadOptions + public interface IDownloadOptions : INotifyPropertyChanged { FileManager.ReplacementCharacters ReplacementCharacters { get; } string DownloadUrl { get; } @@ -14,6 +16,8 @@ namespace AaxDecrypter bool RetainEncryptedFile { get; } bool StripUnabridged { get; } bool CreateCueSheet { get; } + bool DownloadClipsBookmarks { get; } + long DownloadSpeedBps { get; } ChapterInfo ChapterInfo { get; } bool FixupFile { get; } NAudio.Lame.LameConfig LameConfig { get; } @@ -21,5 +25,6 @@ namespace AaxDecrypter bool MatchSourceBitrate { get; } string GetMultipartFileName(MultiConvertFileProperties props); string GetMultipartTitleName(MultiConvertFileProperties props); - } + Task SaveClipsAndBookmarks(string fileName); + } } diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index 093bd1ab..23b7b19a 100644 --- a/Source/AaxDecrypter/NetworkFileStream.cs +++ b/Source/AaxDecrypter/NetworkFileStream.cs @@ -41,9 +41,9 @@ namespace AaxDecrypter [JsonIgnore] public bool IsCancelled => _cancellationSource.IsCancellationRequested; - private static long _globalSpeedLimit = 0; + private long _speedLimit = 0; /// bytes per second - public static long GlobalSpeedLimit { get => _globalSpeedLimit; set => _globalSpeedLimit = value <= 0 ? 0 : Math.Max(value, MIN_BYTES_PER_SECOND); } + public long SpeedLimit { get => _speedLimit; set => _speedLimit = value <= 0 ? 0 : Math.Max(value, MIN_BYTES_PER_SECOND); } #endregion @@ -70,7 +70,7 @@ namespace AaxDecrypter //Minimum throttle rate. The minimum amount of data that can be throttled //on each iteration of the download loop is DOWNLOAD_BUFF_SZ. - private const int MIN_BYTES_PER_SECOND = DOWNLOAD_BUFF_SZ * THROTTLE_FREQUENCY; + public const int MIN_BYTES_PER_SECOND = DOWNLOAD_BUFF_SZ * THROTTLE_FREQUENCY; #endregion @@ -202,7 +202,7 @@ namespace AaxDecrypter bytesReadSinceThrottle += bytesRead; - if (GlobalSpeedLimit >= MIN_BYTES_PER_SECOND && bytesReadSinceThrottle > GlobalSpeedLimit / THROTTLE_FREQUENCY) + if (SpeedLimit >= MIN_BYTES_PER_SECOND && bytesReadSinceThrottle > SpeedLimit / THROTTLE_FREQUENCY) { var delayMS = (int)(startTime.AddSeconds(1d / THROTTLE_FREQUENCY) - DateTime.Now).TotalMilliseconds; if (delayMS > 0) diff --git a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs index b9ac5c3b..e9c062f0 100644 --- a/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs +++ b/Source/AaxDecrypter/UnencryptedAudiobookDownloader.cs @@ -16,7 +16,7 @@ namespace AaxDecrypter { try { - Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat); + Serilog.Log.Information("Begin downloading unencrypted audiobook."); //Step 1 Serilog.Log.Information("Begin Step 1: Get Mp3 Metadata"); @@ -39,6 +39,19 @@ namespace AaxDecrypter } //Step 3 + if (DownloadOptions.DownloadClipsBookmarks) + { + Serilog.Log.Information("Begin Downloading Clips and Bookmarks"); + if (await Task.Run(Step_DownloadClipsBookmarks)) + Serilog.Log.Information("Completed Downloading Clips and Bookmarks"); + else + { + Serilog.Log.Information("Failed to Download Clips and Bookmarks"); + return false; + } + } + + //Step 4 Serilog.Log.Information("Begin Step 3: Cleanup"); if (await Task.Run(Step_Cleanup)) Serilog.Log.Information("Completed Step 3: Cleanup"); @@ -58,7 +71,6 @@ namespace AaxDecrypter } } - public override Task CancelAsync() { IsCanceled = true; diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index a687dd2c..31eaa833 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -173,6 +173,12 @@ namespace AppScaffolding if (!config.Exists(nameof(config.DownloadCoverArt))) config.DownloadCoverArt = true; + + if (!config.Exists(nameof(config.DownloadClipsBookmarks))) + config.DownloadClipsBookmarks = false; + + if (!config.Exists(nameof(config.ClipsBookmarksFileFormat))) + config.ClipsBookmarksFileFormat = Configuration.ClipBookmarkFormat.CSV; if (!config.Exists(nameof(config.AutoDownloadEpisodes))) config.AutoDownloadEpisodes = false; @@ -229,7 +235,7 @@ namespace AppScaffolding { "Using", new JArray{ "Dinah.Core", "Serilog.Exceptions" } }, // dll's name, NOT namespace { "Enrich", new JArray{ "WithCaller", "WithExceptionDetails" } }, }; - config.SetObject("Serilog", serilogObj); + config.SetNonString(serilogObj, "Serilog"); } // to restore original: Console.SetOut(origOut); diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs index 3f7abea3..ea430c5d 100644 --- a/Source/FileLiberator/DownloadDecryptBook.cs +++ b/Source/FileLiberator/DownloadDecryptBook.cs @@ -104,7 +104,7 @@ namespace FileLiberator var api = await libraryBook.GetApiAsync(); var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId); - var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic); + using var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic); var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower()); var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory; @@ -133,9 +133,7 @@ namespace FileLiberator abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path); // REAL WORK DONE HERE - var success = await abDownloader.RunAsync(); - - return success; + return await abDownloader.RunAsync(); } private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic) @@ -168,6 +166,8 @@ namespace FileLiberator Downsample = config.AllowLibationFixup && config.LameDownsampleMono, MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate, CreateCueSheet = config.CreateCueSheet, + DownloadClipsBookmarks = config.DownloadClipsBookmarks, + DownloadSpeedBps = config.DownloadSpeedLimit, LameConfig = GetLameOptions(config), ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)), FixupFile = config.AllowLibationFixup diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs index 325ac18f..0575764c 100644 --- a/Source/FileLiberator/DownloadOptions.cs +++ b/Source/FileLiberator/DownloadOptions.cs @@ -4,11 +4,20 @@ using Dinah.Core; using DataLayer; using LibationFileManager; using FileManager; +using System.Threading.Tasks; +using System.ComponentModel; +using System; +using System.IO; +using ApplicationServices; namespace FileLiberator { - public class DownloadOptions : IDownloadOptions + public class DownloadOptions : IDownloadOptions, IDisposable { + + public event PropertyChangedEventHandler PropertyChanged; + private readonly IDisposable cancellation; + public LibraryBook LibraryBook { get; } public LibraryBookDto LibraryBookDto { get; } public string DownloadUrl { get; } public string UserAgent { get; } @@ -19,7 +28,9 @@ namespace FileLiberator public bool RetainEncryptedFile { get; init; } public bool StripUnabridged { get; init; } public bool CreateCueSheet { get; init; } - public ChapterInfo ChapterInfo { get; init; } + public bool DownloadClipsBookmarks { get; init; } + public long DownloadSpeedBps { get; set; } + public ChapterInfo ChapterInfo { get; init; } public bool FixupFile { get; init; } public NAudio.Lame.LameConfig LameConfig { get; init; } public bool Downsample { get; init; } @@ -32,15 +43,56 @@ namespace FileLiberator public string GetMultipartTitleName(MultiConvertFileProperties props) => Templates.ChapterTitle.GetTitle(LibraryBookDto, props); - public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent) + public async Task SaveClipsAndBookmarks(string fileName) { - LibraryBookDto = ArgumentValidator - .EnsureNotNull(libraryBook, nameof(libraryBook)) - .ToDto(); + if (DownloadClipsBookmarks) + { + var format = Configuration.Instance.ClipsBookmarksFileFormat; + + var formatExtension = format.ToString().ToLowerInvariant(); + var filePath = Path.ChangeExtension(fileName, formatExtension); + + var api = await LibraryBook.GetApiAsync(); + var records = await api.GetRecordsAsync(LibraryBook.Book.AudibleProductId); + + switch(format) + { + case Configuration.ClipBookmarkFormat.CSV: + RecordExporter.ToCsv(filePath, records); + break; + case Configuration.ClipBookmarkFormat.Xlsx: + RecordExporter.ToXlsx(filePath, records); + break; + case Configuration.ClipBookmarkFormat.Json: + RecordExporter.ToJson(filePath, LibraryBook, records); + break; + } + return filePath; + } + return string.Empty; + } + + private void DownloadSpeedChanged(string propertyName, long speed) + { + DownloadSpeedBps = speed; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadSpeedBps))); + } + + public void Dispose() + { + cancellation?.Dispose(); + } + + public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent) + { + LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook)); DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl)); UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent)); - // no null/empty check for key/iv. unencrypted files do not have them - } + LibraryBookDto = LibraryBook.ToDto(); + + cancellation = Configuration.Instance.SubscribeToPropertyChanged(nameof(Configuration.DownloadSpeedLimit), DownloadSpeedChanged); + // no null/empty check for key/iv. unencrypted files do not have them + } } } diff --git a/Source/FileManager/PersistentDictionary.cs b/Source/FileManager/PersistentDictionary.cs index 47bb7687..db66983a 100644 --- a/Source/FileManager/PersistentDictionary.cs +++ b/Source/FileManager/PersistentDictionary.cs @@ -46,16 +46,27 @@ namespace FileManager return stringCache[propertyName]; } - public T GetNonString(string propertyName) - { - var obj = GetObject(propertyName); - if (obj is null) return default; - if (obj is JValue jValue) return jValue.Value(); - if (obj is JObject jObject) return jObject.ToObject(); - return (T)obj; - } + public T GetNonString(string propertyName) + { + var obj = GetObject(propertyName); + if (obj is null) return default; - public object GetObject(string propertyName) + if (obj is JValue jValue) + { + if (jValue.Type == JTokenType.String && typeof(T).IsAssignableTo(typeof(Enum))) + { + return + Enum.TryParse(typeof(T), jValue.Value(), out var enumVal) + ? (T)enumVal + : Enum.GetValues(typeof(T)).Cast().First(); + } + return jValue.Value(); + } + if (obj is JObject jObject) return jObject.ToObject(); + return (T)obj; + } + + public object GetObject(string propertyName) { if (!objectCache.ContainsKey(propertyName)) { diff --git a/Source/LibationAvalonia/FormSaveExtension.cs b/Source/LibationAvalonia/FormSaveExtension.cs index 4f4bdd1b..3dbd64f1 100644 --- a/Source/LibationAvalonia/FormSaveExtension.cs +++ b/Source/LibationAvalonia/FormSaveExtension.cs @@ -93,7 +93,7 @@ namespace LibationAvalonia saveState.Width = (int)form.Bounds.Size.Width; saveState.Height = (int)form.Bounds.Size.Height; - config.SetObject(form.GetType().Name, saveState); + config.SetNonString(saveState, form.GetType().Name); } catch (Exception ex) { diff --git a/Source/LibationAvalonia/Views/MainWindow.ProcessQueue.cs b/Source/LibationAvalonia/Views/MainWindow.ProcessQueue.cs index 7b722a96..b14acea3 100644 --- a/Source/LibationAvalonia/Views/MainWindow.ProcessQueue.cs +++ b/Source/LibationAvalonia/Views/MainWindow.ProcessQueue.cs @@ -56,7 +56,7 @@ namespace LibationAvalonia.Views public void ToggleQueueHideBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) { SetQueueCollapseState(_viewModel.QueueOpen); - Configuration.Instance.SetObject(nameof(_viewModel.QueueOpen), _viewModel.QueueOpen); + Configuration.Instance.SetNonString(_viewModel.QueueOpen, nameof(_viewModel.QueueOpen)); } } } diff --git a/Source/LibationAvalonia/Views/MainWindow.ScanAuto.cs b/Source/LibationAvalonia/Views/MainWindow.ScanAuto.cs index 37cb09a2..bdb0d2d1 100644 --- a/Source/LibationAvalonia/Views/MainWindow.ScanAuto.cs +++ b/Source/LibationAvalonia/Views/MainWindow.ScanAuto.cs @@ -5,6 +5,7 @@ using Dinah.Core; using LibationFileManager; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; namespace LibationAvalonia.Views @@ -13,6 +14,7 @@ namespace LibationAvalonia.Views public partial class MainWindow { private InterruptableTimer autoScanTimer; + private IDisposable cancellation; private void Configure_ScanAuto() { @@ -53,7 +55,11 @@ namespace LibationAvalonia.Views AccountsSettingsPersister.Saved += accountsPostSave; // when autoscan setting is changed, update menu checkbox and run autoscan - Configuration.Instance.AutoScanChanged += startAutoScan; + Configuration.Instance.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(Configuration.Instance.AutoScan)) + startAutoScan(); + }; } private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts; diff --git a/Source/LibationAvalonia/Views/MainWindow.Update.cs b/Source/LibationAvalonia/Views/MainWindow.Update.cs index d7871210..0c5e9ac5 100644 --- a/Source/LibationAvalonia/Views/MainWindow.Update.cs +++ b/Source/LibationAvalonia/Views/MainWindow.Update.cs @@ -97,13 +97,13 @@ namespace LibationAvalonia.Views const string ignoreUpdate = "IgnoreUpdate"; var config = Configuration.Instance; - if (config.GetObject(ignoreUpdate)?.ToString() == upgradeProperties.LatestRelease.ToString()) + if (config.GetString(ignoreUpdate) == upgradeProperties.LatestRelease.ToString()) return; var notificationResult = await new UpgradeNotification(upgradeProperties).ShowDialog(this); if (notificationResult == DialogResult.Ignore) - config.SetObject(ignoreUpdate, upgradeProperties.LatestRelease.ToString()); + config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate); } } catch (Exception ex) diff --git a/Source/LibationFileManager/Configuration.Logging.cs b/Source/LibationFileManager/Configuration.Logging.cs index 9cde9b62..1c6ecee2 100644 --- a/Source/LibationFileManager/Configuration.Logging.cs +++ b/Source/LibationFileManager/Configuration.Logging.cs @@ -36,6 +36,7 @@ namespace LibationFileManager } set { + PropertyChanging?.Invoke(this, new PropertyChangingEventArgsEx(nameof(LogLevel), LogLevel, value)); var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString()); if (!valueWasChanged) { @@ -45,7 +46,9 @@ namespace LibationFileManager configuration.Reload(); - Log.Logger.Information("Updated LogLevel MinimumLevel. {@DebugInfo}", new + PropertyChanged?.Invoke(this, new PropertyChangedEventArgsEx(nameof(LogLevel), value)); + + Log.Logger.Information("Updated LogLevel MinimumLevel. {@DebugInfo}", new { LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(), LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(), diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs index 430e5cea..5de25ef3 100644 --- a/Source/LibationFileManager/Configuration.PersistentSettings.cs +++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs @@ -3,30 +3,51 @@ using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using FileManager; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace LibationFileManager { public partial class Configuration - { + { // note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app // default setting and directory creation occur in class responsible for files. // config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation // exceptions: appsettings.json, LibationFiles dir, Settings.json - private PersistentDictionary persistentDictionary; + private PersistentDictionary persistentDictionary { get; set; } - public T GetNonString(string propertyName) => persistentDictionary.GetNonString(propertyName); - public object GetObject(string propertyName) => persistentDictionary.GetObject(propertyName); - public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue); + public T GetNonString([CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString(propertyName); + public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName); + public string GetString([CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName); + public void SetNonString(object newValue, [CallerMemberName] string propertyName = "") + { + var existing = GetType().GetProperty(propertyName)?.GetValue(this); + if (existing?.Equals(newValue) is true) return; - /// WILL ONLY set if already present. WILL NOT create new - public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) + PropertyChanging?.Invoke(this, new PropertyChangingEventArgsEx(propertyName, existing, newValue)); + persistentDictionary.SetNonString(propertyName, newValue); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgsEx(propertyName, newValue)); + } + public void SetString(string newValue, [CallerMemberName] string propertyName = "") + { + var existing = GetType().GetProperty(propertyName)?.GetValue(this); + if (existing?.Equals(newValue) is true) return; + + PropertyChanging?.Invoke(this, new PropertyChangingEventArgsEx(propertyName, existing, newValue)); + persistentDictionary.SetString(propertyName, newValue); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgsEx(propertyName, newValue)); + } + + /// WILL ONLY set if already present. WILL NOT create new + public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) { - var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging); + var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging); if (settingWasChanged) - configuration?.Reload(); + configuration?.Reload(); } public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json"); @@ -45,161 +66,91 @@ namespace LibationFileManager public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName); [Description("Set cover art as the folder's icon. (Windows only)")] - public bool UseCoverAsFolderIcon - { - get => persistentDictionary.GetNonString(nameof(UseCoverAsFolderIcon)); - set => persistentDictionary.SetNonString(nameof(UseCoverAsFolderIcon), value); - } + public bool UseCoverAsFolderIcon { get => GetNonString(); set => SetNonString(value); } [Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")] - public bool BetaOptIn - { - get => persistentDictionary.GetNonString(nameof(BetaOptIn)); - set => persistentDictionary.SetNonString(nameof(BetaOptIn), value); - } + public bool BetaOptIn { get => GetNonString(); set => SetNonString(value); } [Description("Location for book storage. Includes destination of newly liberated books")] - public string Books - { - get => persistentDictionary.GetString(nameof(Books)); - set => persistentDictionary.SetString(nameof(Books), value); - } + public string Books { get => GetString(); set => SetString(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 => persistentDictionary.GetString(nameof(InProgress)); - set => persistentDictionary.SetString(nameof(InProgress), value); - } + public string InProgress { get => GetString(); set => SetString(value); } [Description("Allow Libation to fix up audiobook metadata")] - public bool AllowLibationFixup - { - get => persistentDictionary.GetNonString(nameof(AllowLibationFixup)); - set => persistentDictionary.SetNonString(nameof(AllowLibationFixup), value); - } + public bool AllowLibationFixup { get => GetNonString(); set => SetNonString(value); } - [Description("Create a cue sheet (.cue)")] - public bool CreateCueSheet - { - get => persistentDictionary.GetNonString(nameof(CreateCueSheet)); - set => persistentDictionary.SetNonString(nameof(CreateCueSheet), value); - } + [Description("Create a cue sheet (.cue)")] + public bool CreateCueSheet { get => GetNonString(); set => SetNonString(value); } - [Description("Retain the Aax file after successfully decrypting")] - public bool RetainAaxFile - { - get => persistentDictionary.GetNonString(nameof(RetainAaxFile)); - set => persistentDictionary.SetNonString(nameof(RetainAaxFile), value); - } + [Description("Retain the Aax file after successfully decrypting")] + public bool RetainAaxFile { get => GetNonString(); set => SetNonString(value); } - [Description("Split my books into multiple files by chapter")] - public bool SplitFilesByChapter - { - get => persistentDictionary.GetNonString(nameof(SplitFilesByChapter)); - set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value); - } + [Description("Split my books into multiple files by chapter")] + public bool SplitFilesByChapter { get => GetNonString(); set => SetNonString(value); } - [Description("Merge Opening/End Credits into the following/preceding chapters")] - public bool MergeOpeningAndEndCredits - { - get => persistentDictionary.GetNonString(nameof(MergeOpeningAndEndCredits)); - set => persistentDictionary.SetNonString(nameof(MergeOpeningAndEndCredits), value); - } + [Description("Merge Opening/End Credits into the following/preceding chapters")] + public bool MergeOpeningAndEndCredits { get => GetNonString(); set => SetNonString(value); } - [Description("Strip \"(Unabridged)\" from audiobook metadata tags")] - public bool StripUnabridged - { - get => persistentDictionary.GetNonString(nameof(StripUnabridged)); - set => persistentDictionary.SetNonString(nameof(StripUnabridged), value); - } + [Description("Strip \"(Unabridged)\" from audiobook metadata tags")] + public bool StripUnabridged { get => GetNonString(); set => SetNonString(value); } - [Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")] - public bool StripAudibleBrandAudio - { - get => persistentDictionary.GetNonString(nameof(StripAudibleBrandAudio)); - set => persistentDictionary.SetNonString(nameof(StripAudibleBrandAudio), value); - } + [Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")] + public bool StripAudibleBrandAudio { get => GetNonString(); set => SetNonString(value); } - [Description("Decrypt to lossy format?")] - public bool DecryptToLossy - { - get => persistentDictionary.GetNonString(nameof(DecryptToLossy)); - set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value); - } + [Description("Decrypt to lossy format?")] + public bool DecryptToLossy { get => GetNonString(); set => SetNonString(value); } - [Description("Lame encoder target. true = Bitrate, false = Quality")] - public bool LameTargetBitrate - { - get => persistentDictionary.GetNonString(nameof(LameTargetBitrate)); - set => persistentDictionary.SetNonString(nameof(LameTargetBitrate), value); - } + [Description("Lame encoder target. true = Bitrate, false = Quality")] + public bool LameTargetBitrate { get => GetNonString(); set => SetNonString(value); } - [Description("Lame encoder downsamples to mono")] - public bool LameDownsampleMono - { - get => persistentDictionary.GetNonString(nameof(LameDownsampleMono)); - set => persistentDictionary.SetNonString(nameof(LameDownsampleMono), value); - } + [Description("Lame encoder downsamples to mono")] + public bool LameDownsampleMono { get => GetNonString(); set => SetNonString(value); } [Description("Lame target bitrate [16,320]")] - public int LameBitrate - { - get => persistentDictionary.GetNonString(nameof(LameBitrate)); - set => persistentDictionary.SetNonString(nameof(LameBitrate), value); - } + public int LameBitrate { get => GetNonString(); set => SetNonString(value); } [Description("Restrict encoder to constant bitrate?")] - public bool LameConstantBitrate - { - get => persistentDictionary.GetNonString(nameof(LameConstantBitrate)); - set => persistentDictionary.SetNonString(nameof(LameConstantBitrate), value); - } + public bool LameConstantBitrate { get => GetNonString(); set => SetNonString(value); } - [Description("Match the source bitrate?")] - public bool LameMatchSourceBR - { - get => persistentDictionary.GetNonString(nameof(LameMatchSourceBR)); - set => persistentDictionary.SetNonString(nameof(LameMatchSourceBR), value); - } + [Description("Match the source bitrate?")] + public bool LameMatchSourceBR { get => GetNonString(); set => SetNonString(value); } - [Description("Lame target VBR quality [10,100]")] - public int LameVBRQuality - { - get => persistentDictionary.GetNonString(nameof(LameVBRQuality)); - set => persistentDictionary.SetNonString(nameof(LameVBRQuality), value); - } + [Description("Lame target VBR quality [10,100]")] + public int LameVBRQuality { get => GetNonString(); set => SetNonString(value); } - [Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")] - public Dictionary GridColumnsVisibilities - { - get => persistentDictionary.GetNonString>(nameof(GridColumnsVisibilities)); - set => persistentDictionary.SetNonString(nameof(GridColumnsVisibilities), value); - } + [Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")] + public Dictionary GridColumnsVisibilities { get => GetNonString>(); set => SetNonString(value); } [Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")] - public Dictionary GridColumnsDisplayIndices - { - get => persistentDictionary.GetNonString>(nameof(GridColumnsDisplayIndices)); - set => persistentDictionary.SetNonString(nameof(GridColumnsDisplayIndices), value); - } + public Dictionary GridColumnsDisplayIndices { get => GetNonString>(); set => SetNonString(value); } [Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")] - public Dictionary GridColumnsWidths - { - get => persistentDictionary.GetNonString>(nameof(GridColumnsWidths)); - set => persistentDictionary.SetNonString(nameof(GridColumnsWidths), value); - } + public Dictionary GridColumnsWidths { get => GetNonString>(); set => SetNonString(value); } [Description("Save cover image alongside audiobook?")] - public bool DownloadCoverArt - { - get => persistentDictionary.GetNonString(nameof(DownloadCoverArt)); - set => persistentDictionary.SetNonString(nameof(DownloadCoverArt), value); + public bool DownloadCoverArt { get => GetNonString(); set => SetNonString(value); } + + [Description("Download clips and bookmarks?")] + public bool DownloadClipsBookmarks { get => GetNonString(); set => SetNonString(value); } + + [Description("File format to save clips and bookmarks")] + public ClipBookmarkFormat ClipsBookmarksFileFormat { get => GetNonString(); set => SetNonString(value); } + + [JsonConverter(typeof(StringEnumConverter))] + public enum ClipBookmarkFormat + { + [Description("Comma-separated values")] + CSV, + [Description("Microsoft Excel Spreadsheet")] + Xlsx, + [Description("JavaScript Object Notation (JSON)")] + Json } - public enum BadBookAction + [JsonConverter(typeof(StringEnumConverter))] + public enum BadBookAction { [Description("Ask each time what action to take.")] Ask = 0, @@ -212,91 +163,46 @@ namespace LibationFileManager } [Description("When liberating books and there is an error, Libation should:")] - public BadBookAction BadBook - { - get - { - var badBookStr = persistentDictionary.GetString(nameof(BadBook)); - return Enum.TryParse(badBookStr, out var badBookEnum) ? badBookEnum : BadBookAction.Ask; - } - set => persistentDictionary.SetString(nameof(BadBook), value.ToString()); - } + public BadBookAction BadBook { get => GetNonString(); set => SetNonString(value); } [Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")] - public bool ShowImportedStats - { - get => persistentDictionary.GetNonString(nameof(ShowImportedStats)); - set => persistentDictionary.SetNonString(nameof(ShowImportedStats), value); - } + public bool ShowImportedStats { get => GetNonString(); set => SetNonString(value); } [Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")] - public bool ImportEpisodes - { - get => persistentDictionary.GetNonString(nameof(ImportEpisodes)); - set => persistentDictionary.SetNonString(nameof(ImportEpisodes), value); - } + public bool ImportEpisodes { get => GetNonString(); set => SetNonString(value); } - [Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")] - public bool DownloadEpisodes - { - get => persistentDictionary.GetNonString(nameof(DownloadEpisodes)); - set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value); - } - - public event EventHandler AutoScanChanged; + [Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")] + public bool DownloadEpisodes { get => GetNonString(); set => SetNonString(value); } [Description("Automatically run periodic scans in the background?")] - public bool AutoScan - { - get => persistentDictionary.GetNonString(nameof(AutoScan)); - set - { - if (AutoScan != value) - { - persistentDictionary.SetNonString(nameof(AutoScan), value); - AutoScanChanged?.Invoke(null, null); - } - } - } + public bool AutoScan { get => GetNonString(); set => SetNonString(value); } [Description("Auto download books? After scan, download new books in 'checked' accounts.")] // poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific - public bool AutoDownloadEpisodes - { - get => persistentDictionary.GetNonString(nameof(AutoDownloadEpisodes)); - set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value); - } + public bool AutoDownloadEpisodes { get => GetNonString(); set => SetNonString(value); } - [Description("Save all podcast episodes in a series to the series parent folder?")] - public bool SavePodcastsToParentFolder - { - get => persistentDictionary.GetNonString(nameof(SavePodcastsToParentFolder)); - set => persistentDictionary.SetNonString(nameof(SavePodcastsToParentFolder), value); - } + [Description("Save all podcast episodes in a series to the series parent folder?")] + public bool SavePodcastsToParentFolder { get => GetNonString(); set => SetNonString(value); } [Description("Global download speed limit in bytes per second.")] public long DownloadSpeedLimit { get { - AaxDecrypter.NetworkFileStream.GlobalSpeedLimit = persistentDictionary.GetNonString(nameof(DownloadSpeedLimit)); - return AaxDecrypter.NetworkFileStream.GlobalSpeedLimit; + var limit = GetNonString(); + return limit <= 0 ? 0 : Math.Max(limit, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND); } set { - AaxDecrypter.NetworkFileStream.GlobalSpeedLimit = value; - persistentDictionary.SetNonString(nameof(DownloadSpeedLimit), AaxDecrypter.NetworkFileStream.GlobalSpeedLimit); + var limit = value <= 0 ? 0 : Math.Max(value, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND); + SetNonString(limit); } } #region templates: custom file naming [Description("Edit how filename characters are replaced")] - public ReplacementCharacters ReplacementCharacters - { - get => persistentDictionary.GetNonString(nameof(ReplacementCharacters)); - set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value); - } + public ReplacementCharacters ReplacementCharacters { get => GetNonString(); set => SetNonString(value); } [Description("How to format the folders in which files will be saved")] public string FolderTemplate @@ -326,12 +232,12 @@ namespace LibationFileManager set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value); } - private string getTemplate(string settingName, Templates templ) => templ.GetValid(persistentDictionary.GetString(settingName)); + private string getTemplate(string settingName, Templates templ) => templ.GetValid(GetString(settingName)); private void setTemplate(string settingName, Templates templ, string newValue) { var template = newValue?.Trim(); if (templ.IsValid(template)) - persistentDictionary.SetString(settingName, template); + SetString(template, settingName); } #endregion } diff --git a/Source/LibationFileManager/Configuration.PropertyChange.cs b/Source/LibationFileManager/Configuration.PropertyChange.cs new file mode 100644 index 00000000..b3d032c9 --- /dev/null +++ b/Source/LibationFileManager/Configuration.PropertyChange.cs @@ -0,0 +1,155 @@ +using Dinah.Core; +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace LibationFileManager +{ + public partial class Configuration : INotifyPropertyChanging, INotifyPropertyChanged + { + public event PropertyChangingEventHandler PropertyChanging; + public event PropertyChangedEventHandler PropertyChanged; + private readonly Dictionary> propertyChangedActions = new(); + private readonly Dictionary> propertyChangingActions = new(); + + /// + /// Clear all subscriptions to PropertyChanged for + /// + public void ClearChangedSubscriptions(string propertyName) + { + if (propertyChangedActions.ContainsKey(propertyName) + && propertyChangedActions[propertyName] is not null) + propertyChangedActions[propertyName].Clear(); + } + + /// + /// Clear all subscriptions to PropertyChanging for + /// + public void ClearChangingSubscriptions(string propertyName) + { + if (propertyChangingActions.ContainsKey(propertyName) + && propertyChangingActions[propertyName] is not null) + propertyChangingActions[propertyName].Clear(); + } + + /// + /// Add an action to be executed when a property's value has changed + /// + /// The 's + /// Name of the property whose change triggers the + /// Action to be executed with parameters: and NewValue + /// A reference to an interface that allows observers to stop receiving notifications before the provider has finished sending them. + public IDisposable SubscribeToPropertyChanged(string propertyName, Action action) + { + validateSubscriber(propertyName, action); + + if (!propertyChangedActions.ContainsKey(propertyName)) + propertyChangedActions.Add(propertyName, new List()); + + var actionlist = propertyChangedActions[propertyName]; + + if (!actionlist.Contains(action)) + actionlist.Add(action); + + return new Unsubscriber(actionlist, action); + } + + /// + /// Add an action to be executed when a property's value is changing + /// + /// The 's + /// Name of the property whose change triggers the + /// Action to be executed with parameters: , OldValue, and NewValue + /// A reference to an interface that allows observers to stop receiving notifications before the provider has finished sending them. + public IDisposable SubscribeToPropertyChanging(string propertyName, Action action) + { + validateSubscriber(propertyName, action); + + if (!propertyChangingActions.ContainsKey(propertyName)) + propertyChangingActions.Add(propertyName, new List()); + + var actionlist = propertyChangingActions[propertyName]; + + if (!actionlist.Contains(action)) + actionlist.Add(action); + + return new Unsubscriber(actionlist, action); + } + + private void validateSubscriber(string propertyName, MulticastDelegate action) + { + ArgumentValidator.EnsureNotNullOrWhiteSpace(propertyName, nameof(propertyName)); + ArgumentValidator.EnsureNotNull(action, nameof(action)); + + var propertyInfo = GetType().GetProperty(propertyName); + + if (propertyInfo is null) + throw new MissingMemberException($"{nameof(Configuration)}.{propertyName} does not exist."); + + if (propertyInfo.PropertyType != typeof(T)) + throw new InvalidCastException($"{nameof(Configuration)}.{propertyName} is {propertyInfo.PropertyType}, but parameter is {typeof(T)}."); + } + + private void Configuration_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e is PropertyChangedEventArgsEx args && propertyChangedActions.ContainsKey(args.PropertyName)) + { + foreach (var action in propertyChangedActions[args.PropertyName]) + { + action.DynamicInvoke(args.PropertyName, args.NewValue); + } + } + } + + private void Configuration_PropertyChanging(object sender, PropertyChangingEventArgs e) + { + if (e is PropertyChangingEventArgsEx args && propertyChangingActions.ContainsKey(args.PropertyName)) + { + foreach (var action in propertyChangingActions[args.PropertyName]) + { + action.DynamicInvoke(args.PropertyName, args.OldValue, args.NewValue); + } + } + } + + private class PropertyChangingEventArgsEx : PropertyChangingEventArgs + { + public object OldValue { get; } + public object NewValue { get; } + + public PropertyChangingEventArgsEx(string propertyName, object oldValue, object newValue) : base(propertyName) + { + OldValue = oldValue; + NewValue = newValue; + } + } + + private class PropertyChangedEventArgsEx : PropertyChangedEventArgs + { + public object NewValue { get; } + + public PropertyChangedEventArgsEx(string propertyName, object newValue) : base(propertyName) + { + NewValue = newValue; + } + } + + private class Unsubscriber : IDisposable + { + private List _observers; + private MulticastDelegate _observer; + + internal Unsubscriber(List observers, MulticastDelegate observer) + { + _observers = observers; + _observer = observer; + } + + public void Dispose() + { + if (_observers.Contains(_observer)) + _observers.Remove(_observer); + } + } + } +} diff --git a/Source/LibationFileManager/LibationFileManager.csproj b/Source/LibationFileManager/LibationFileManager.csproj index c7cb0609..90f7fe43 100644 --- a/Source/LibationFileManager/LibationFileManager.csproj +++ b/Source/LibationFileManager/LibationFileManager.csproj @@ -20,10 +20,6 @@ - - - - embedded diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index 115750e4..df1fa290 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -275,6 +275,8 @@ namespace LibationFileManager public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath, ReplacementCharacters replacements = null) { + if (string.IsNullOrWhiteSpace(template)) return string.Empty; + replacements ??= Configuration.Instance.ReplacementCharacters; var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName)); @@ -319,7 +321,8 @@ namespace LibationFileManager public string GetPortionTitle(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props) { - ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template)); + if (string.IsNullOrEmpty(template)) return string.Empty; + ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); var fileNamingTemplate = new MetadataNamingTemplate(template); diff --git a/Source/LibationWinForms/Dialogs/BookRecordsDialog.cs b/Source/LibationWinForms/Dialogs/BookRecordsDialog.cs index 0c815a87..bc54876e 100644 --- a/Source/LibationWinForms/Dialogs/BookRecordsDialog.cs +++ b/Source/LibationWinForms/Dialogs/BookRecordsDialog.cs @@ -80,9 +80,16 @@ namespace LibationWinForms.Dialogs if (control is Control c) { if (c.InvokeRequired) - c.Invoke(new MethodInvoker(() => c.Enabled = enabled)); + c.Invoke(new MethodInvoker(() => + { + c.Enabled = enabled; + c.Focus(); + })); else + { c.Enabled = enabled; + c.Focus(); + } } } @@ -166,6 +173,8 @@ namespace LibationWinForms.Dialogs private async Task saveRecords(IEnumerable records) { + if (!records.Any()) return; + try { var saveFileDialog = diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs index a9e3c3a7..f7aa4cf4 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs @@ -17,9 +17,19 @@ namespace LibationWinForms.Dialogs this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio)); this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged)); + clipsBookmarksFormatCb.Items.AddRange( + new object[] + { + Configuration.ClipBookmarkFormat.CSV, + Configuration.ClipBookmarkFormat.Xlsx, + Configuration.ClipBookmarkFormat.Json + }); + allowLibationFixupCbox.Checked = config.AllowLibationFixup; createCueSheetCbox.Checked = config.CreateCueSheet; downloadCoverArtCbox.Checked = config.DownloadCoverArt; + downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks; + clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat; retainAaxFileCbox.Checked = config.RetainAaxFile; splitFilesByChapterCbox.Checked = config.SplitFilesByChapter; mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits; @@ -44,6 +54,7 @@ namespace LibationWinForms.Dialogs convertFormatRb_CheckedChanged(this, EventArgs.Empty); allowLibationFixupCbox_CheckedChanged(this, EventArgs.Empty); splitFilesByChapterCbox_CheckedChanged(this, EventArgs.Empty); + downloadClipsBookmarksCbox_CheckedChanged(this, EventArgs.Empty); } private void Save_AudioSettings(Configuration config) @@ -51,6 +62,8 @@ namespace LibationWinForms.Dialogs config.AllowLibationFixup = allowLibationFixupCbox.Checked; config.CreateCueSheet = createCueSheetCbox.Checked; config.DownloadCoverArt = downloadCoverArtCbox.Checked; + config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked; + config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem; config.RetainAaxFile = retainAaxFileCbox.Checked; config.SplitFilesByChapter = splitFilesByChapterCbox.Checked; config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked; @@ -68,6 +81,12 @@ namespace LibationWinForms.Dialogs config.ChapterTitleTemplate = chapterTitleTemplateTb.Text; } + + private void downloadClipsBookmarksCbox_CheckedChanged(object sender, EventArgs e) + { + clipsBookmarksFormatCb.Enabled = downloadClipsBookmarksCbox.Checked; + } + private void lameTargetRb_CheckedChanged(object sender, EventArgs e) { lameBitrateGb.Enabled = lameTargetBitrateRb.Checked; diff --git a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs index 233155c6..3c828c52 100644 --- a/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs +++ b/Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs @@ -73,6 +73,8 @@ this.folderTemplateTb = new System.Windows.Forms.TextBox(); this.folderTemplateLbl = new System.Windows.Forms.Label(); this.tab4AudioFileOptions = new System.Windows.Forms.TabPage(); + this.clipsBookmarksFormatCb = new System.Windows.Forms.ComboBox(); + this.downloadClipsBookmarksCbox = new System.Windows.Forms.CheckBox(); this.audiobookFixupsGb = new System.Windows.Forms.GroupBox(); this.stripUnabridgedCbox = new System.Windows.Forms.CheckBox(); this.chapterTitleTemplateGb = new System.Windows.Forms.GroupBox(); @@ -281,7 +283,7 @@ this.allowLibationFixupCbox.AutoSize = true; this.allowLibationFixupCbox.Checked = true; this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked; - this.allowLibationFixupCbox.Location = new System.Drawing.Point(19, 118); + this.allowLibationFixupCbox.Location = new System.Drawing.Point(19, 143); this.allowLibationFixupCbox.Name = "allowLibationFixupCbox"; this.allowLibationFixupCbox.Size = new System.Drawing.Size(163, 19); this.allowLibationFixupCbox.TabIndex = 10; @@ -633,6 +635,8 @@ // // tab4AudioFileOptions // + this.tab4AudioFileOptions.Controls.Add(this.clipsBookmarksFormatCb); + this.tab4AudioFileOptions.Controls.Add(this.downloadClipsBookmarksCbox); this.tab4AudioFileOptions.Controls.Add(this.audiobookFixupsGb); this.tab4AudioFileOptions.Controls.Add(this.chapterTitleTemplateGb); this.tab4AudioFileOptions.Controls.Add(this.lameOptionsGb); @@ -649,6 +653,26 @@ this.tab4AudioFileOptions.Text = "Audio File Options"; this.tab4AudioFileOptions.UseVisualStyleBackColor = true; // + // clipsBookmarksFormatCb + // + this.clipsBookmarksFormatCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.clipsBookmarksFormatCb.FormattingEnabled = true; + this.clipsBookmarksFormatCb.Location = new System.Drawing.Point(269, 64); + this.clipsBookmarksFormatCb.Name = "clipsBookmarksFormatCb"; + this.clipsBookmarksFormatCb.Size = new System.Drawing.Size(67, 23); + this.clipsBookmarksFormatCb.TabIndex = 21; + // + // downloadClipsBookmarksCbox + // + this.downloadClipsBookmarksCbox.AutoSize = true; + this.downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 68); + this.downloadClipsBookmarksCbox.Name = "downloadClipsBookmarksCbox"; + this.downloadClipsBookmarksCbox.Size = new System.Drawing.Size(248, 19); + this.downloadClipsBookmarksCbox.TabIndex = 20; + this.downloadClipsBookmarksCbox.Text = "Download Clips, Notes, and Bookmarks as"; + this.downloadClipsBookmarksCbox.UseVisualStyleBackColor = true; + this.downloadClipsBookmarksCbox.CheckedChanged += new System.EventHandler(this.downloadClipsBookmarksCbox_CheckedChanged); + // // audiobookFixupsGb // this.audiobookFixupsGb.Controls.Add(this.splitFilesByChapterCbox); @@ -656,7 +680,7 @@ this.audiobookFixupsGb.Controls.Add(this.convertLosslessRb); this.audiobookFixupsGb.Controls.Add(this.convertLossyRb); this.audiobookFixupsGb.Controls.Add(this.stripAudibleBrandingCbox); - this.audiobookFixupsGb.Location = new System.Drawing.Point(6, 143); + this.audiobookFixupsGb.Location = new System.Drawing.Point(6, 169); this.audiobookFixupsGb.Name = "audiobookFixupsGb"; this.audiobookFixupsGb.Size = new System.Drawing.Size(403, 160); this.audiobookFixupsGb.TabIndex = 19; @@ -1032,7 +1056,7 @@ // mergeOpeningEndCreditsCbox // this.mergeOpeningEndCreditsCbox.AutoSize = true; - this.mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 93); + this.mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 118); this.mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox"; this.mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(198, 19); this.mergeOpeningEndCreditsCbox.TabIndex = 13; @@ -1042,7 +1066,7 @@ // retainAaxFileCbox // this.retainAaxFileCbox.AutoSize = true; - this.retainAaxFileCbox.Location = new System.Drawing.Point(19, 68); + this.retainAaxFileCbox.Location = new System.Drawing.Point(19, 93); this.retainAaxFileCbox.Name = "retainAaxFileCbox"; this.retainAaxFileCbox.Size = new System.Drawing.Size(132, 19); this.retainAaxFileCbox.TabIndex = 10; @@ -1214,5 +1238,7 @@ private System.Windows.Forms.GroupBox audiobookFixupsGb; private System.Windows.Forms.CheckBox betaOptInCbox; private System.Windows.Forms.CheckBox useCoverAsFolderIconCb; - } + private System.Windows.Forms.ComboBox clipsBookmarksFormatCb; + private System.Windows.Forms.CheckBox downloadClipsBookmarksCbox; + } } \ No newline at end of file diff --git a/Source/LibationWinForms/Form1.ProcessQueue.cs b/Source/LibationWinForms/Form1.ProcessQueue.cs index 031702ae..62bae864 100644 --- a/Source/LibationWinForms/Form1.ProcessQueue.cs +++ b/Source/LibationWinForms/Form1.ProcessQueue.cs @@ -78,7 +78,7 @@ namespace LibationWinForms private void ToggleQueueHideBtn_Click(object sender, EventArgs e) { SetQueueCollapseState(!splitContainer1.Panel2Collapsed); - Configuration.Instance.SetObject(nameof(splitContainer1.Panel2Collapsed), splitContainer1.Panel2Collapsed); + Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed)); } private void ProcessBookQueue1_PopOut(object sender, EventArgs e) diff --git a/Source/LibationWinForms/Form1.ScanAuto.cs b/Source/LibationWinForms/Form1.ScanAuto.cs index 5fb10932..11dd1407 100644 --- a/Source/LibationWinForms/Form1.ScanAuto.cs +++ b/Source/LibationWinForms/Form1.ScanAuto.cs @@ -56,12 +56,20 @@ namespace LibationWinForms AccountsSettingsPersister.Saving += accountsPreSave; AccountsSettingsPersister.Saved += accountsPostSave; - // when autoscan setting is changed, update menu checkbox and run autoscan - Configuration.Instance.AutoScanChanged += updateAutoScanLibraryToolStripMenuItem; - Configuration.Instance.AutoScanChanged += startAutoScan; + Configuration.Instance.PropertyChanged += Configuration_PropertyChanged; } - private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts; + private void Configuration_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Configuration.Instance.AutoScan)) + { + // when autoscan setting is changed, update menu checkbox and run autoscan + updateAutoScanLibraryToolStripMenuItem(sender, e); + startAutoScan(sender, e); + } + } + + private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts; private List<(string AccountId, string LocaleName)> getDefaultAccounts() { using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); diff --git a/Source/LibationWinForms/FormSaveExtension.cs b/Source/LibationWinForms/FormSaveExtension.cs index 22be5c73..d4b8feee 100644 --- a/Source/LibationWinForms/FormSaveExtension.cs +++ b/Source/LibationWinForms/FormSaveExtension.cs @@ -87,7 +87,7 @@ namespace LibationWinForms saveState.IsMaximized = form.WindowState == FormWindowState.Maximized; - config.SetObject(form.Name, saveState); + config.SetNonString(saveState, form.Name); } } diff --git a/Source/LibationWinForms/GridView/GridEntry.cs b/Source/LibationWinForms/GridView/GridEntry.cs index 0a0d3c13..43aff6fa 100644 --- a/Source/LibationWinForms/GridView/GridEntry.cs +++ b/Source/LibationWinForms/GridView/GridEntry.cs @@ -71,6 +71,15 @@ namespace LibationWinForms.GridView && updateReviewTask?.IsCompleted is not false) { updateReviewTask = UpdateRating(value); + updateReviewTask.ContinueWith(t => + { + if (t.Result) + { + _myRating = value; + LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, value); + } + NotifyPropertyChanged(); + }); } } } @@ -80,18 +89,12 @@ namespace LibationWinForms.GridView #region User rating - private Task updateReviewTask; - private async Task UpdateRating(Rating rating) + private Task updateReviewTask; + private async Task UpdateRating(Rating rating) { var api = await LibraryBook.GetApiAsync(); - if (await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating)) - { - _myRating = rating; - LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, rating); - } - - this.NotifyPropertyChanged(nameof(MyRating)); + return await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating); } #endregion