Merge pull request #651 from Mbucari/master

Overhaul LibationCli and add Download Quality Option
This commit is contained in:
rmcrackan 2023-07-03 21:57:04 -04:00 committed by GitHub
commit ecfe0dc033
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 994 additions and 427 deletions

View File

@ -13,7 +13,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AAXClean.Codecs" Version="1.0.4" /> <PackageReference Include="AAXClean.Codecs" Version="1.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -51,6 +51,34 @@ namespace AaxDecrypter
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Copyright)) if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Copyright))
AaxFile.AppleTags.Copyright = AaxFile.AppleTags.Copyright.Replace("(P)", "℗").Replace("&#169;", "©"); AaxFile.AppleTags.Copyright = AaxFile.AppleTags.Copyright.Replace("(P)", "℗").Replace("&#169;", "©");
//Add audiobook shelf tags
//https://github.com/advplyr/audiobookshelf/issues/1794#issuecomment-1565050213
const string tagDomain = "com.pilabor.tone";
AaxFile.AppleTags.Title = DownloadOptions.Title;
if (DownloadOptions.Subtitle is string subtitle)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SUBTITLE", subtitle);
if (DownloadOptions.Publisher is string publisher)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PUBLISHER", publisher);
if (DownloadOptions.Language is string language)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "LANGUAGE", language);
if (DownloadOptions.AudibleProductId is string asin)
{
AaxFile.AppleTags.Asin = asin;
AaxFile.AppleTags.AppleListBox.EditOrAddTag("asin", asin);
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_ASIN", asin);
}
if (DownloadOptions.SeriesName is string series)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SERIES", series);
if (DownloadOptions.SeriesNumber is float part)
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part.ToString());
} }
//Finishing configuring lame encoder. //Finishing configuring lame encoder.

View File

@ -21,6 +21,13 @@ namespace AaxDecrypter
long DownloadSpeedBps { get; } long DownloadSpeedBps { get; }
ChapterInfo ChapterInfo { get; } ChapterInfo ChapterInfo { get; }
bool FixupFile { get; } bool FixupFile { get; }
string AudibleProductId { get; }
string Title { get; }
string Subtitle { get; }
string Publisher { get; }
string Language { get; }
string SeriesName { get; }
float? SeriesNumber { get; }
NAudio.Lame.LameConfig LameConfig { get; } NAudio.Lame.LameConfig LameConfig { get; }
bool Downsample { get; } bool Downsample { get; }
bool MatchSourceBitrate { get; } bool MatchSourceBitrate { get; }

View File

@ -1,4 +1,5 @@
using AAXClean; using AAXClean;
using AAXClean.Codecs;
using NAudio.Lame; using NAudio.Lame;
using System; using System;
@ -6,6 +7,7 @@ namespace AaxDecrypter
{ {
public static class MpegUtil public static class MpegUtil
{ {
private const string TagDomain = "com.pilabor.tone";
public static void ConfigureLameOptions(Mp4File mp4File, LameConfig lameConfig, bool downsample, bool matchSourceBitrate) public static void ConfigureLameOptions(Mp4File mp4File, LameConfig lameConfig, bool downsample, bool matchSourceBitrate)
{ {
double bitrateMultiple = 1; double bitrateMultiple = 1;
@ -36,6 +38,21 @@ namespace AaxDecrypter
else if (lameConfig.VBR == VBRMode.ABR) else if (lameConfig.VBR == VBRMode.ABR)
lameConfig.ABRRateKbps = kbps; lameConfig.ABRRateKbps = kbps;
} }
//Setup metadata tags
lameConfig.ID3 = mp4File.AppleTags.ToIDTags();
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "SUBTITLE") is string subtitle)
lameConfig.ID3.Subtitle = subtitle;
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "LANGUAGE") is string lang)
lameConfig.ID3.UserDefinedText.Add("LANGUAGE", lang);
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "SERIES") is string series)
lameConfig.ID3.UserDefinedText.Add("SERIES", series);
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "PART") is string part)
lameConfig.ID3.UserDefinedText.Add("PART", part);
} }
} }
} }

View File

