Add Configurations property change notifications
This commit is contained in:
parent
f09baa1318
commit
1f7000c2c9
@ -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");
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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<bool> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<string> SaveClipsAndBookmarks(string fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,9 +41,9 @@ namespace AaxDecrypter
|
||||
[JsonIgnore]
|
||||
public bool IsCancelled => _cancellationSource.IsCancellationRequested;
|
||||
|
||||
private static long _globalSpeedLimit = 0;
|
||||
private long _speedLimit = 0;
|
||||
/// <summary>bytes per second</summary>
|
||||
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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -174,6 +174,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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<string> 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<long>(nameof(Configuration.DownloadSpeedLimit), DownloadSpeedChanged);
|
||||
// no null/empty check for key/iv. unencrypted files do not have them
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,16 +46,27 @@ namespace FileManager
|
||||
return stringCache[propertyName];
|
||||
}
|
||||
|
||||
public T GetNonString<T>(string propertyName)
|
||||
{
|
||||
var obj = GetObject(propertyName);
|
||||
if (obj is null) return default;
|
||||
if (obj is JValue jValue) return jValue.Value<T>();
|
||||
if (obj is JObject jObject) return jObject.ToObject<T>();
|
||||
return (T)obj;
|
||||
}
|
||||
public T GetNonString<T>(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<string>(), out var enumVal)
|
||||
? (T)enumVal
|
||||
: Enum.GetValues(typeof(T)).Cast<T>().First();
|
||||
}
|
||||
return jValue.Value<T>();
|
||||
}
|
||||
if (obj is JObject jObject) return jObject.ToObject<T>();
|
||||
return (T)obj;
|
||||
}
|
||||
|
||||
public object GetObject(string propertyName)
|
||||
{
|
||||
if (!objectCache.ContainsKey(propertyName))
|
||||
{
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<DialogResult>(this);
|
||||
|
||||
if (notificationResult == DialogResult.Ignore)
|
||||
config.SetObject(ignoreUpdate, upgradeProperties.LatestRelease.ToString());
|
||||
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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<T>(string propertyName) => persistentDictionary.GetNonString<T>(propertyName);
|
||||
public object GetObject(string propertyName) => persistentDictionary.GetObject(propertyName);
|
||||
public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue);
|
||||
public T GetNonString<T>([CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString<T>(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;
|
||||
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
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<bool>(nameof(UseCoverAsFolderIcon));
|
||||
set => persistentDictionary.SetNonString(nameof(UseCoverAsFolderIcon), value);
|
||||
}
|
||||
public bool UseCoverAsFolderIcon { get => GetNonString<bool>(); 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<bool>(nameof(BetaOptIn));
|
||||
set => persistentDictionary.SetNonString(nameof(BetaOptIn), value);
|
||||
}
|
||||
public bool BetaOptIn { get => GetNonString<bool>(); 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<bool>(nameof(AllowLibationFixup));
|
||||
set => persistentDictionary.SetNonString(nameof(AllowLibationFixup), value);
|
||||
}
|
||||
public bool AllowLibationFixup { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Create a cue sheet (.cue)")]
|
||||
public bool CreateCueSheet
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(CreateCueSheet));
|
||||
set => persistentDictionary.SetNonString(nameof(CreateCueSheet), value);
|
||||
}
|
||||
[Description("Create a cue sheet (.cue)")]
|
||||
public bool CreateCueSheet { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Retain the Aax file after successfully decrypting")]
|
||||
public bool RetainAaxFile
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(RetainAaxFile));
|
||||
set => persistentDictionary.SetNonString(nameof(RetainAaxFile), value);
|
||||
}
|
||||
[Description("Retain the Aax file after successfully decrypting")]
|
||||
public bool RetainAaxFile { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Split my books into multiple files by chapter")]
|
||||
public bool SplitFilesByChapter
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(SplitFilesByChapter));
|
||||
set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value);
|
||||
}
|
||||
[Description("Split my books into multiple files by chapter")]
|
||||
public bool SplitFilesByChapter { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Merge Opening/End Credits into the following/preceding chapters")]
|
||||
public bool MergeOpeningAndEndCredits
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(MergeOpeningAndEndCredits));
|
||||
set => persistentDictionary.SetNonString(nameof(MergeOpeningAndEndCredits), value);
|
||||
}
|
||||
[Description("Merge Opening/End Credits into the following/preceding chapters")]
|
||||
public bool MergeOpeningAndEndCredits { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Strip \"(Unabridged)\" from audiobook metadata tags")]
|
||||
public bool StripUnabridged
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(StripUnabridged));
|
||||
set => persistentDictionary.SetNonString(nameof(StripUnabridged), value);
|
||||
}
|
||||
[Description("Strip \"(Unabridged)\" from audiobook metadata tags")]
|
||||
public bool StripUnabridged { get => GetNonString<bool>(); 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<bool>(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<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Decrypt to lossy format?")]
|
||||
public bool DecryptToLossy
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DecryptToLossy));
|
||||
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
|
||||
}
|
||||
[Description("Decrypt to lossy format?")]
|
||||
public bool DecryptToLossy { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame encoder target. true = Bitrate, false = Quality")]
|
||||
public bool LameTargetBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameTargetBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameTargetBitrate), value);
|
||||
}
|
||||
[Description("Lame encoder target. true = Bitrate, false = Quality")]
|
||||
public bool LameTargetBitrate { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame encoder downsamples to mono")]
|
||||
public bool LameDownsampleMono
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameDownsampleMono));
|
||||
set => persistentDictionary.SetNonString(nameof(LameDownsampleMono), value);
|
||||
}
|
||||
[Description("Lame encoder downsamples to mono")]
|
||||
public bool LameDownsampleMono { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame target bitrate [16,320]")]
|
||||
public int LameBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(LameBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameBitrate), value);
|
||||
}
|
||||
public int LameBitrate { get => GetNonString<int>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Restrict encoder to constant bitrate?")]
|
||||
public bool LameConstantBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameConstantBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameConstantBitrate), value);
|
||||
}
|
||||
public bool LameConstantBitrate { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Match the source bitrate?")]
|
||||
public bool LameMatchSourceBR
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameMatchSourceBR));
|
||||
set => persistentDictionary.SetNonString(nameof(LameMatchSourceBR), value);
|
||||
}
|
||||
[Description("Match the source bitrate?")]
|
||||
public bool LameMatchSourceBR { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame target VBR quality [10,100]")]
|
||||
public int LameVBRQuality
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(LameVBRQuality));
|
||||
set => persistentDictionary.SetNonString(nameof(LameVBRQuality), value);
|
||||
}
|
||||
[Description("Lame target VBR quality [10,100]")]
|
||||
public int LameVBRQuality { get => GetNonString<int>(); set => SetNonString(value); }
|
||||
|
||||
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
|
||||
public Dictionary<string, bool> GridColumnsVisibilities
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string, bool>>(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<string, bool> GridColumnsVisibilities { get => GetNonString<Dictionary<string, bool>>(); set => SetNonString(value); }
|
||||
|
||||
[Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")]
|
||||
public Dictionary<string, int> GridColumnsDisplayIndices
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string, int>>(nameof(GridColumnsDisplayIndices));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsDisplayIndices), value);
|
||||
}
|
||||
public Dictionary<string, int> GridColumnsDisplayIndices { get => GetNonString<Dictionary<string, int>>(); set => SetNonString(value); }
|
||||
|
||||
[Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")]
|
||||
public Dictionary<string, int> GridColumnsWidths
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string, int>>(nameof(GridColumnsWidths));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsWidths), value);
|
||||
}
|
||||
public Dictionary<string, int> GridColumnsWidths { get => GetNonString<Dictionary<string, int>>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Save cover image alongside audiobook?")]
|
||||
public bool DownloadCoverArt
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DownloadCoverArt));
|
||||
set => persistentDictionary.SetNonString(nameof(DownloadCoverArt), value);
|
||||
public bool DownloadCoverArt { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Download clips and bookmarks?")]
|
||||
public bool DownloadClipsBookmarks { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("File format to save clips and bookmarks")]
|
||||
public ClipBookmarkFormat ClipsBookmarksFileFormat { get => GetNonString<ClipBookmarkFormat>(); 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<BadBookAction>(badBookStr, out var badBookEnum) ? badBookEnum : BadBookAction.Ask;
|
||||
}
|
||||
set => persistentDictionary.SetString(nameof(BadBook), value.ToString());
|
||||
}
|
||||
public BadBookAction BadBook { get => GetNonString<BadBookAction>(); 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<bool>(nameof(ShowImportedStats));
|
||||
set => persistentDictionary.SetNonString(nameof(ShowImportedStats), value);
|
||||
}
|
||||
public bool ShowImportedStats { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||
public bool ImportEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(ImportEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(ImportEpisodes), value);
|
||||
}
|
||||
public bool ImportEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
|
||||
public bool DownloadEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(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<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Automatically run periodic scans in the background?")]
|
||||
public bool AutoScan
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AutoScan));
|
||||
set
|
||||
{
|
||||
if (AutoScan != value)
|
||||
{
|
||||
persistentDictionary.SetNonString(nameof(AutoScan), value);
|
||||
AutoScanChanged?.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool AutoScan { get => GetNonString<bool>(); 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<bool>(nameof(AutoDownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value);
|
||||
}
|
||||
public bool AutoDownloadEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Save all podcast episodes in a series to the series parent folder?")]
|
||||
public bool SavePodcastsToParentFolder
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(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<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Global download speed limit in bytes per second.")]
|
||||
public long DownloadSpeedLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
AaxDecrypter.NetworkFileStream.GlobalSpeedLimit = persistentDictionary.GetNonString<long>(nameof(DownloadSpeedLimit));
|
||||
return AaxDecrypter.NetworkFileStream.GlobalSpeedLimit;
|
||||
var limit = GetNonString<long>();
|
||||
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<ReplacementCharacters>(nameof(ReplacementCharacters));
|
||||
set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value);
|
||||
}
|
||||
public ReplacementCharacters ReplacementCharacters { get => GetNonString<ReplacementCharacters>(); 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
|
||||
}
|
||||
|
||||
155
Source/LibationFileManager/Configuration.PropertyChange.cs
Normal file
155
Source/LibationFileManager/Configuration.PropertyChange.cs
Normal file
@ -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<string, List<MulticastDelegate>> propertyChangedActions = new();
|
||||
private readonly Dictionary<string, List<MulticastDelegate>> propertyChangingActions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Clear all subscriptions to Property<b>Changed</b> for <paramref name="propertyName"/>
|
||||
/// </summary>
|
||||
public void ClearChangedSubscriptions(string propertyName)
|
||||
{
|
||||
if (propertyChangedActions.ContainsKey(propertyName)
|
||||
&& propertyChangedActions[propertyName] is not null)
|
||||
propertyChangedActions[propertyName].Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all subscriptions to Property<b>Changing</b> for <paramref name="propertyName"/>
|
||||
/// </summary>
|
||||
public void ClearChangingSubscriptions(string propertyName)
|
||||
{
|
||||
if (propertyChangingActions.ContainsKey(propertyName)
|
||||
&& propertyChangingActions[propertyName] is not null)
|
||||
propertyChangingActions[propertyName].Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an action to be executed when a property's value has changed
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The <paramref name="propertyName"/>'s <see cref="Type"/></typeparam>
|
||||
/// <param name="propertyName">Name of the property whose change triggers the <paramref name="action"/></param>
|
||||
/// <param name="action">Action to be executed with parameters: <paramref name="propertyName"/> and <strong>NewValue</strong></param>
|
||||
/// <returns>A reference to an interface that allows observers to stop receiving notifications before the provider has finished sending them.</returns>
|
||||
public IDisposable SubscribeToPropertyChanged<T>(string propertyName, Action<string, T> action)
|
||||
{
|
||||
validateSubscriber<T>(propertyName, action);
|
||||
|
||||
if (!propertyChangedActions.ContainsKey(propertyName))
|
||||
propertyChangedActions.Add(propertyName, new List<MulticastDelegate>());
|
||||
|
||||
var actionlist = propertyChangedActions[propertyName];
|
||||
|
||||
if (!actionlist.Contains(action))
|
||||
actionlist.Add(action);
|
||||
|
||||
return new Unsubscriber(actionlist, action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an action to be executed when a property's value is changing
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The <paramref name="propertyName"/>'s <see cref="Type"/></typeparam>
|
||||
/// <param name="propertyName">Name of the property whose change triggers the <paramref name="action"/></param>
|
||||
/// <param name="action">Action to be executed with parameters: <paramref name="propertyName"/>, <b>OldValue</b>, and <b>NewValue</b></param>
|
||||
/// <returns>A reference to an interface that allows observers to stop receiving notifications before the provider has finished sending them.</returns>
|
||||
public IDisposable SubscribeToPropertyChanging<T>(string propertyName, Action<string, T, T> action)
|
||||
{
|
||||
validateSubscriber<T>(propertyName, action);
|
||||
|
||||
if (!propertyChangingActions.ContainsKey(propertyName))
|
||||
propertyChangingActions.Add(propertyName, new List<MulticastDelegate>());
|
||||
|
||||
var actionlist = propertyChangingActions[propertyName];
|
||||
|
||||
if (!actionlist.Contains(action))
|
||||
actionlist.Add(action);
|
||||
|
||||
return new Unsubscriber(actionlist, action);
|
||||
}
|
||||
|
||||
private void validateSubscriber<T>(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<MulticastDelegate> _observers;
|
||||
private MulticastDelegate _observer;
|
||||
|
||||
internal Unsubscriber(List<MulticastDelegate> observers, MulticastDelegate observer)
|
||||
{
|
||||
_observers = observers;
|
||||
_observer = observer;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_observers.Contains(_observer))
|
||||
_observers.Remove(_observer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,10 +20,6 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="OSInterop\" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<IRecord> records)
|
||||
{
|
||||
if (!records.Any()) return;
|
||||
|
||||
try
|
||||
{
|
||||
var saveFileDialog =
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -87,7 +87,7 @@ namespace LibationWinForms
|
||||
|
||||
saveState.IsMaximized = form.WindowState == FormWindowState.Maximized;
|
||||
|
||||
config.SetObject(form.Name, saveState);
|
||||
config.SetNonString(saveState, form.Name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<bool> updateReviewTask;
|
||||
private async Task<bool> 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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user