@ -14,7 +14,6 @@ namespace AaxDecrypter
public class NetworkFileStream : Stream, IUpdatable public class NetworkFileStream : Stream, IUpdatable
{ {
public event EventHandler Updated; public event EventHandler Updated;
public event EventHandler DownloadCompleted;
#region Public Properties #region Public Properties
@ -41,6 +40,9 @@ namespace AaxDecrypter
[JsonIgnore] [JsonIgnore]
public bool IsCancelled => _cancellationSource.IsCancellationRequested; public bool IsCancelled => _cancellationSource.IsCancellationRequested;
[JsonIgnore]
public Task DownloadTask { get; private set; }
private long _speedLimit = 0; private long _speedLimit = 0;
/// <summary>bytes per second</summary> /// <summary>bytes per second</summary>
public long SpeedLimit { get => _speedLimit; set => _speedLimit = 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); }
@ -52,7 +54,6 @@ namespace AaxDecrypter
private FileStream _readFile { get; } private FileStream _readFile { get; }
private CancellationTokenSource _cancellationSource { get; } = new(); private CancellationTokenSource _cancellationSource { get; } = new();
private EventWaitHandle _downloadedPiece { get; set; } private EventWaitHandle _downloadedPiece { get; set; }
private Task _backgroundDownloadTask { get; set; }
#endregion #endregion
@ -128,7 +129,7 @@ namespace AaxDecrypter
if (uriToSameFile.Host != Uri.Host) if (uriToSameFile.Host != Uri.Host)
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}"); throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
if (_backgroundDownloadTask is not null) if (DownloadTask is not null)
throw new InvalidOperationException("Cannot change Uri after download has started."); throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile; Uri = uriToSameFile;
@ -141,7 +142,7 @@ namespace AaxDecrypter
{ {
if (ContentLength != 0 && WritePosition == ContentLength) if (ContentLength != 0 && WritePosition == ContentLength)
{ {
_backgroundDownloadTask = Task.CompletedTask; DownloadTask = Task.CompletedTask;
return; return;
} }
@ -167,7 +168,8 @@ namespace AaxDecrypter
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background. //Download the file in the background.
_backgroundDownloadTask = Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token);
DownloadTask = Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token);
} }
/// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary> /// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
@ -234,7 +236,6 @@ namespace AaxDecrypter
_writeFile.Close(); _writeFile.Close();
_downloadedPiece.Set(); _downloadedPiece.Set();
OnUpdate(); OnUpdate();
DownloadCompleted?.Invoke(this, null);
} }
} }
@ -256,7 +257,7 @@ namespace AaxDecrypter
{ {
get get
{ {
if (_backgroundDownloadTask is null) if (DownloadTask is null)
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}"); throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
return ContentLength; return ContentLength;
} }
@ -280,7 +281,7 @@ namespace AaxDecrypter
public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count)
{ {
if (_backgroundDownloadTask is null) if (DownloadTask is null)
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}"); throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
var toRead = Math.Min(count, Length - Position); var toRead = Math.Min(count, Length - Position);
@ -306,7 +307,7 @@ namespace AaxDecrypter
private void WaitToPosition(long requiredPosition) private void WaitToPosition(long requiredPosition)
{ {
while (WritePosition < requiredPosition while (WritePosition < requiredPosition
&& _backgroundDownloadTask?.IsCompleted is false && DownloadTask?.IsCompleted is false
&& !IsCancelled) && !IsCancelled)
{ {
_downloadedPiece.WaitOne(50); _downloadedPiece.WaitOne(50);
@ -326,7 +327,7 @@ namespace AaxDecrypter
if (disposing && !disposed) if (disposing && !disposed)
{ {
_cancellationSource.Cancel(); _cancellationSource.Cancel();
_backgroundDownloadTask?.GetAwaiter().GetResult(); DownloadTask?.GetAwaiter().GetResult();
_downloadedPiece?.Dispose(); _downloadedPiece?.Dispose();
_cancellationSource?.Dispose(); _cancellationSource?.Dispose();
_readFile.Dispose(); _readFile.Dispose();

View File

@ -26,11 +26,7 @@ namespace AaxDecrypter
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync() protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{ {
TaskCompletionSource completionSource = new(); await InputFileStream.DownloadTask;
InputFileStream.DownloadCompleted += (_, _) => completionSource.SetResult();
await completionSource.Task;
if (IsCanceled) if (IsCanceled)
return false; return false;

View File

@ -43,21 +43,6 @@ namespace AppScaffolding
public static ReleaseIdentifier ReleaseIdentifier { get; private set; } public static ReleaseIdentifier ReleaseIdentifier { get; private set; }
public static Variety Variety { get; private set; } public static Variety Variety { get; private set; }
public static void SetReleaseIdentifier(Variety varietyType)
{
Variety = Enum.IsDefined(varietyType) ? varietyType : Variety.None;
var releaseID = (ReleaseIdentifier)((int)varietyType | (int)Configuration.OS | (int)RuntimeInformation.ProcessArchitecture);
if (Enum.IsDefined(releaseID))
ReleaseIdentifier = releaseID;
else
{
ReleaseIdentifier = ReleaseIdentifier.None;
Serilog.Log.Logger.Warning("Unknown release identifier @{DebugInfo}", new { Variety = varietyType, Configuration.OS, RuntimeInformation.ProcessArchitecture });
}
}
// AppScaffolding // AppScaffolding
private static Assembly _executingAssembly; private static Assembly _executingAssembly;
private static Assembly ExecutingAssembly private static Assembly ExecutingAssembly
@ -111,6 +96,22 @@ namespace AppScaffolding
configureLogging(config); configureLogging(config);
logStartupState(config); logStartupState(config);
#region Determine Libation Variery and Release ID
Variety = File.Exists("System.Windows.Forms.dll") ? Variety.Classic : Variety.Chardonnay;
var releaseID = (ReleaseIdentifier)((int)Variety | (int)Configuration.OS | (int)RuntimeInformation.ProcessArchitecture);
if (Enum.IsDefined(releaseID))
ReleaseIdentifier = releaseID;
else
{
ReleaseIdentifier = ReleaseIdentifier.None;
Serilog.Log.Logger.Warning("Unknown release identifier @{DebugInfo}", new { Variety, Configuration.OS, RuntimeInformation.ProcessArchitecture });
}
#endregion
// all else should occur after logging // all else should occur after logging
wireUpSystemEvents(config); wireUpSystemEvents(config);

View File

@ -13,6 +13,10 @@ namespace DataLayer
LC_64_22050_stereo = (64L << 18) | (22050 << 2) | 2, LC_64_22050_stereo = (64L << 18) | (22050 << 2) | 2,
LC_64_44100_stereo = (64L << 18) | (44100 << 2) | 2, LC_64_44100_stereo = (64L << 18) | (44100 << 2) | 2,
LC_128_44100_stereo = (128L << 18) | (44100 << 2) | 2, LC_128_44100_stereo = (128L << 18) | (44100 << 2) | 2,
AAX_22_32 = LC_32_22050_stereo,
AAX_22_64 = LC_64_22050_stereo,
AAX_44_64 = LC_64_44100_stereo,
AAX_44_128 = LC_128_44100_stereo
} }
public class AudioFormat : IComparable<AudioFormat>, IComparable public class AudioFormat : IComparable<AudioFormat>, IComparable

View File

@ -121,8 +121,9 @@ namespace FileLiberator
downloadValidation(libraryBook); downloadValidation(libraryBook);
var quality = (AudibleApi.DownloadQuality)config.FileDownloadQuality;
var api = await libraryBook.GetApiAsync(); var api = await libraryBook.GetApiAsync();
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId); var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, quality);
using var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic); using var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower()); var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
@ -169,6 +170,9 @@ namespace FileLiberator
? contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs ? contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs
: 0; : 0;
//Set the requested AudioFormat for use in file naming templates
libraryBook.Book.AudioFormat = AudioFormat.FromString(contentLic.ContentMetadata.ContentReference.ContentFormat);
var dlOptions = new DownloadOptions(config, libraryBook, contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl) var dlOptions = new DownloadOptions(config, libraryBook, contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl)
{ {
AudibleKey = contentLic?.Voucher?.Key, AudibleKey = contentLic?.Voucher?.Key,

View File

@ -21,6 +21,13 @@ namespace FileLiberator
public TimeSpan RuntimeLength { get; init; } public TimeSpan RuntimeLength { get; init; }
public OutputFormat OutputFormat { get; init; } public OutputFormat OutputFormat { get; init; }
public ChapterInfo ChapterInfo { get; init; } public ChapterInfo ChapterInfo { get; init; }
public string Title => LibraryBook.Book.Title;
public string Subtitle => LibraryBook.Book.Subtitle;
public string Publisher => LibraryBook.Book.Publisher;
public string Language => LibraryBook.Book.Language;
public string AudibleProductId => LibraryBookDto.AudibleProductId;
public string SeriesName => LibraryBookDto.SeriesName;
public float? SeriesNumber => LibraryBookDto.SeriesNumber;
public NAudio.Lame.LameConfig LameConfig { get; init; } public NAudio.Lame.LameConfig LameConfig { get; init; }
public string UserAgent => AudibleApi.Resources.Download_User_Agent; public string UserAgent => AudibleApi.Resources.Download_User_Agent;
public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio; public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio;

View File

@ -163,6 +163,11 @@ namespace FileManager
public override string ToString() => Path; public override string ToString() => Path;
public override int GetHashCode() => Path.GetHashCode();
public override bool Equals(object obj) => obj is LongPath other && Path == other.Path;
public static bool operator ==(LongPath path1, LongPath path2) => path1.Equals(path2);
public static bool operator !=(LongPath path1, LongPath path2) => !path1.Equals(path2);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)] [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength); private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength);

View File

@ -67,13 +67,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia" Version="11.0.0-rc2.2" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc2.2" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-rc1.1" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-rc2.2" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc2.2" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc2.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc2.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" /> <ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />

View File

@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="750" d:DesignHeight="600" mc:Ignorable="d" d:DesignWidth="750" d:DesignHeight="650"
xmlns:controls="clr-namespace:LibationAvalonia.Controls" xmlns:controls="clr-namespace:LibationAvalonia.Controls"
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings" xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
x:DataType="vm:AudioSettingsVM" x:DataType="vm:AudioSettingsVM"
@ -32,6 +32,18 @@
Grid.Row="0" Grid.Row="0"
Grid.Column="0"> Grid.Column="0">
<Grid ColumnDefinitions="*,Auto">
<TextBlock
VerticalAlignment="Center"
Text="{CompiledBinding FileDownloadQualityText}" />
<controls:WheelComboBox
Margin="5,0,0,0"
Grid.Column="1"
ItemsSource="{CompiledBinding DownloadQualities}"
SelectedItem="{CompiledBinding FileDownloadQuality}"/>
</Grid>
<CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}"> <CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding CreateCueSheetText}" /> <TextBlock Text="{CompiledBinding CreateCueSheetText}" />
</CheckBox> </CheckBox>

View File

@ -70,13 +70,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-rc1.1" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" /> <PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-rc2.2" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia" Version="11.0.0-rc2.2" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-rc2.2" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc2.2" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc2.2" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc2.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc1.1" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc2.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -48,12 +48,6 @@ namespace LibationAvalonia
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime()); var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
var appBuilderTask = Task.Run(BuildAvaloniaApp); var appBuilderTask = Task.Run(BuildAvaloniaApp);
LibationScaffolding.SetReleaseIdentifier(Variety.Chardonnay);
if (LibationScaffolding.ReleaseIdentifier is ReleaseIdentifier.None)
return;
if (config.LibationSettingsAreValid) if (config.LibationSettingsAreValid)
{ {
if (!RunDbMigrations(config)) if (!RunDbMigrations(config))
@ -81,7 +75,7 @@ namespace LibationAvalonia
LibationScaffolding.RunPostConfigMigrations(config); LibationScaffolding.RunPostConfigMigrations(config);
LibationScaffolding.RunPostMigrationScaffolding(config); LibationScaffolding.RunPostMigrationScaffolding(config);
return true; return LibationScaffolding.ReleaseIdentifier is not ReleaseIdentifier.None;
} }
catch (Exception exDebug) catch (Exception exDebug)
{ {

View File

@ -48,6 +48,7 @@ namespace LibationAvalonia.ViewModels.Settings
DownloadCoverArt = config.DownloadCoverArt; DownloadCoverArt = config.DownloadCoverArt;
RetainAaxFile = config.RetainAaxFile; RetainAaxFile = config.RetainAaxFile;
DownloadClipsBookmarks = config.DownloadClipsBookmarks; DownloadClipsBookmarks = config.DownloadClipsBookmarks;
FileDownloadQuality = config.FileDownloadQuality;
ClipBookmarkFormat = config.ClipsBookmarksFileFormat; ClipBookmarkFormat = config.ClipsBookmarksFileFormat;
SplitFilesByChapter = config.SplitFilesByChapter; SplitFilesByChapter = config.SplitFilesByChapter;
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits; MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
@ -74,6 +75,7 @@ namespace LibationAvalonia.ViewModels.Settings
config.DownloadCoverArt = DownloadCoverArt; config.DownloadCoverArt = DownloadCoverArt;
config.RetainAaxFile = RetainAaxFile; config.RetainAaxFile = RetainAaxFile;
config.DownloadClipsBookmarks = DownloadClipsBookmarks; config.DownloadClipsBookmarks = DownloadClipsBookmarks;
config.FileDownloadQuality = FileDownloadQuality;
config.ClipsBookmarksFileFormat = ClipBookmarkFormat; config.ClipsBookmarksFileFormat = ClipBookmarkFormat;
config.SplitFilesByChapter = SplitFilesByChapter; config.SplitFilesByChapter = SplitFilesByChapter;
config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits; config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits;
@ -93,7 +95,9 @@ namespace LibationAvalonia.ViewModels.Settings
config.MaxSampleRate = SelectedSampleRate?.Value ?? config.MaxSampleRate; config.MaxSampleRate = SelectedSampleRate?.Value ?? config.MaxSampleRate;
} }
public AvaloniaList<Configuration.DownloadQuality> DownloadQualities { get; } = new(Enum<Configuration.DownloadQuality>.GetValues());
public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues()); public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues());
public string FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality));
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet)); public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));
public string AllowLibationFixupText { get; } = Configuration.GetDescription(nameof(Configuration.AllowLibationFixup)); public string AllowLibationFixupText { get; } = Configuration.GetDescription(nameof(Configuration.AllowLibationFixup));
public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt)); public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt));
@ -109,6 +113,7 @@ namespace LibationAvalonia.ViewModels.Settings
public bool DownloadCoverArt { get; set; } public bool DownloadCoverArt { get; set; }
public bool RetainAaxFile { get; set; } public bool RetainAaxFile { get; set; }
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); } public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
public Configuration.DownloadQuality FileDownloadQuality { get; set; }
public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; } public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; }
public bool MergeOpeningAndEndCredits { get; set; } public bool MergeOpeningAndEndCredits { get; set; }
public bool StripAudibleBrandAudio { get; set; } public bool StripAudibleBrandAudio { get; set; }

View File

@ -0,0 +1,76 @@
using System;
using System.IO;
namespace LibationCli;
internal class ConsoleProgressBar
{
public TextWriter Output { get; }
public int MaxWidth { get; }
public char ProgressChar { get; }
public char NoProgressChar { get; }
public double? Progress
{
get => m_Progress;
set
{
m_Progress = value ?? 0;
WriteProgress();
}
}
public TimeSpan RemainingTime
{
get => m_RemainingTime;
set
{
m_RemainingTime = value;
WriteProgress();
}
}
private double m_Progress;
private TimeSpan m_RemainingTime;
private int m_LastWriteLength = 0;
private const int MAX_ETA_LEN = 10;
private readonly int m_NumProgressPieces;
public ConsoleProgressBar(
TextWriter output,
int maxWidth = 80,
char progressCharr = '#',
char noProgressChar = '.')
{
Output = output;
MaxWidth = maxWidth;
ProgressChar = progressCharr;
NoProgressChar = noProgressChar;
m_NumProgressPieces = MaxWidth - MAX_ETA_LEN - 4;
}
private void WriteProgress()
{
var numCompleted = (int)Math.Round(double.Min(100, m_Progress) * m_NumProgressPieces / 100);
var numRemaining = m_NumProgressPieces - numCompleted;
var progressBar = $"[{new string(ProgressChar, numCompleted)}{new string(NoProgressChar, numRemaining)}] ";
progressBar += RemainingTime.TotalMinutes > 1000
? "ETA ∞"
: $"ETA {(int)RemainingTime.TotalMinutes}:{RemainingTime.Seconds:D2}";
Output.Write(new string('\b', m_LastWriteLength) + progressBar);
if (progressBar.Length < m_LastWriteLength)
{
var extra = m_LastWriteLength - progressBar.Length;
Output.Write(new string(' ', extra) + new string('\b', extra));
}
m_LastWriteLength = progressBar.Length;
}
public void Clear()
=> Output.Write(
new string('\b', m_LastWriteLength) +
new string(' ', m_LastWriteLength) +
new string('\b', m_LastWriteLength));
}

View File

@ -0,0 +1,48 @@
using AppScaffolding;
using CommandLine;
using CommandLine.Text;
namespace LibationCli;
[Verb("help", HelpText = "Display more information on a specific command.")]
internal class HelpVerb
{
/// <summary>
/// Name of the verb to get help about
/// </summary>
[Value(0, Default = "")]
public string HelpType { get; set; }
/// <summary>
/// Create a base <see cref="HelpText"/> for <see cref="LibationCli"/>
/// </summary>
public static HelpText CreateHelpText() => new HelpText
{
AutoVersion = false,
AutoHelp = false,
Heading = $"LibationCli v{LibationScaffolding.BuildVersion.ToString(3)}",
AdditionalNewLineAfterOption = true,
MaximumDisplayWidth = 80
};
/// <summary>
/// Get the <see cref="HelpType"/>'s <see cref="HelpText"/>
/// </summary>
public HelpText GetHelpText()
{
var helpText = CreateHelpText();
var result = new Parser().ParseArguments(new string[] { HelpType }, Program.VerbTypes);
if (result.TypeInfo.Current == typeof(NullInstance))
{
//HelpType is not a defined verb so get LibationCli usage
helpText.AddVerbs(Program.VerbTypes);
}
else
{
helpText.AutoHelp = true;
helpText.AddDashesToOption = true;
helpText.AddOptions(result);
}
return helpText;
}
}

View File

@ -3,7 +3,8 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun> <PublishReadyToRun>true</PublishReadyToRun>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>

View File

@ -1,8 +1,5 @@
using System; using CommandLine;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommandLine;
namespace LibationCli namespace LibationCli
{ {

View File

@ -1,10 +1,8 @@
using System; using ApplicationServices;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using AudibleUtilities;
using CommandLine; using CommandLine;
using System;
using System.IO;
using System.Threading.Tasks;
namespace LibationCli namespace LibationCli
{ {
@ -29,26 +27,38 @@ namespace LibationCli
} }
*/ */
#endregion #endregion
[Option(shortName: 'x', longName: "xlsx", SetName = "xlsx", Required = true)] [Option(shortName: 'x', longName: "xlsx", HelpText = "Microsoft Excel Spreadsheet", SetName = "xlsx")]
public bool xlsx { get; set; } public bool xlsx { get; set; }
[Option(shortName: 'c', longName: "csv", SetName = "csv", Required = true)] [Option(shortName: 'c', longName: "csv", HelpText = "Comma-separated values", SetName = "csv")]
public bool csv { get; set; } public bool csv { get; set; }
[Option(shortName: 'j', longName: "json", SetName = "json", Required = true)] [Option(shortName: 'j', longName: "json", HelpText = "JavaScript Object Notation", SetName = "json")]
public bool json { get; set; } public bool json { get; set; }
protected override Task ProcessAsync() protected override Task ProcessAsync()
{ {
if (xlsx) Action<string> exporter
LibraryExporter.ToXlsx(FilePath); = csv ? LibraryExporter.ToCsv
if (csv) : json ? LibraryExporter.ToJson
LibraryExporter.ToCsv(FilePath); : xlsx ? LibraryExporter.ToXlsx
if (json) : Path.GetExtension(FilePath)?.ToLower() switch
LibraryExporter.ToJson(FilePath); {
".xlsx" => LibraryExporter.ToXlsx,
".csv" => LibraryExporter.ToCsv,
".json" => LibraryExporter.ToJson,
_ => null
};
if (exporter is null)
{
PrintVerbUsage($"Undefined export format for file type \"{Path.GetExtension(FilePath)}\"");
}
else
{
exporter(FilePath);
Console.WriteLine($"Library exported to: {FilePath}"); Console.WriteLine($"Library exported to: {FilePath}");
}
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@ -1,10 +1,7 @@
using System; using CommandLine;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommandLine;
using DataLayer; using DataLayer;
using FileLiberator; using FileLiberator;
using System.Threading.Tasks;
namespace LibationCli namespace LibationCli
{ {

View File

@ -1,18 +1,18 @@
using System; using ApplicationServices;
using AudibleUtilities;
using CommandLine;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ApplicationServices;
using AudibleUtilities;
using CommandLine;
namespace LibationCli namespace LibationCli
{ {
[Verb("scan", HelpText = "Scan library. Default: scan all accounts. Optional: use 'account' flag to specify a single account.")] [Verb("scan", HelpText = "Scan library. Default: scan all accounts. Optional: use 'account' flag to specify a single account.")]
public class ScanOptions : OptionsBase public class ScanOptions : OptionsBase
{ {
[Value(0, MetaName = "Accounts", HelpText = "Optional: nicknames of accounts to scan.", Required = false)] [Value(0, MetaName = "Accounts", HelpText = "Optional: user ID or nicknames of accounts to scan.", Required = false)]
public IEnumerable<string> AccountNicknames { get; set; } public IEnumerable<string> AccountNames { get; set; }
protected override async Task ProcessAsync() protected override async Task ProcessAsync()
{ {
@ -42,13 +42,19 @@ namespace LibationCli
private Account[] getAccounts() private Account[] getAccounts()
{ {
using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.GetAll().ToArray(); var allAccounts = persister.AccountsSettings.GetAll().ToArray();
if (!AccountNicknames.Any()) if (!AccountNames.Any())
return accounts; return allAccounts;
var found = accounts.Where(acct => AccountNicknames.Contains(acct.AccountName)).ToArray(); var accountNames = AccountNames.Select(n => n.ToLower()).ToArray();
var notFound = AccountNicknames.Except(found.Select(f => f.AccountName)).ToArray();
var found
= allAccounts
.Where(acct => accountNames.Contains(acct.AccountName.ToLower()) || accountNames.Contains(acct.AccountId.ToLower()))
.ToArray();
var notFound = allAccounts.Except(found).ToArray();
// no accounts found. do not continue // no accounts found. do not continue
if (!found.Any()) if (!found.Any())

View File

@ -0,0 +1,56 @@
using ApplicationServices;
using CommandLine;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationCli.Options;
[Verb("search", HelpText = "Search for books in your library")]
internal class SearchOptions : OptionsBase
{
[Option('n', Default = 10, HelpText = "Number of search results per page")]
public int NumResultsPerPage { get; set; }
[Value(0, MetaName = "query", Required = true, HelpText = "Lucene search string")]
public IEnumerable<string> Query { get; set; }
protected override Task ProcessAsync()
{
var query = string.Join(" ", Query).Trim('\"');
var results = SearchEngineCommands.Search(query).Docs.ToList();
Console.WriteLine($"Found {results.Count} matching results.");
string nextPrompt = "Press any key for the next " + NumResultsPerPage + " results or Esc for all results";
bool waitForNextBatch = true;
for (int i = 0; i < results.Count; i += NumResultsPerPage)
{
var sb = new StringBuilder();
for (int j = i; j < int.Min(results.Count, i + NumResultsPerPage); j++)
sb.AppendLine(getDocDisplay(results[j].Doc));
Console.Write(sb.ToString());
if (waitForNextBatch)
{
Console.Write(nextPrompt);
waitForNextBatch = Console.ReadKey(intercept: true).Key != ConsoleKey.Escape;
ReplaceConsoleText(Console.Out, nextPrompt.Length, "");
Console.CursorLeft = 0;
}
}
return Task.CompletedTask;
}
private static string getDocDisplay(Lucene.Net.Documents.Document doc)
{
var title = doc.GetField("title");
var id = doc.GetField("_ID_");
return $"[{id.StringValue}] - {title.StringValue}";
}
}

View File

@ -1,31 +1,63 @@
using System; using ApplicationServices;
using CommandLine;
using DataLayer;
using Dinah.Core;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ApplicationServices;
using AudibleUtilities;
using CommandLine;
namespace LibationCli namespace LibationCli
{ {
[Verb("set-status", HelpText = """ [Verb("set-status", HelpText = """
Set download statuses throughout library based on whether each book's audio file can be found. Set download statuses throughout library based on whether each book's audio file can be found.
Must include at least one flag: --downloaded , --not-downloaded.
Downloaded: If the audio file can be found, set download status to 'Downloaded'.
Not Downloaded: If the audio file cannot be found, set download status to 'Not Downloaded'
""")] """)]
public class SetDownloadStatusOptions : OptionsBase public class SetDownloadStatusOptions : OptionsBase
{ {
[Option(shortName: 'd', longName: "downloaded", Required = true)] //https://github.com/commandlineparser/commandline/wiki/Option-Groups
[Option(shortName: 'd', longName: "downloaded", Group = "Download Status", HelpText = "set download status to 'Downloaded'")]
public bool SetDownloaded { get; set; } public bool SetDownloaded { get; set; }
[Option(shortName: 'n', longName: "not-downloaded", Required = true)] [Option(shortName: 'n', longName: "not-downloaded", Group = "Download Status", HelpText = "set download status to 'Not Downloaded'")]
public bool SetNotDownloaded { get; set; } public bool SetNotDownloaded { get; set; }
[Option("force", HelpText = "Set the download status regardless of whether the book's audio file can be found. Only one download status option may be used with this option.")]
public bool Force { get; set; }
[Value(0, MetaName = "[asins]", HelpText = "Optional product IDs of books on which to set download status.")]
public IEnumerable<string> Asins { get; set; }
protected override async Task ProcessAsync() protected override async Task ProcessAsync()
{ {
if (Force && SetDownloaded && SetNotDownloaded)
{
PrintVerbUsage("ERROR:\nWhen run with --force option, only one download status option may be used.");
return;
}
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking(); var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
if (Asins.Any())
{
var asins = Asins.Select(a => a.TrimStart('[').TrimEnd(']').ToLower()).ToArray();
libraryBooks = libraryBooks.Where(lb => lb.Book.AudibleProductId.ToLower().In(asins)).ToList();
if (libraryBooks.Count == 0)
{
Console.Error.WriteLine("Could not find any books matching asins");
return;
}
}
if (Force)
{
var status = SetDownloaded ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
var num = libraryBooks.UpdateBookStatus(status);
Console.WriteLine($"Set LiberatedStatus to '{status}' on {"book".PluralizeWithCount(num)}");
}
else
{
var bulkSetStatus = new BulkSetDownloadStatus(libraryBooks, SetDownloaded, SetNotDownloaded); var bulkSetStatus = new BulkSetDownloadStatus(libraryBooks, SetDownloaded, SetNotDownloaded);
await Task.Run(() => bulkSetStatus.Discover()); await Task.Run(() => bulkSetStatus.Discover());
bulkSetStatus.Execute(); bulkSetStatus.Execute();
@ -34,4 +66,5 @@ namespace LibationCli
Console.WriteLine(msg); Console.WriteLine(msg);
} }
} }
}
} }

View File

@ -0,0 +1,59 @@
using AppScaffolding;
using CommandLine;
using System;
using System.Threading.Tasks;
namespace LibationCli.Options;
[Verb("version", HelpText = "Display version information.")]
internal class VersionOptions : OptionsBase
{
[Option('c', "check", Required = false, HelpText = "Check if an upgrade is available")]
public bool CheckForUpgrade { get; set; }
protected override Task ProcessAsync()
{
const string checkingForUpgrade = "Checking for upgrade...";
Console.WriteLine($"Libation {LibationScaffolding.Variety} v{LibationScaffolding.BuildVersion.ToString(3)}");
if (CheckForUpgrade)
{
Console.Write(checkingForUpgrade);
var origColor = Console.ForegroundColor;
try
{
var upgradeProperties = LibationScaffolding.GetLatestRelease();
if (upgradeProperties is null)
{
Console.ForegroundColor = ConsoleColor.Green;
ReplaceConsoleText(Console.Out, checkingForUpgrade.Length, "No available upgrade");
Console.WriteLine();
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
ReplaceConsoleText(Console.Out, checkingForUpgrade.Length, $"Upgrade Available: v{upgradeProperties.LatestRelease.ToString(3)}");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine(upgradeProperties.ZipUrl);
Console.WriteLine();
Console.WriteLine("Release Notes");
Console.WriteLine("=============");
Console.WriteLine(upgradeProperties.Notes);
}
}
catch
{
Console.Error.WriteLine("ERROR CHECKING FOR UPGRADE");
}
finally
{
Console.ForegroundColor = origColor;
}
}
return Task.CompletedTask;
}
}

View File

@ -1,8 +1,8 @@
using System; using CommandLine;
using System.Collections.Generic; using System;
using System.Linq; using System.IO;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommandLine;
namespace LibationCli namespace LibationCli
{ {
@ -17,15 +17,34 @@ namespace LibationCli
catch (Exception ex) catch (Exception ex)
{ {
Environment.ExitCode = (int)ExitCode.RunTimeError; Environment.ExitCode = (int)ExitCode.RunTimeError;
PrintVerbUsage(new string[]
Console.Error.WriteLine("ERROR"); {
Console.Error.WriteLine("====="); "ERROR",
Console.Error.WriteLine(ex.Message); "=====",
Console.Error.WriteLine(); ex.Message,
Console.Error.WriteLine(ex.StackTrace); "",
ex.StackTrace
});
} }
} }
protected void PrintVerbUsage(params string[] linesBeforeUsage)
{
var verb = GetType().GetCustomAttribute<VerbAttribute>().Name;
var helpText = new HelpVerb { HelpType = verb }.GetHelpText();
helpText.AddPreOptionsLines(linesBeforeUsage);
helpText.AddPreOptionsLine("");
helpText.AddPreOptionsLine($"{verb} Usage:");
Console.Error.WriteLine(helpText);
}
protected static void ReplaceConsoleText(TextWriter writer, int previousLength, string newText)
{
writer.Write(new string('\b', previousLength));
writer.Write(newText);
writer.Write(new string(' ', int.Max(0, previousLength - newText.Length)));
}
protected abstract Task ProcessAsync(); protected abstract Task ProcessAsync();
} }
} }

View File

@ -1,23 +1,34 @@
using System; using ApplicationServices;
using CommandLine;
using DataLayer;
using Dinah.Core;
using FileLiberator;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ApplicationServices;
using CommandLine;
using DataLayer;
using FileLiberator;
namespace LibationCli namespace LibationCli
{ {
public abstract class ProcessableOptionsBase : OptionsBase public abstract class ProcessableOptionsBase : OptionsBase
{ {
[Value(0, MetaName = "[asins]", HelpText = "Optional product IDs of books to process.")]
public IEnumerable<string> Asins { get; set; }
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook> completedAction = null) protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook> completedAction = null)
where TProcessable : Processable, new() where TProcessable : Processable, new()
{ {
var progressBar = new ConsoleProgressBar(Console.Out);
var strProc = new TProcessable(); var strProc = new TProcessable();
strProc.Begin += (o, e) => Console.WriteLine($"{typeof(TProcessable).Name} Begin: {e}"); strProc.Begin += (o, e) => Console.WriteLine($"{typeof(TProcessable).Name} Begin: {e}");
strProc.Completed += (o, e) => Console.WriteLine($"{typeof(TProcessable).Name} Completed: {e}");
strProc.Completed += (o, e) =>
{
progressBar.Clear();
Console.WriteLine($"{typeof(TProcessable).Name} Completed: {e}");
};
strProc.Completed += (s, e) => strProc.Completed += (s, e) =>
{ {
@ -32,13 +43,28 @@ namespace LibationCli
} }
}; };
strProc.StreamingTimeRemaining += (_, e) => progressBar.RemainingTime = e;
strProc.StreamingProgressChanged += (_, e) => progressBar.Progress = e.ProgressPercentage;
return strProc; return strProc;
} }
protected static async Task RunAsync(Processable Processable) protected async Task RunAsync(Processable Processable)
{ {
foreach (var libraryBook in Processable.GetValidLibraryBooks(DbContexts.GetLibrary_Flat_NoTracking())) var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
await ProcessOneAsync(Processable, libraryBook, false);
if (Asins.Any())
{
var asinsLower = Asins.Select(a => a.TrimStart('[').TrimEnd(']').ToLower()).ToArray();
foreach (var lb in libraryBooks.Where(lb => lb.Book.AudibleProductId.ToLower().In(asinsLower)))
await ProcessOneAsync(Processable, lb, true);
}
else
{
foreach (var lb in Processable.GetValidLibraryBooks(libraryBooks))
await ProcessOneAsync(Processable, lb, false);
}
var done = "Done. All books have been processed"; var done = "Done. All books have been processed";
Console.WriteLine(done); Console.WriteLine(done);

View File

@ -1,12 +1,9 @@
using System; using CommandLine;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommandLine;
using CommandLine.Text; using CommandLine.Text;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.Collections; using System;
using Dinah.Core.Collections.Generic; using System.Linq;
using System.Threading.Tasks;
namespace LibationCli namespace LibationCli
{ {
@ -19,47 +16,63 @@ namespace LibationCli
} }
class Program class Program
{ {
static async Task<int> Main(string[] args) public readonly static Type[] VerbTypes = Setup.LoadVerbs();
static async Task Main(string[] args)
{ {
//***********************************************//
// //
// do not use Configuration before this line //
// //
//***********************************************//
Setup.Initialize();
var types = Setup.LoadVerbs();
#if DEBUG #if DEBUG
string input = null; string input = "";
//input = " set-status -n --force B017V4IM1G";
//input = " liberate B017V4IM1G";
//input = " convert B017V4IM1G";
//input = " search \"-liberated\"";
//input = " export --help"; //input = " export --help";
//input = " version --check";
//input = " scan rmcrackan"; //input = " scan rmcrackan";
//input = " help set-status";
//input = " liberate "; //input = " liberate ";
// note: this hack will fail for quoted file paths with spaces because it will break on those spaces // note: this hack will fail for quoted file paths with spaces because it will break on those spaces
if (!string.IsNullOrWhiteSpace(input)) if (!string.IsNullOrWhiteSpace(input))
args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var setBreakPointHere = args; var setBreakPointHere = args;
#endif #endif
var result = Parser.Default.ParseArguments(args, types); var result = new Parser(ConfigureParser).ParseArguments(args, VerbTypes);
if (result.Value is HelpVerb helper)
Console.Error.WriteLine(helper.GetHelpText());
else if (result.TypeInfo.Current == typeof(HelpVerb))
{
//Error parsing the command, but the verb type was identified as HelpVerb
//Print LibationCli usage
var helpText = HelpVerb.CreateHelpText();
helpText.AddVerbs(VerbTypes);
Console.Error.WriteLine(helpText);
}
else if (result.Errors.Any())
HandleErrors(result);
else
{
//Everything parsed correctly, so execute the command
//***********************************************//
// //
// do not use Configuration before this line //
// //
//***********************************************//
Setup.Initialize();
// if successfully parsed // if successfully parsed
// async: run parsed options // async: run parsed options
await result.WithParsedAsync<OptionsBase>(opt => opt.Run()); await result.WithParsedAsync<OptionsBase>(opt => opt.Run());
}
// if not successfully parsed
// sync: handle parse errors
result.WithNotParsed(errors => HandleErrors(result, errors));
return Environment.ExitCode;
} }
private static void HandleErrors(ParserResult<object> result, IEnumerable<Error> errors) private static void HandleErrors(ParserResult<object> result)
{ {
var errorsList = errors.ToList(); var errorsList = result.Errors.ToList();
if (errorsList.Any(e => e.Tag.In(ErrorType.HelpRequestedError, ErrorType.VersionRequestedError, ErrorType.HelpVerbRequestedError))) if (errorsList.Any(e => e.Tag.In(ErrorType.HelpRequestedError, ErrorType.VersionRequestedError, ErrorType.HelpVerbRequestedError)))
{ {
Environment.ExitCode = (int)ExitCode.NonRunNonError; Environment.ExitCode = (int)ExitCode.NonRunNonError;
@ -67,17 +80,36 @@ namespace LibationCli
} }
Environment.ExitCode = (int)ExitCode.ParseError; Environment.ExitCode = (int)ExitCode.ParseError;
var helpText = HelpVerb.CreateHelpText();
if (errorsList.Any(e => e.Tag.In(ErrorType.NoVerbSelectedError))) if (errorsList.OfType<NoVerbSelectedError>().Any())
{ {
Console.Error.WriteLine("No verb selected"); //Print LibationCli usage
return; helpText.AddPreOptionsLine("No verb selected");
helpText.AddVerbs(VerbTypes);
}
else
{
//print the specified verb's usage
helpText.AddDashesToOption = true;
helpText.AutoHelp = true;
if (!errorsList.OfType<UnknownOptionError>().Any(o => o.Token.ToLower() == "help"))
{
//verb was not executed with the "--help" option,
//so print verb option parsing error info.
helpText = HelpText.DefaultParsingErrorsHandler(result, helpText);
} }
var helpText = HelpText.AutoBuild(result, helpText.AddOptions(result);
h => HelpText.DefaultParsingErrorsHandler(result, h), }
e => e); Console.Error.WriteLine(helpText);
Console.WriteLine(helpText); }
private static void ConfigureParser(ParserSettings settings)
{
settings.AutoVersion = false;
settings.AutoHelp = false;
} }
} }
} }

View File

@ -1,14 +1,8 @@
using System; using AppScaffolding;
using System.Collections.Generic; using CommandLine;
using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using AppScaffolding;
using CommandLine;
using CommandLine.Text;
using Dinah.Core;
using Dinah.Core.Collections;
using Dinah.Core.Collections.Generic;
namespace LibationCli namespace LibationCli
{ {
@ -26,28 +20,6 @@ namespace LibationCli
LibationScaffolding.RunPostConfigMigrations(config); LibationScaffolding.RunPostConfigMigrations(config);
LibationScaffolding.RunPostMigrationScaffolding(config); LibationScaffolding.RunPostMigrationScaffolding(config);
#if !DEBUG
checkForUpdate();
#endif
}
private static void checkForUpdate()
{
var upgradeProperties = LibationScaffolding.GetLatestRelease();
if (upgradeProperties is null)
return;
var origColor = Console.ForegroundColor;
try
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"UPDATE AVAILABLE @ {upgradeProperties.ZipUrl}");
}
finally
{
Console.ForegroundColor = origColor;
}
} }
public static Type[] LoadVerbs() => Assembly.GetExecutingAssembly() public static Type[] LoadVerbs() => Assembly.GetExecutingAssembly()

View File

@ -126,7 +126,16 @@ namespace LibationFileManager
BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories); BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
var regex = GetBookSearchRegex(productId); var regex = GetBookSearchRegex(productId);
return BookDirectoryFiles.FindFiles(regex);
//Find all extant files matching the productId
//using both the file system and the file path cache
return
FilePathCache
.GetFiles(productId)
.Where(c => c.fileType == FileType.Audio && File.Exists(c.path))
.Select(c => c.path)
.Union(BookDirectoryFiles.FindFiles(regex))
.ToList();
} }
public void Refresh() => BookDirectoryFiles.RefreshFiles(); public void Refresh() => BookDirectoryFiles.RefreshFiles();

View File

@ -205,6 +205,16 @@ namespace LibationFileManager
Added Added
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum DownloadQuality
{
High,
Normal
}
[Description("Audio quality to request from Audible:")]
public DownloadQuality FileDownloadQuality { get => GetNonString(defaultValue: DownloadQuality.High); set => SetNonString(value); }
[Description("Set file \"created\" timestamp to:")] [Description("Set file \"created\" timestamp to:")]
public DateTimeSource CreationTime { get => GetNonString(defaultValue: DateTimeSource.File); set => SetNonString(value); } public DateTimeSource CreationTime { get => GetNonString(defaultValue: DateTimeSource.File); set => SetNonString(value); }

View File

@ -9,6 +9,7 @@ namespace LibationWinForms.Dialogs
{ {
private void Load_AudioSettings(Configuration config) private void Load_AudioSettings(Configuration config)
{ {
this.fileDownloadQualityLbl.Text = desc(nameof(config.FileDownloadQuality));
this.allowLibationFixupCbox.Text = desc(nameof(config.AllowLibationFixup)); this.allowLibationFixupCbox.Text = desc(nameof(config.AllowLibationFixup));
this.createCueSheetCbox.Text = desc(nameof(config.CreateCueSheet)); this.createCueSheetCbox.Text = desc(nameof(config.CreateCueSheet));
this.downloadCoverArtCbox.Text = desc(nameof(config.DownloadCoverArt)); this.downloadCoverArtCbox.Text = desc(nameof(config.DownloadCoverArt));
@ -19,6 +20,13 @@ namespace LibationWinForms.Dialogs
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged)); this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning)); this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning));
fileDownloadQualityCb.Items.AddRange(
new object[]
{
Configuration.DownloadQuality.Normal,
Configuration.DownloadQuality.High
});
clipsBookmarksFormatCb.Items.AddRange( clipsBookmarksFormatCb.Items.AddRange(
new object[] new object[]
{ {
@ -44,6 +52,7 @@ namespace LibationWinForms.Dialogs
createCueSheetCbox.Checked = config.CreateCueSheet; createCueSheetCbox.Checked = config.CreateCueSheet;
downloadCoverArtCbox.Checked = config.DownloadCoverArt; downloadCoverArtCbox.Checked = config.DownloadCoverArt;
downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks; downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks;
fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality;
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat; clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
retainAaxFileCbox.Checked = config.RetainAaxFile; retainAaxFileCbox.Checked = config.RetainAaxFile;
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter; splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
@ -87,6 +96,7 @@ namespace LibationWinForms.Dialogs
config.CreateCueSheet = createCueSheetCbox.Checked; config.CreateCueSheet = createCueSheetCbox.Checked;
config.DownloadCoverArt = downloadCoverArtCbox.Checked; config.DownloadCoverArt = downloadCoverArtCbox.Checked;
config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked; config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked;
config.FileDownloadQuality = (Configuration.DownloadQuality)fileDownloadQualityCb.SelectedItem;
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem; config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
config.RetainAaxFile = retainAaxFileCbox.Checked; config.RetainAaxFile = retainAaxFileCbox.Checked;
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked; config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
@ -98,7 +108,6 @@ namespace LibationWinForms.Dialogs
config.LameTargetBitrate = lameTargetBitrateRb.Checked; config.LameTargetBitrate = lameTargetBitrateRb.Checked;
config.MaxSampleRate = ((EnumDiaplay<AAXClean.SampleRate>)maxSampleRateCb.SelectedItem).Value; config.MaxSampleRate = ((EnumDiaplay<AAXClean.SampleRate>)maxSampleRateCb.SelectedItem).Value;
config.LameEncoderQuality = (NAudio.Lame.EncoderQuality)encoderQualityCb.SelectedItem; config.LameEncoderQuality = (NAudio.Lame.EncoderQuality)encoderQualityCb.SelectedItem;
encoderQualityCb.SelectedItem = config.LameEncoderQuality;
config.LameDownsampleMono = lameDownsampleMonoCbox.Checked; config.LameDownsampleMono = lameDownsampleMonoCbox.Checked;
config.LameBitrate = lameBitrateTb.Value; config.LameBitrate = lameBitrateTb.Value;
config.LameConstantBitrate = lameConstantBitrateCbox.Checked; config.LameConstantBitrate = lameConstantBitrateCbox.Checked;

File diff suppressed because it is too large Load Diff

View File

@ -14,15 +14,6 @@ namespace LibationWinForms.Dialogs
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes)); this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes)); this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
this.autoDownloadEpisodesCb.Text = desc(nameof(config.AutoDownloadEpisodes)); this.autoDownloadEpisodesCb.Text = desc(nameof(config.AutoDownloadEpisodes));
creationTimeLbl.Text = desc(nameof(config.CreationTime));
lastWriteTimeLbl.Text = desc(nameof(config.LastWriteTime));
var dateTimeSources = Enum.GetValues<Configuration.DateTimeSource>().Select(v => new EnumDiaplay<Configuration.DateTimeSource>(v)).ToArray();
creationTimeCb.Items.AddRange(dateTimeSources);
lastWriteTimeCb.Items.AddRange(dateTimeSources);
creationTimeCb.SelectedItem = dateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? dateTimeSources[0];
lastWriteTimeCb.SelectedItem = dateTimeSources.SingleOrDefault(v => v.Value == config.LastWriteTime) ?? dateTimeSources[0];
autoScanCb.Checked = config.AutoScan; autoScanCb.Checked = config.AutoScan;
showImportedStatsCb.Checked = config.ShowImportedStats; showImportedStatsCb.Checked = config.ShowImportedStats;
@ -32,9 +23,6 @@ namespace LibationWinForms.Dialogs
} }
private void Save_ImportLibrary(Configuration config) private void Save_ImportLibrary(Configuration config)
{ {
config.CreationTime = ((EnumDiaplay<Configuration.DateTimeSource>)creationTimeCb.SelectedItem).Value;
config.LastWriteTime = ((EnumDiaplay<Configuration.DateTimeSource>)lastWriteTimeCb.SelectedItem).Value;
config.AutoScan = autoScanCb.Checked; config.AutoScan = autoScanCb.Checked;
config.ShowImportedStats = showImportedStatsCb.Checked; config.ShowImportedStats = showImportedStatsCb.Checked;
config.ImportEpisodes = importEpisodesCb.Checked; config.ImportEpisodes = importEpisodesCb.Checked;

View File

@ -1,6 +1,7 @@
using Dinah.Core; using Dinah.Core;
using FileManager; using FileManager;
using LibationFileManager; using LibationFileManager;
using LibationUiBase;
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -25,6 +26,16 @@ namespace LibationWinForms.Dialogs
betaOptInCbox.Text = desc(nameof(config.BetaOptIn)); betaOptInCbox.Text = desc(nameof(config.BetaOptIn));
saveEpisodesToSeriesFolderCbox.Text = desc(nameof(config.SavePodcastsToParentFolder)); saveEpisodesToSeriesFolderCbox.Text = desc(nameof(config.SavePodcastsToParentFolder));
overwriteExistingCbox.Text = desc(nameof(config.OverwriteExisting)); overwriteExistingCbox.Text = desc(nameof(config.OverwriteExisting));
creationTimeLbl.Text = desc(nameof(config.CreationTime));
lastWriteTimeLbl.Text = desc(nameof(config.LastWriteTime));
var dateTimeSources = Enum.GetValues<Configuration.DateTimeSource>().Select(v => new EnumDiaplay<Configuration.DateTimeSource>(v)).ToArray();
creationTimeCb.Items.AddRange(dateTimeSources);
lastWriteTimeCb.Items.AddRange(dateTimeSources);
creationTimeCb.SelectedItem = dateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? dateTimeSources[0];
lastWriteTimeCb.SelectedItem = dateTimeSources.SingleOrDefault(v => v.Value == config.LastWriteTime) ?? dateTimeSources[0];
booksSelectControl.SetSearchTitle("books location"); booksSelectControl.SetSearchTitle("books location");
booksSelectControl.SetDirectoryItems( booksSelectControl.SetDirectoryItems(
@ -82,6 +93,11 @@ namespace LibationWinForms.Dialogs
config.OverwriteExisting = overwriteExistingCbox.Checked; config.OverwriteExisting = overwriteExistingCbox.Checked;
config.BetaOptIn = betaOptInCbox.Checked; config.BetaOptIn = betaOptInCbox.Checked;
config.CreationTime = ((EnumDiaplay<Configuration.DateTimeSource>)creationTimeCb.SelectedItem).Value;
config.LastWriteTime = ((EnumDiaplay<Configuration.DateTimeSource>)lastWriteTimeCb.SelectedItem).Value;
} }

View File

@ -30,8 +30,6 @@ namespace LibationWinForms
ApplicationConfiguration.Initialize(); ApplicationConfiguration.Initialize();
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.Variety.Classic);
//***********************************************// //***********************************************//
// // // //
// do not use Configuration before this line // // do not use Configuration before this line //