Merge pull request #1314 from Mbucari/master
New audio format features, bug fixes, and minor tweaks/improvements.
This commit is contained in:
commit
0bb5bba3c8
@ -46,9 +46,12 @@ These tags will be replaced in the template with the audiobook's values.
|
|||||||
|\<series\>|All series to which the book belongs (if any)|[Series List](#series-list-formatters)|
|
|\<series\>|All series to which the book belongs (if any)|[Series List](#series-list-formatters)|
|
||||||
|\<first series\>|First series|[Series](#series-formatters)|
|
|\<first series\>|First series|[Series](#series-formatters)|
|
||||||
|\<series#\>|Number order in series (alias for \<first series[{#}]\>|[Number](#number-formatters)|
|
|\<series#\>|Number order in series (alias for \<first series[{#}]\>|[Number](#number-formatters)|
|
||||||
|\<bitrate\>|File's original bitrate (Kbps)|[Number](#number-formatters)|
|
|\<bitrate\>|Bitrate (kbps) of the last downloaded audiobook|[Number](#number-formatters)|
|
||||||
|\<samplerate\>|File's original audio sample rate|[Number](#number-formatters)|
|
|\<samplerate\>|Sample rate (Hz) of the last downloaded audiobook|[Number](#number-formatters)|
|
||||||
|\<channels\>|Number of audio channels|[Number](#number-formatters)|
|
|\<channels\>|Number of audio channels in the last downloaded audiobook|[Number](#number-formatters)|
|
||||||
|
|\<codec\>|Audio codec of the last downloaded audiobook|[Text](#text-formatters)|
|
||||||
|
|\<file version\>|Audible's file version number of the last downloaded audiobook|[Text](#text-formatters)|
|
||||||
|
|\<libation version\>|Libation version used during last download of the audiobook|[Text](#text-formatters)|
|
||||||
|\<account\>|Audible account of this book|[Text](#text-formatters)|
|
|\<account\>|Audible account of this book|[Text](#text-formatters)|
|
||||||
|\<account nickname\>|Audible account nickname of this book|[Text](#text-formatters)|
|
|\<account nickname\>|Audible account nickname of this book|[Text](#text-formatters)|
|
||||||
|\<locale\>|Region/country|[Text](#text-formatters)|
|
|\<locale\>|Region/country|[Text](#text-formatters)|
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AAXClean.Codecs" Version="2.0.1.3" />
|
<PackageReference Include="AAXClean.Codecs" Version="2.0.2.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
using AAXClean;
|
using AAXClean;
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
{
|
{
|
||||||
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
|
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
|
||||||
{
|
{
|
||||||
public event EventHandler<AppleTags> RetrievedMetadata;
|
public event EventHandler<AppleTags>? RetrievedMetadata;
|
||||||
|
|
||||||
public Mp4File AaxFile { get; private set; }
|
public Mp4File? AaxFile { get; private set; }
|
||||||
protected Mp4Operation AaxConversion { get; set; }
|
protected Mp4Operation? AaxConversion { get; set; }
|
||||||
|
|
||||||
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
protected AaxcDownloadConvertBase(string outDirectory, string cacheDirectory, IDownloadOptions dlOptions)
|
||||||
: base(outFileName, cacheDirectory, dlOptions) { }
|
: base(outDirectory, cacheDirectory, dlOptions) { }
|
||||||
|
|
||||||
/// <summary>Setting cover art by this method will insert the art into the audiobook metadata</summary>
|
/// <summary>Setting cover art by this method will insert the art into the audiobook metadata</summary>
|
||||||
public override void SetCoverArt(byte[] coverArt)
|
public override void SetCoverArt(byte[] coverArt)
|
||||||
@ -31,11 +33,13 @@ namespace AaxDecrypter
|
|||||||
|
|
||||||
private Mp4File Open()
|
private Mp4File Open()
|
||||||
{
|
{
|
||||||
if (DownloadOptions.InputType is FileType.Dash)
|
if (DownloadOptions.DecryptionKeys is not KeyData[] keys || keys.Length == 0)
|
||||||
|
throw new InvalidOperationException($"{nameof(DownloadOptions.DecryptionKeys)} cannot be null or empty for a '{DownloadOptions.InputType}' file.");
|
||||||
|
else if (DownloadOptions.InputType is FileType.Dash)
|
||||||
{
|
{
|
||||||
//We may have multiple keys , so use the key whose key ID matches
|
//We may have multiple keys , so use the key whose key ID matches
|
||||||
//the dash files default Key ID.
|
//the dash files default Key ID.
|
||||||
var keyIds = DownloadOptions.DecryptionKeys.Select(k => new Guid(k.KeyPart1, bigEndian: true)).ToArray();
|
var keyIds = keys.Select(k => new Guid(k.KeyPart1, bigEndian: true)).ToArray();
|
||||||
|
|
||||||
var dash = new DashFile(InputFileStream);
|
var dash = new DashFile(InputFileStream);
|
||||||
var kidIndex = Array.IndexOf(keyIds, dash.Tenc.DefaultKID);
|
var kidIndex = Array.IndexOf(keyIds, dash.Tenc.DefaultKID);
|
||||||
@ -43,26 +47,38 @@ namespace AaxDecrypter
|
|||||||
if (kidIndex == -1)
|
if (kidIndex == -1)
|
||||||
throw new InvalidOperationException($"None of the {keyIds.Length} key IDs match the dash file's default KeyID of {dash.Tenc.DefaultKID}");
|
throw new InvalidOperationException($"None of the {keyIds.Length} key IDs match the dash file's default KeyID of {dash.Tenc.DefaultKID}");
|
||||||
|
|
||||||
DownloadOptions.DecryptionKeys[0] = DownloadOptions.DecryptionKeys[kidIndex];
|
keys[0] = keys[kidIndex];
|
||||||
var keyId = DownloadOptions.DecryptionKeys[kidIndex].KeyPart1;
|
var keyId = keys[kidIndex].KeyPart1;
|
||||||
var key = DownloadOptions.DecryptionKeys[kidIndex].KeyPart2;
|
var key = keys[kidIndex].KeyPart2 ?? throw new InvalidOperationException($"{nameof(DownloadOptions.DecryptionKeys)} for '{DownloadOptions.InputType}' must have a non-null decryption key (KeyPart2).");
|
||||||
|
|
||||||
dash.SetDecryptionKey(keyId, key);
|
dash.SetDecryptionKey(keyId, key);
|
||||||
|
WriteKeyFile($"KeyId={Convert.ToHexString(keyId)}{Environment.NewLine}Key={Convert.ToHexString(key)}");
|
||||||
return dash;
|
return dash;
|
||||||
}
|
}
|
||||||
else if (DownloadOptions.InputType is FileType.Aax)
|
else if (DownloadOptions.InputType is FileType.Aax)
|
||||||
{
|
{
|
||||||
var aax = new AaxFile(InputFileStream);
|
var aax = new AaxFile(InputFileStream);
|
||||||
aax.SetDecryptionKey(DownloadOptions.DecryptionKeys[0].KeyPart1);
|
var key = keys[0].KeyPart1;
|
||||||
|
aax.SetDecryptionKey(keys[0].KeyPart1);
|
||||||
|
WriteKeyFile($"ActivationBytes={Convert.ToHexString(key)}");
|
||||||
return aax;
|
return aax;
|
||||||
}
|
}
|
||||||
else if (DownloadOptions.InputType is FileType.Aaxc)
|
else if (DownloadOptions.InputType is FileType.Aaxc)
|
||||||
{
|
{
|
||||||
var aax = new AaxFile(InputFileStream);
|
var aax = new AaxFile(InputFileStream);
|
||||||
aax.SetDecryptionKey(DownloadOptions.DecryptionKeys[0].KeyPart1, DownloadOptions.DecryptionKeys[0].KeyPart2);
|
var key = keys[0].KeyPart1;
|
||||||
|
var iv = keys[0].KeyPart2 ?? throw new InvalidOperationException($"{nameof(DownloadOptions.DecryptionKeys)} for '{DownloadOptions.InputType}' must have a non-null initialization vector (KeyPart2).");
|
||||||
|
aax.SetDecryptionKey(keys[0].KeyPart1, iv);
|
||||||
|
WriteKeyFile($"Key={Convert.ToHexString(key)}{Environment.NewLine}IV={Convert.ToHexString(iv)}");
|
||||||
return aax;
|
return aax;
|
||||||
}
|
}
|
||||||
else throw new InvalidOperationException($"{nameof(DownloadOptions.InputType)} of '{DownloadOptions.InputType}' is unknown.");
|
else throw new InvalidOperationException($"{nameof(DownloadOptions.InputType)} of '{DownloadOptions.InputType}' is unknown.");
|
||||||
|
|
||||||
|
void WriteKeyFile(string contents)
|
||||||
|
{
|
||||||
|
var keyFile = Path.Combine(Path.ChangeExtension(InputFileStream.SaveFilePath, ".key"));
|
||||||
|
File.WriteAllText(keyFile, contents + Environment.NewLine);
|
||||||
|
OnTempFileCreated(new(keyFile));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bool Step_GetMetadata()
|
protected bool Step_GetMetadata()
|
||||||
|
|||||||
@ -5,20 +5,20 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
{
|
{
|
||||||
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
|
public class AaxcDownloadMultiConverter : AaxcDownloadConvertBase
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan minChapterLength = TimeSpan.FromSeconds(3);
|
private static readonly TimeSpan minChapterLength = TimeSpan.FromSeconds(3);
|
||||||
private FileStream workingFileStream;
|
private FileStream? workingFileStream;
|
||||||
|
|
||||||
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
public AaxcDownloadMultiConverter(string outDirectory, string cacheDirectory, IDownloadOptions dlOptions)
|
||||||
: base(outFileName, cacheDirectory, dlOptions)
|
: base(outDirectory, cacheDirectory, dlOptions)
|
||||||
{
|
{
|
||||||
AsyncSteps.Name = $"Download, Convert Aaxc To {DownloadOptions.OutputFormat}, and Split";
|
AsyncSteps.Name = $"Download, Convert Aaxc To {DownloadOptions.OutputFormat}, and Split";
|
||||||
AsyncSteps["Step 1: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
|
AsyncSteps["Step 1: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
|
||||||
AsyncSteps["Step 2: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
|
AsyncSteps["Step 2: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
|
||||||
AsyncSteps["Step 3: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
@ -59,6 +59,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
|||||||
*/
|
*/
|
||||||
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||||
{
|
{
|
||||||
|
if (AaxFile is null) return false;
|
||||||
var chapters = DownloadOptions.ChapterInfo.Chapters;
|
var chapters = DownloadOptions.ChapterInfo.Chapters;
|
||||||
|
|
||||||
// Ensure split files are at least minChapterLength in duration.
|
// Ensure split files are at least minChapterLength in duration.
|
||||||
@ -83,10 +84,10 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await (AaxConversion = decryptMultiAsync(splitChapters));
|
await (AaxConversion = decryptMultiAsync(AaxFile, splitChapters));
|
||||||
|
|
||||||
if (AaxConversion.IsCompletedSuccessfully)
|
if (AaxConversion.IsCompletedSuccessfully)
|
||||||
await moveMoovToBeginning(workingFileStream?.Name);
|
await moveMoovToBeginning(AaxFile, workingFileStream?.Name);
|
||||||
|
|
||||||
return AaxConversion.IsCompletedSuccessfully;
|
return AaxConversion.IsCompletedSuccessfully;
|
||||||
}
|
}
|
||||||
@ -97,17 +98,17 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mp4Operation decryptMultiAsync(ChapterInfo splitChapters)
|
private Mp4Operation decryptMultiAsync(Mp4File aaxFile, ChapterInfo splitChapters)
|
||||||
{
|
{
|
||||||
var chapterCount = 0;
|
var chapterCount = 0;
|
||||||
return
|
return
|
||||||
DownloadOptions.OutputFormat == OutputFormat.M4b
|
DownloadOptions.OutputFormat == OutputFormat.M4b
|
||||||
? AaxFile.ConvertToMultiMp4aAsync
|
? aaxFile.ConvertToMultiMp4aAsync
|
||||||
(
|
(
|
||||||
splitChapters,
|
splitChapters,
|
||||||
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback)
|
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback)
|
||||||
)
|
)
|
||||||
: AaxFile.ConvertToMultiMp3Async
|
: aaxFile.ConvertToMultiMp3Async
|
||||||
(
|
(
|
||||||
splitChapters,
|
splitChapters,
|
||||||
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
|
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
|
||||||
@ -116,33 +117,32 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
|||||||
|
|
||||||
void newSplit(int currentChapter, ChapterInfo splitChapters, INewSplitCallback newSplitCallback)
|
void newSplit(int currentChapter, ChapterInfo splitChapters, INewSplitCallback newSplitCallback)
|
||||||
{
|
{
|
||||||
|
moveMoovToBeginning(aaxFile, workingFileStream?.Name).GetAwaiter().GetResult();
|
||||||
|
var newTempFile = GetNewTempFilePath(DownloadOptions.OutputFormat.ToString());
|
||||||
MultiConvertFileProperties props = new()
|
MultiConvertFileProperties props = new()
|
||||||
{
|
{
|
||||||
OutputFileName = OutputFileName,
|
OutputFileName = newTempFile.FilePath,
|
||||||
PartsPosition = currentChapter,
|
PartsPosition = currentChapter,
|
||||||
PartsTotal = splitChapters.Count,
|
PartsTotal = splitChapters.Count,
|
||||||
Title = newSplitCallback?.Chapter?.Title,
|
Title = newSplitCallback.Chapter?.Title,
|
||||||
};
|
};
|
||||||
|
|
||||||
moveMoovToBeginning(workingFileStream?.Name).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
newSplitCallback.OutputFile = workingFileStream = createOutputFileStream(props);
|
newSplitCallback.OutputFile = workingFileStream = createOutputFileStream(props);
|
||||||
newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitle(props);
|
newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitle(props);
|
||||||
newSplitCallback.TrackNumber = currentChapter;
|
newSplitCallback.TrackNumber = currentChapter;
|
||||||
newSplitCallback.TrackCount = splitChapters.Count;
|
newSplitCallback.TrackCount = splitChapters.Count;
|
||||||
|
|
||||||
OnFileCreated(workingFileStream.Name);
|
OnTempFileCreated(newTempFile with { PartProperties = props });
|
||||||
}
|
}
|
||||||
|
|
||||||
FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
|
FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
|
||||||
{
|
{
|
||||||
var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties);
|
FileUtility.SaferDelete(multiConvertFileProperties.OutputFileName);
|
||||||
FileUtility.SaferDelete(fileName);
|
return File.Open(multiConvertFileProperties.OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||||
return File.Open(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mp4Operation moveMoovToBeginning(string filename)
|
private Mp4Operation moveMoovToBeginning(Mp4File aaxFile, string? filename)
|
||||||
{
|
{
|
||||||
if (DownloadOptions.OutputFormat is OutputFormat.M4b
|
if (DownloadOptions.OutputFormat is OutputFormat.M4b
|
||||||
&& DownloadOptions.MoveMoovToBeginning
|
&& DownloadOptions.MoveMoovToBeginning
|
||||||
@ -151,7 +151,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
|||||||
{
|
{
|
||||||
return Mp4File.RelocateMoovAsync(filename);
|
return Mp4File.RelocateMoovAsync(filename);
|
||||||
}
|
}
|
||||||
else return Mp4Operation.FromCompleted(AaxFile);
|
else return Mp4Operation.FromCompleted(aaxFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,13 +6,16 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
{
|
{
|
||||||
public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
|
public class AaxcDownloadSingleConverter : AaxcDownloadConvertBase
|
||||||
{
|
{
|
||||||
private readonly AverageSpeed averageSpeed = new();
|
private readonly AverageSpeed averageSpeed = new();
|
||||||
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
private TempFile? outputTempFile;
|
||||||
: base(outFileName, cacheDirectory, dlOptions)
|
|
||||||
|
public AaxcDownloadSingleConverter(string outDirectory, string cacheDirectory, IDownloadOptions dlOptions)
|
||||||
|
: base(outDirectory, cacheDirectory, dlOptions)
|
||||||
{
|
{
|
||||||
var step = 1;
|
var step = 1;
|
||||||
|
|
||||||
@ -21,7 +24,6 @@ namespace AaxDecrypter
|
|||||||
AsyncSteps[$"Step {step++}: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
|
AsyncSteps[$"Step {step++}: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
|
||||||
if (DownloadOptions.MoveMoovToBeginning && DownloadOptions.OutputFormat is OutputFormat.M4b)
|
if (DownloadOptions.MoveMoovToBeginning && DownloadOptions.OutputFormat is OutputFormat.M4b)
|
||||||
AsyncSteps[$"Step {step++}: Move moov atom to beginning"] = Step_MoveMoov;
|
AsyncSteps[$"Step {step++}: Move moov atom to beginning"] = Step_MoveMoov;
|
||||||
AsyncSteps[$"Step {step++}: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
|
|
||||||
AsyncSteps[$"Step {step++}: Create Cue"] = Step_CreateCueAsync;
|
AsyncSteps[$"Step {step++}: Create Cue"] = Step_CreateCueAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,14 +41,16 @@ namespace AaxDecrypter
|
|||||||
|
|
||||||
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||||
{
|
{
|
||||||
FileUtility.SaferDelete(OutputFileName);
|
if (AaxFile is null) return false;
|
||||||
|
outputTempFile = GetNewTempFilePath(DownloadOptions.OutputFormat.ToString());
|
||||||
|
FileUtility.SaferDelete(outputTempFile.FilePath);
|
||||||
|
|
||||||
using var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
using var outputFile = File.Open(outputTempFile.FilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||||
OnFileCreated(OutputFileName);
|
OnTempFileCreated(outputTempFile);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await (AaxConversion = decryptAsync(outputFile));
|
await (AaxConversion = decryptAsync(AaxFile, outputFile));
|
||||||
|
|
||||||
return AaxConversion.IsCompletedSuccessfully;
|
return AaxConversion.IsCompletedSuccessfully;
|
||||||
}
|
}
|
||||||
@ -58,14 +62,15 @@ namespace AaxDecrypter
|
|||||||
|
|
||||||
private async Task<bool> Step_MoveMoov()
|
private async Task<bool> Step_MoveMoov()
|
||||||
{
|
{
|
||||||
AaxConversion = Mp4File.RelocateMoovAsync(OutputFileName);
|
if (outputTempFile is null) return false;
|
||||||
|
AaxConversion = Mp4File.RelocateMoovAsync(outputTempFile.FilePath);
|
||||||
AaxConversion.ConversionProgressUpdate += AaxConversion_MoovProgressUpdate;
|
AaxConversion.ConversionProgressUpdate += AaxConversion_MoovProgressUpdate;
|
||||||
await AaxConversion;
|
await AaxConversion;
|
||||||
AaxConversion.ConversionProgressUpdate -= AaxConversion_MoovProgressUpdate;
|
AaxConversion.ConversionProgressUpdate -= AaxConversion_MoovProgressUpdate;
|
||||||
return AaxConversion.IsCompletedSuccessfully;
|
return AaxConversion.IsCompletedSuccessfully;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AaxConversion_MoovProgressUpdate(object sender, ConversionProgressEventArgs e)
|
private void AaxConversion_MoovProgressUpdate(object? sender, ConversionProgressEventArgs e)
|
||||||
{
|
{
|
||||||
averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds);
|
averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds);
|
||||||
|
|
||||||
@ -84,20 +89,20 @@ namespace AaxDecrypter
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mp4Operation decryptAsync(Stream outputFile)
|
private Mp4Operation decryptAsync(Mp4File aaxFile, Stream outputFile)
|
||||||
=> DownloadOptions.OutputFormat == OutputFormat.Mp3
|
=> DownloadOptions.OutputFormat == OutputFormat.Mp3
|
||||||
? AaxFile.ConvertToMp3Async
|
? aaxFile.ConvertToMp3Async
|
||||||
(
|
(
|
||||||
outputFile,
|
outputFile,
|
||||||
DownloadOptions.LameConfig,
|
DownloadOptions.LameConfig,
|
||||||
DownloadOptions.ChapterInfo
|
DownloadOptions.ChapterInfo
|
||||||
)
|
)
|
||||||
: DownloadOptions.FixupFile
|
: DownloadOptions.FixupFile
|
||||||
? AaxFile.ConvertToMp4aAsync
|
? aaxFile.ConvertToMp4aAsync
|
||||||
(
|
(
|
||||||
outputFile,
|
outputFile,
|
||||||
DownloadOptions.ChapterInfo
|
DownloadOptions.ChapterInfo
|
||||||
)
|
)
|
||||||
: AaxFile.ConvertToMp4aAsync(outputFile);
|
: aaxFile.ConvertToMp4aAsync(outputFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,55 +6,50 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
{
|
{
|
||||||
public enum OutputFormat { M4b, Mp3 }
|
public enum OutputFormat { M4b, Mp3 }
|
||||||
|
|
||||||
public abstract class AudiobookDownloadBase
|
public abstract class AudiobookDownloadBase
|
||||||
{
|
{
|
||||||
public event EventHandler<string> RetrievedTitle;
|
public event EventHandler<string?>? RetrievedTitle;
|
||||||
public event EventHandler<string> RetrievedAuthors;
|
public event EventHandler<string?>? RetrievedAuthors;
|
||||||
public event EventHandler<string> RetrievedNarrators;
|
public event EventHandler<string?>? RetrievedNarrators;
|
||||||
public event EventHandler<byte[]> RetrievedCoverArt;
|
public event EventHandler<byte[]?>? RetrievedCoverArt;
|
||||||
public event EventHandler<DownloadProgress> DecryptProgressUpdate;
|
public event EventHandler<DownloadProgress>? DecryptProgressUpdate;
|
||||||
public event EventHandler<TimeSpan> DecryptTimeRemaining;
|
public event EventHandler<TimeSpan>? DecryptTimeRemaining;
|
||||||
public event EventHandler<string> FileCreated;
|
public event EventHandler<TempFile>? TempFileCreated;
|
||||||
|
|
||||||
public bool IsCanceled { get; protected set; }
|
public bool IsCanceled { get; protected set; }
|
||||||
protected AsyncStepSequence AsyncSteps { get; } = new();
|
protected AsyncStepSequence AsyncSteps { get; } = new();
|
||||||
protected string OutputFileName { get; }
|
protected string OutputDirectory { get; }
|
||||||
public IDownloadOptions DownloadOptions { get; }
|
public IDownloadOptions DownloadOptions { get; }
|
||||||
protected NetworkFileStream InputFileStream => nfsPersister.NetworkFileStream;
|
protected NetworkFileStream InputFileStream => NfsPersister.NetworkFileStream;
|
||||||
protected virtual long InputFilePosition => InputFileStream.Position;
|
protected virtual long InputFilePosition => InputFileStream.Position;
|
||||||
private bool downloadFinished;
|
private bool downloadFinished;
|
||||||
|
|
||||||
private readonly NetworkFileStreamPersister nfsPersister;
|
private NetworkFileStreamPersister? m_nfsPersister;
|
||||||
|
private NetworkFileStreamPersister NfsPersister => m_nfsPersister ??= OpenNetworkFileStream();
|
||||||
private readonly DownloadProgress zeroProgress;
|
private readonly DownloadProgress zeroProgress;
|
||||||
private readonly string jsonDownloadState;
|
private readonly string jsonDownloadState;
|
||||||
private readonly string tempFilePath;
|
private readonly string tempFilePath;
|
||||||
|
|
||||||
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
protected AudiobookDownloadBase(string outDirectory, string cacheDirectory, IDownloadOptions dlOptions)
|
||||||
{
|
{
|
||||||
OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
|
OutputDirectory = ArgumentValidator.EnsureNotNullOrWhiteSpace(outDirectory, nameof(outDirectory));
|
||||||
|
DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions));
|
||||||
|
DownloadOptions.DownloadSpeedChanged += (_, speed) => InputFileStream.SpeedLimit = speed;
|
||||||
|
|
||||||
var outDir = Path.GetDirectoryName(OutputFileName);
|
if (!Directory.Exists(OutputDirectory))
|
||||||
if (!Directory.Exists(outDir))
|
Directory.CreateDirectory(OutputDirectory);
|
||||||
Directory.CreateDirectory(outDir);
|
|
||||||
|
|
||||||
if (!Directory.Exists(cacheDirectory))
|
if (!Directory.Exists(cacheDirectory))
|
||||||
Directory.CreateDirectory(cacheDirectory);
|
Directory.CreateDirectory(cacheDirectory);
|
||||||
|
|
||||||
jsonDownloadState = Path.Combine(cacheDirectory, Path.GetFileName(Path.ChangeExtension(OutputFileName, ".json")));
|
jsonDownloadState = Path.Combine(cacheDirectory, $"{DownloadOptions.AudibleProductId}.json");
|
||||||
tempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
|
tempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
|
||||||
|
|
||||||
DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions));
|
|
||||||
DownloadOptions.DownloadSpeedChanged += (_, speed) => InputFileStream.SpeedLimit = speed;
|
|
||||||
|
|
||||||
// delete file after validation is complete
|
|
||||||
FileUtility.SaferDelete(OutputFileName);
|
|
||||||
|
|
||||||
nfsPersister = OpenNetworkFileStream();
|
|
||||||
|
|
||||||
zeroProgress = new DownloadProgress
|
zeroProgress = new DownloadProgress
|
||||||
{
|
{
|
||||||
BytesReceived = 0,
|
BytesReceived = 0,
|
||||||
@ -65,24 +60,30 @@ namespace AaxDecrypter
|
|||||||
OnDecryptProgressUpdate(zeroProgress);
|
OnDecryptProgressUpdate(zeroProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected TempFile GetNewTempFilePath(string extension)
|
||||||
|
{
|
||||||
|
extension = FileUtility.GetStandardizedExtension(extension);
|
||||||
|
var path = Path.Combine(OutputDirectory, Guid.NewGuid().ToString("N") + extension);
|
||||||
|
return new(path, extension);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> RunAsync()
|
public async Task<bool> RunAsync()
|
||||||
{
|
{
|
||||||
await InputFileStream.BeginDownloadingAsync();
|
await InputFileStream.BeginDownloadingAsync();
|
||||||
var progressTask = Task.Run(reportProgress);
|
var progressTask = Task.Run(reportProgress);
|
||||||
|
|
||||||
AsyncSteps[$"Cleanup"] = CleanupAsync;
|
|
||||||
(bool success, var elapsed) = await AsyncSteps.RunAsync();
|
(bool success, var elapsed) = await AsyncSteps.RunAsync();
|
||||||
|
|
||||||
//Stop the downloader so it doesn't keep running in the background.
|
//Stop the downloader so it doesn't keep running in the background.
|
||||||
if (!success)
|
if (!success)
|
||||||
nfsPersister.Dispose();
|
NfsPersister.Dispose();
|
||||||
|
|
||||||
await progressTask;
|
await progressTask;
|
||||||
|
|
||||||
var speedup = DownloadOptions.RuntimeLength / elapsed;
|
var speedup = DownloadOptions.RuntimeLength / elapsed;
|
||||||
Serilog.Log.Information($"Speedup is {speedup:F0}x realtime.");
|
Serilog.Log.Information($"Speedup is {speedup:F0}x realtime.");
|
||||||
|
|
||||||
nfsPersister.Dispose();
|
NfsPersister.Dispose();
|
||||||
return success;
|
return success;
|
||||||
|
|
||||||
async Task reportProgress()
|
async Task reportProgress()
|
||||||
@ -129,50 +130,43 @@ namespace AaxDecrypter
|
|||||||
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
|
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
|
||||||
|
|
||||||
public virtual void SetCoverArt(byte[] coverArt) { }
|
public virtual void SetCoverArt(byte[] coverArt) { }
|
||||||
|
protected void OnRetrievedTitle(string? title)
|
||||||
protected void OnRetrievedTitle(string title)
|
|
||||||
=> RetrievedTitle?.Invoke(this, title);
|
=> RetrievedTitle?.Invoke(this, title);
|
||||||
protected void OnRetrievedAuthors(string authors)
|
protected void OnRetrievedAuthors(string? authors)
|
||||||
=> RetrievedAuthors?.Invoke(this, authors);
|
=> RetrievedAuthors?.Invoke(this, authors);
|
||||||
protected void OnRetrievedNarrators(string narrators)
|
protected void OnRetrievedNarrators(string? narrators)
|
||||||
=> RetrievedNarrators?.Invoke(this, narrators);
|
=> RetrievedNarrators?.Invoke(this, narrators);
|
||||||
protected void OnRetrievedCoverArt(byte[] coverArt)
|
protected void OnRetrievedCoverArt(byte[]? coverArt)
|
||||||
=> RetrievedCoverArt?.Invoke(this, coverArt);
|
=> RetrievedCoverArt?.Invoke(this, coverArt);
|
||||||
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
|
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
|
||||||
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
|
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
|
||||||
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
|
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
|
||||||
=> DecryptTimeRemaining?.Invoke(this, timeRemaining);
|
=> DecryptTimeRemaining?.Invoke(this, timeRemaining);
|
||||||
protected void OnFileCreated(string path)
|
public void OnTempFileCreated(TempFile path)
|
||||||
=> FileCreated?.Invoke(this, path);
|
=> TempFileCreated?.Invoke(this, path);
|
||||||
|
|
||||||
protected virtual void FinalizeDownload()
|
protected virtual void FinalizeDownload()
|
||||||
{
|
{
|
||||||
nfsPersister?.Dispose();
|
NfsPersister.Dispose();
|
||||||
downloadFinished = true;
|
downloadFinished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task<bool> Step_DownloadClipsBookmarksAsync()
|
|
||||||
{
|
|
||||||
if (!IsCanceled && DownloadOptions.DownloadClipsBookmarks)
|
|
||||||
{
|
|
||||||
var recordsFile = await DownloadOptions.SaveClipsAndBookmarksAsync(OutputFileName);
|
|
||||||
|
|
||||||
if (File.Exists(recordsFile))
|
|
||||||
OnFileCreated(recordsFile);
|
|
||||||
}
|
|
||||||
return !IsCanceled;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task<bool> Step_CreateCueAsync()
|
protected async Task<bool> Step_CreateCueAsync()
|
||||||
{
|
{
|
||||||
if (!DownloadOptions.CreateCueSheet) return !IsCanceled;
|
if (!DownloadOptions.CreateCueSheet) return !IsCanceled;
|
||||||
|
|
||||||
|
if (DownloadOptions.ChapterInfo.Count <= 1)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information($"Skipped creating .cue because book has no chapters.");
|
||||||
|
return !IsCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
// not a critical step. its failure should not prevent future steps from running
|
// not a critical step. its failure should not prevent future steps from running
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var path = Path.ChangeExtension(OutputFileName, ".cue");
|
var tempFile = GetNewTempFilePath(".cue");
|
||||||
await File.WriteAllTextAsync(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo));
|
await File.WriteAllTextAsync(tempFile.FilePath, Cue.CreateContents(Path.GetFileName(tempFile.FilePath), DownloadOptions.ChapterInfo));
|
||||||
OnFileCreated(path);
|
OnTempFileCreated(tempFile);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -181,58 +175,9 @@ namespace AaxDecrypter
|
|||||||
return !IsCanceled;
|
return !IsCanceled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> CleanupAsync()
|
|
||||||
{
|
|
||||||
if (IsCanceled) return false;
|
|
||||||
|
|
||||||
FileUtility.SaferDelete(jsonDownloadState);
|
|
||||||
|
|
||||||
if (DownloadOptions.DecryptionKeys != null &&
|
|
||||||
DownloadOptions.RetainEncryptedFile &&
|
|
||||||
DownloadOptions.InputType is AAXClean.FileType fileType)
|
|
||||||
{
|
|
||||||
//Write aax decryption key
|
|
||||||
string keyPath = Path.ChangeExtension(tempFilePath, ".key");
|
|
||||||
FileUtility.SaferDelete(keyPath);
|
|
||||||
string aaxPath;
|
|
||||||
|
|
||||||
if (fileType is AAXClean.FileType.Aax)
|
|
||||||
{
|
|
||||||
await File.WriteAllTextAsync(keyPath, $"ActivationBytes={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart1)}");
|
|
||||||
aaxPath = Path.ChangeExtension(tempFilePath, ".aax");
|
|
||||||
}
|
|
||||||
else if (fileType is AAXClean.FileType.Aaxc)
|
|
||||||
{
|
|
||||||
await File.WriteAllTextAsync(keyPath,
|
|
||||||
$"Key={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart1)}{Environment.NewLine}" +
|
|
||||||
$"IV={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart2)}");
|
|
||||||
aaxPath = Path.ChangeExtension(tempFilePath, ".aaxc");
|
|
||||||
}
|
|
||||||
else if (fileType is AAXClean.FileType.Dash)
|
|
||||||
{
|
|
||||||
await File.WriteAllTextAsync(keyPath,
|
|
||||||
$"KeyId={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart1)}{Environment.NewLine}" +
|
|
||||||
$"Key={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart2)}");
|
|
||||||
aaxPath = Path.ChangeExtension(tempFilePath, ".dash");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw new InvalidOperationException($"Unknown file type: {fileType}");
|
|
||||||
|
|
||||||
if (tempFilePath != aaxPath)
|
|
||||||
FileUtility.SaferMove(tempFilePath, aaxPath);
|
|
||||||
|
|
||||||
OnFileCreated(aaxPath);
|
|
||||||
OnFileCreated(keyPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
FileUtility.SaferDelete(tempFilePath);
|
|
||||||
|
|
||||||
return !IsCanceled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private NetworkFileStreamPersister OpenNetworkFileStream()
|
private NetworkFileStreamPersister OpenNetworkFileStream()
|
||||||
{
|
{
|
||||||
NetworkFileStreamPersister nfsp = default;
|
NetworkFileStreamPersister? nfsp = default;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(jsonDownloadState))
|
if (!File.Exists(jsonDownloadState))
|
||||||
@ -253,8 +198,14 @@ namespace AaxDecrypter
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
nfsp.NetworkFileStream.RequestHeaders["User-Agent"] = DownloadOptions.UserAgent;
|
//nfsp will only be null when an unhandled exception occurs. Let the caller handle it.
|
||||||
nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
|
if (nfsp is not null)
|
||||||
|
{
|
||||||
|
nfsp.NetworkFileStream.RequestHeaders["User-Agent"] = DownloadOptions.UserAgent;
|
||||||
|
nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
|
||||||
|
OnTempFileCreated(new(tempFilePath, DownloadOptions.InputType.ToString()));
|
||||||
|
OnTempFileCreated(new(jsonDownloadState));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkFileStreamPersister newNetworkFilePersister()
|
NetworkFileStreamPersister newNetworkFilePersister()
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using AAXClean;
|
using AAXClean;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
@ -33,11 +32,8 @@ namespace AaxDecrypter
|
|||||||
KeyData[]? DecryptionKeys { get; }
|
KeyData[]? DecryptionKeys { get; }
|
||||||
TimeSpan RuntimeLength { get; }
|
TimeSpan RuntimeLength { get; }
|
||||||
OutputFormat OutputFormat { get; }
|
OutputFormat OutputFormat { get; }
|
||||||
bool TrimOutputToChapterLength { get; }
|
|
||||||
bool RetainEncryptedFile { get; }
|
|
||||||
bool StripUnabridged { get; }
|
bool StripUnabridged { get; }
|
||||||
bool CreateCueSheet { get; }
|
bool CreateCueSheet { get; }
|
||||||
bool DownloadClipsBookmarks { get; }
|
|
||||||
long DownloadSpeedBps { get; }
|
long DownloadSpeedBps { get; }
|
||||||
ChapterInfo ChapterInfo { get; }
|
ChapterInfo ChapterInfo { get; }
|
||||||
bool FixupFile { get; }
|
bool FixupFile { get; }
|
||||||
@ -52,9 +48,7 @@ namespace AaxDecrypter
|
|||||||
bool Downsample { get; }
|
bool Downsample { get; }
|
||||||
bool MatchSourceBitrate { get; }
|
bool MatchSourceBitrate { get; }
|
||||||
bool MoveMoovToBeginning { get; }
|
bool MoveMoovToBeginning { get; }
|
||||||
string GetMultipartFileName(MultiConvertFileProperties props);
|
|
||||||
string GetMultipartTitle(MultiConvertFileProperties props);
|
string GetMultipartTitle(MultiConvertFileProperties props);
|
||||||
Task<string> SaveClipsAndBookmarksAsync(string fileName);
|
|
||||||
public FileType? InputType { get; }
|
public FileType? InputType { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,6 +100,12 @@ namespace AaxDecrypter
|
|||||||
Position = WritePosition
|
Position = WritePosition
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (_writeFile.Length < WritePosition)
|
||||||
|
{
|
||||||
|
_writeFile.Dispose();
|
||||||
|
throw new InvalidDataException($"{SaveFilePath} file length is shorter than {WritePosition}");
|
||||||
|
}
|
||||||
|
|
||||||
_readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
_readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
|
||||||
SetUriForSameFile(uri);
|
SetUriForSameFile(uri);
|
||||||
|
|||||||
17
Source/AaxDecrypter/TempFile.cs
Normal file
17
Source/AaxDecrypter/TempFile.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using FileManager;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
namespace AaxDecrypter;
|
||||||
|
|
||||||
|
public record TempFile
|
||||||
|
{
|
||||||
|
public LongPath FilePath { get; init; }
|
||||||
|
public string Extension { get; }
|
||||||
|
public MultiConvertFileProperties? PartProperties { get; init; }
|
||||||
|
public TempFile(LongPath filePath, string? extension = null)
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
extension ??= System.IO.Path.GetExtension(filePath);
|
||||||
|
Extension = FileUtility.GetStandardizedExtension(extension).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using FileManager;
|
using FileManager;
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
@ -8,13 +7,12 @@ namespace AaxDecrypter
|
|||||||
{
|
{
|
||||||
protected override long InputFilePosition => InputFileStream.WritePosition;
|
protected override long InputFilePosition => InputFileStream.WritePosition;
|
||||||
|
|
||||||
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, IDownloadOptions dlLic)
|
public UnencryptedAudiobookDownloader(string outDirectory, string cacheDirectory, IDownloadOptions dlLic)
|
||||||
: base(outFileName, cacheDirectory, dlLic)
|
: base(outDirectory, cacheDirectory, dlLic)
|
||||||
{
|
{
|
||||||
AsyncSteps.Name = "Download Unencrypted Audiobook";
|
AsyncSteps.Name = "Download Unencrypted Audiobook";
|
||||||
AsyncSteps["Step 1: Download Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
|
AsyncSteps["Step 1: Download Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
|
||||||
AsyncSteps["Step 2: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
|
AsyncSteps["Step 2: Create Cue"] = Step_CreateCueAsync;
|
||||||
AsyncSteps["Step 3: Create Cue"] = Step_CreateCueAsync;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||||
@ -26,8 +24,9 @@ namespace AaxDecrypter
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
FinalizeDownload();
|
FinalizeDownload();
|
||||||
FileUtility.SaferMove(InputFileStream.SaveFilePath, OutputFileName);
|
var tempFile = GetNewTempFilePath(DownloadOptions.OutputFormat.ToString());
|
||||||
OnFileCreated(OutputFileName);
|
FileUtility.SaferMove(InputFileStream.SaveFilePath, tempFile.FilePath);
|
||||||
|
OnTempFileCreated(tempFile);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -521,8 +521,8 @@ namespace ApplicationServices
|
|||||||
udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating);
|
udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating);
|
||||||
});
|
});
|
||||||
|
|
||||||
public static int UpdateBookStatus(this LibraryBook lb, LiberatedStatus bookStatus, Version libationVersion)
|
public static int UpdateBookStatus(this LibraryBook lb, LiberatedStatus bookStatus, Version? libationVersion, AudioFormat audioFormat, string audioVersion)
|
||||||
=> lb.UpdateUserDefinedItem(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion); });
|
=> lb.UpdateUserDefinedItem(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion, audioFormat, audioVersion); });
|
||||||
|
|
||||||
public static int UpdateBookStatus(this LibraryBook libraryBook, LiberatedStatus bookStatus)
|
public static int UpdateBookStatus(this LibraryBook libraryBook, LiberatedStatus bookStatus)
|
||||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
=> libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||||
|
|||||||
@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using CsvHelper;
|
using CsvHelper;
|
||||||
using CsvHelper.Configuration.Attributes;
|
using CsvHelper.Configuration.Attributes;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using NPOI.XSSF.UserModel;
|
using NPOI.XSSF.UserModel;
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace ApplicationServices
|
namespace ApplicationServices
|
||||||
{
|
{
|
||||||
@ -115,7 +115,29 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
[Name("IsFinished")]
|
[Name("IsFinished")]
|
||||||
public bool IsFinished { get; set; }
|
public bool IsFinished { get; set; }
|
||||||
}
|
|
||||||
|
[Name("IsSpatial")]
|
||||||
|
public bool IsSpatial { get; set; }
|
||||||
|
|
||||||
|
[Name("Last Downloaded File Version")]
|
||||||
|
public string LastDownloadedFileVersion { get; set; }
|
||||||
|
|
||||||
|
[Ignore /* csv ignore */]
|
||||||
|
public AudioFormat LastDownloadedFormat { get; set; }
|
||||||
|
|
||||||
|
[Name("Last Downloaded Codec"), JsonIgnore]
|
||||||
|
public string CodecString => LastDownloadedFormat?.CodecString ?? "";
|
||||||
|
|
||||||
|
[Name("Last Downloaded Sample rate"), JsonIgnore]
|
||||||
|
public int? SampleRate => LastDownloadedFormat?.SampleRate;
|
||||||
|
|
||||||
|
[Name("Last Downloaded Audio Channels"), JsonIgnore]
|
||||||
|
public int? ChannelCount => LastDownloadedFormat?.ChannelCount;
|
||||||
|
|
||||||
|
[Name("Last Downloaded Bitrate"), JsonIgnore]
|
||||||
|
public int? BitRate => LastDownloadedFormat?.BitRate;
|
||||||
|
}
|
||||||
|
|
||||||
public static class LibToDtos
|
public static class LibToDtos
|
||||||
{
|
{
|
||||||
public static List<ExportDto> ToDtos(this IEnumerable<LibraryBook> library)
|
public static List<ExportDto> ToDtos(this IEnumerable<LibraryBook> library)
|
||||||
@ -135,16 +157,16 @@ namespace ApplicationServices
|
|||||||
HasPdf = a.Book.HasPdf(),
|
HasPdf = a.Book.HasPdf(),
|
||||||
SeriesNames = a.Book.SeriesNames(),
|
SeriesNames = a.Book.SeriesNames(),
|
||||||
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Order} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
|
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Order} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
|
||||||
CommunityRatingOverall = a.Book.Rating?.OverallRating,
|
CommunityRatingOverall = a.Book.Rating?.OverallRating.ZeroIsNull(),
|
||||||
CommunityRatingPerformance = a.Book.Rating?.PerformanceRating,
|
CommunityRatingPerformance = a.Book.Rating?.PerformanceRating.ZeroIsNull(),
|
||||||
CommunityRatingStory = a.Book.Rating?.StoryRating,
|
CommunityRatingStory = a.Book.Rating?.StoryRating.ZeroIsNull(),
|
||||||
PictureId = a.Book.PictureId,
|
PictureId = a.Book.PictureId,
|
||||||
IsAbridged = a.Book.IsAbridged,
|
IsAbridged = a.Book.IsAbridged,
|
||||||
DatePublished = a.Book.DatePublished,
|
DatePublished = a.Book.DatePublished,
|
||||||
CategoriesNames = string.Join("; ", a.Book.LowestCategoryNames()),
|
CategoriesNames = string.Join("; ", a.Book.LowestCategoryNames()),
|
||||||
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
|
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating.ZeroIsNull(),
|
||||||
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
|
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating.ZeroIsNull(),
|
||||||
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
|
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating.ZeroIsNull(),
|
||||||
MyLibationTags = a.Book.UserDefinedItem.Tags,
|
MyLibationTags = a.Book.UserDefinedItem.Tags,
|
||||||
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
|
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
|
||||||
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
|
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
|
||||||
@ -152,8 +174,13 @@ namespace ApplicationServices
|
|||||||
Language = a.Book.Language,
|
Language = a.Book.Language,
|
||||||
LastDownloaded = a.Book.UserDefinedItem.LastDownloaded,
|
LastDownloaded = a.Book.UserDefinedItem.LastDownloaded,
|
||||||
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
|
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
|
||||||
IsFinished = a.Book.UserDefinedItem.IsFinished
|
IsFinished = a.Book.UserDefinedItem.IsFinished,
|
||||||
}).ToList();
|
IsSpatial = a.Book.IsSpatial,
|
||||||
|
LastDownloadedFileVersion = a.Book.UserDefinedItem.LastDownloadedFileVersion ?? "",
|
||||||
|
LastDownloadedFormat = a.Book.UserDefinedItem.LastDownloadedFormat
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
private static float? ZeroIsNull(this float value) => value is 0 ? null : value;
|
||||||
}
|
}
|
||||||
public static class LibraryExporter
|
public static class LibraryExporter
|
||||||
{
|
{
|
||||||
@ -162,7 +189,6 @@ namespace ApplicationServices
|
|||||||
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
|
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
|
||||||
if (!dtos.Any())
|
if (!dtos.Any())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
using var writer = new System.IO.StreamWriter(saveFilePath);
|
using var writer = new System.IO.StreamWriter(saveFilePath);
|
||||||
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
|
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
|
||||||
|
|
||||||
@ -174,7 +200,7 @@ namespace ApplicationServices
|
|||||||
public static void ToJson(string saveFilePath)
|
public static void ToJson(string saveFilePath)
|
||||||
{
|
{
|
||||||
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
|
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
|
||||||
var json = Newtonsoft.Json.JsonConvert.SerializeObject(dtos, Newtonsoft.Json.Formatting.Indented);
|
var json = JsonConvert.SerializeObject(dtos, Formatting.Indented);
|
||||||
System.IO.File.WriteAllText(saveFilePath, json);
|
System.IO.File.WriteAllText(saveFilePath, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +253,13 @@ namespace ApplicationServices
|
|||||||
nameof(ExportDto.Language),
|
nameof(ExportDto.Language),
|
||||||
nameof(ExportDto.LastDownloaded),
|
nameof(ExportDto.LastDownloaded),
|
||||||
nameof(ExportDto.LastDownloadedVersion),
|
nameof(ExportDto.LastDownloadedVersion),
|
||||||
nameof(ExportDto.IsFinished)
|
nameof(ExportDto.IsFinished),
|
||||||
|
nameof(ExportDto.IsSpatial),
|
||||||
|
nameof(ExportDto.LastDownloadedFileVersion),
|
||||||
|
nameof(ExportDto.CodecString),
|
||||||
|
nameof(ExportDto.SampleRate),
|
||||||
|
nameof(ExportDto.ChannelCount),
|
||||||
|
nameof(ExportDto.BitRate)
|
||||||
};
|
};
|
||||||
var col = 0;
|
var col = 0;
|
||||||
foreach (var c in columns)
|
foreach (var c in columns)
|
||||||
@ -248,15 +280,10 @@ namespace ApplicationServices
|
|||||||
foreach (var dto in dtos)
|
foreach (var dto in dtos)
|
||||||
{
|
{
|
||||||
col = 0;
|
col = 0;
|
||||||
|
row = sheet.CreateRow(rowIndex++);
|
||||||
row = sheet.CreateRow(rowIndex);
|
|
||||||
|
|
||||||
row.CreateCell(col++).SetCellValue(dto.Account);
|
row.CreateCell(col++).SetCellValue(dto.Account);
|
||||||
|
row.CreateCell(col++).SetCellValue(dto.DateAdded).CellStyle = dateStyle;
|
||||||
var dateCell = row.CreateCell(col++);
|
|
||||||
dateCell.CellStyle = dateStyle;
|
|
||||||
dateCell.SetCellValue(dto.DateAdded);
|
|
||||||
|
|
||||||
row.CreateCell(col++).SetCellValue(dto.AudibleProductId);
|
row.CreateCell(col++).SetCellValue(dto.AudibleProductId);
|
||||||
row.CreateCell(col++).SetCellValue(dto.Locale);
|
row.CreateCell(col++).SetCellValue(dto.Locale);
|
||||||
row.CreateCell(col++).SetCellValue(dto.Title);
|
row.CreateCell(col++).SetCellValue(dto.Title);
|
||||||
@ -269,56 +296,46 @@ namespace ApplicationServices
|
|||||||
row.CreateCell(col++).SetCellValue(dto.HasPdf);
|
row.CreateCell(col++).SetCellValue(dto.HasPdf);
|
||||||
row.CreateCell(col++).SetCellValue(dto.SeriesNames);
|
row.CreateCell(col++).SetCellValue(dto.SeriesNames);
|
||||||
row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
|
row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
|
||||||
|
row.CreateCell(col++).SetCellValue(dto.CommunityRatingOverall);
|
||||||
col = createCell(row, col, dto.CommunityRatingOverall);
|
row.CreateCell(col++).SetCellValue(dto.CommunityRatingPerformance);
|
||||||
col = createCell(row, col, dto.CommunityRatingPerformance);
|
row.CreateCell(col++).SetCellValue(dto.CommunityRatingStory);
|
||||||
col = createCell(row, col, dto.CommunityRatingStory);
|
|
||||||
|
|
||||||
row.CreateCell(col++).SetCellValue(dto.PictureId);
|
row.CreateCell(col++).SetCellValue(dto.PictureId);
|
||||||
row.CreateCell(col++).SetCellValue(dto.IsAbridged);
|
row.CreateCell(col++).SetCellValue(dto.IsAbridged);
|
||||||
|
row.CreateCell(col++).SetCellValue(dto.DatePublished).CellStyle = dateStyle;
|
||||||
var datePubCell = row.CreateCell(col++);
|
|
||||||
datePubCell.CellStyle = dateStyle;
|
|
||||||
if (dto.DatePublished.HasValue)
|
|
||||||
datePubCell.SetCellValue(dto.DatePublished.Value);
|
|
||||||
else
|
|
||||||
datePubCell.SetCellValue("");
|
|
||||||
|
|
||||||
row.CreateCell(col++).SetCellValue(dto.CategoriesNames);
|
row.CreateCell(col++).SetCellValue(dto.CategoriesNames);
|
||||||
|
row.CreateCell(col++).SetCellValue(dto.MyRatingOverall);
|
||||||
col = createCell(row, col, dto.MyRatingOverall);
|
row.CreateCell(col++).SetCellValue(dto.MyRatingPerformance);
|
||||||
col = createCell(row, col, dto.MyRatingPerformance);
|
row.CreateCell(col++).SetCellValue(dto.MyRatingStory);
|
||||||
col = createCell(row, col, dto.MyRatingStory);
|
|
||||||
|
|
||||||
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
|
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
|
||||||
row.CreateCell(col++).SetCellValue(dto.BookStatus);
|
row.CreateCell(col++).SetCellValue(dto.BookStatus);
|
||||||
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
|
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
|
||||||
row.CreateCell(col++).SetCellValue(dto.ContentType);
|
row.CreateCell(col++).SetCellValue(dto.ContentType);
|
||||||
row.CreateCell(col++).SetCellValue(dto.Language);
|
row.CreateCell(col++).SetCellValue(dto.Language);
|
||||||
|
row.CreateCell(col++).SetCellValue(dto.LastDownloaded).CellStyle = dateStyle;
|
||||||
if (dto.LastDownloaded.HasValue)
|
row.CreateCell(col++).SetCellValue(dto.LastDownloadedVersion);
|
||||||
{
|
row.CreateCell(col++).SetCellValue(dto.IsFinished);
|
||||||
dateCell = row.CreateCell(col);
|
row.CreateCell(col++).SetCellValue(dto.IsSpatial);
|
||||||
dateCell.CellStyle = dateStyle;
|
row.CreateCell(col++).SetCellValue(dto.LastDownloadedFileVersion);
|
||||||
dateCell.SetCellValue(dto.LastDownloaded.Value);
|
row.CreateCell(col++).SetCellValue(dto.CodecString);
|
||||||
}
|
row.CreateCell(col++).SetCellValue(dto.SampleRate);
|
||||||
|
row.CreateCell(col++).SetCellValue(dto.ChannelCount);
|
||||||
row.CreateCell(++col).SetCellValue(dto.LastDownloadedVersion);
|
row.CreateCell(col++).SetCellValue(dto.BitRate);
|
||||||
row.CreateCell(++col).SetCellValue(dto.IsFinished);
|
|
||||||
|
|
||||||
rowIndex++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
|
using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
|
||||||
workbook.Write(fileData);
|
workbook.Write(fileData);
|
||||||
}
|
}
|
||||||
private static int createCell(NPOI.SS.UserModel.IRow row, int col, float? nullableFloat)
|
|
||||||
{
|
private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, DateTime? nullableDate)
|
||||||
if (nullableFloat.HasValue)
|
=> nullableDate.HasValue ? cell.SetCellValue(nullableDate.Value)
|
||||||
row.CreateCell(col++).SetCellValue(nullableFloat.Value);
|
: cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric);
|
||||||
else
|
|
||||||
row.CreateCell(col++).SetCellValue("");
|
private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, int? nullableInt)
|
||||||
return col;
|
=> nullableInt.HasValue ? cell.SetCellValue(nullableInt.Value)
|
||||||
}
|
: cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric);
|
||||||
|
|
||||||
|
private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, float? nullableFloat)
|
||||||
|
=> nullableFloat.HasValue ? cell.SetCellValue(nullableFloat.Value)
|
||||||
|
: cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AudibleApi" Version="9.4.1.1" />
|
<PackageReference Include="AudibleApi" Version="9.4.2.1" />
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.31.1" />
|
<PackageReference Include="Google.Protobuf" Version="3.31.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
70
Source/DataLayer/AudioFormat.cs
Normal file
70
Source/DataLayer/AudioFormat.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace DataLayer;
|
||||||
|
|
||||||
|
public enum Codec : byte
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Mp3,
|
||||||
|
AAC_LC,
|
||||||
|
xHE_AAC,
|
||||||
|
EC_3,
|
||||||
|
AC_4
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AudioFormat
|
||||||
|
{
|
||||||
|
public static AudioFormat Default => new(Codec.Unknown, 0, 0, 0);
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool IsDefault => Codec is Codec.Unknown && BitRate == 0 && SampleRate == 0 && ChannelCount == 0;
|
||||||
|
[JsonIgnore]
|
||||||
|
public Codec Codec { get; set; }
|
||||||
|
public int SampleRate { get; set; }
|
||||||
|
public int ChannelCount { get; set; }
|
||||||
|
public int BitRate { get; set; }
|
||||||
|
|
||||||
|
public AudioFormat(Codec codec, int bitRate, int sampleRate, int channelCount)
|
||||||
|
{
|
||||||
|
Codec = codec;
|
||||||
|
BitRate = bitRate;
|
||||||
|
SampleRate = sampleRate;
|
||||||
|
ChannelCount = channelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CodecString => Codec switch
|
||||||
|
{
|
||||||
|
Codec.Mp3 => "mp3",
|
||||||
|
Codec.AAC_LC => "AAC-LC",
|
||||||
|
Codec.xHE_AAC => "xHE-AAC",
|
||||||
|
Codec.EC_3 => "EC-3",
|
||||||
|
Codec.AC_4 => "AC-4",
|
||||||
|
Codec.Unknown or _ => "[Unknown]",
|
||||||
|
};
|
||||||
|
|
||||||
|
//Property | Start | Num | Max | Current Max |
|
||||||
|
// | Bit | Bits | Value | Value Used |
|
||||||
|
//-----------------------------------------------------
|
||||||
|
//Codec | 35 | 4 | 15 | 5 |
|
||||||
|
//BitRate | 23 | 12 | 4_095 | 768 |
|
||||||
|
//SampleRate | 5 | 18 | 262_143 | 48_000 |
|
||||||
|
//ChannelCount | 0 | 5 | 31 | 6 |
|
||||||
|
public long Serialize() =>
|
||||||
|
((long)Codec << 35) |
|
||||||
|
((long)BitRate << 23) |
|
||||||
|
((long)SampleRate << 5) |
|
||||||
|
(long)ChannelCount;
|
||||||
|
|
||||||
|
public static AudioFormat Deserialize(long value)
|
||||||
|
{
|
||||||
|
var codec = (Codec)((value >> 35) & 15);
|
||||||
|
var bitRate = (int)((value >> 23) & 4_095);
|
||||||
|
var sampleRate = (int)((value >> 5) & 262_143);
|
||||||
|
var channelCount = (int)(value & 31);
|
||||||
|
return new AudioFormat(codec, bitRate, sampleRate, channelCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> IsDefault ? "[Unknown Audio Format]"
|
||||||
|
: $"{CodecString} ({ChannelCount}ch | {SampleRate:N0}Hz | {BitRate}kbps)";
|
||||||
|
}
|
||||||
@ -13,7 +13,6 @@ namespace DataLayer.Configurations
|
|||||||
|
|
||||||
entity.OwnsOne(b => b.Rating);
|
entity.OwnsOne(b => b.Rating);
|
||||||
|
|
||||||
entity.Property(nameof(Book._audioFormat));
|
|
||||||
//
|
//
|
||||||
// CRUCIAL: ignore unmapped collections, even get-only
|
// CRUCIAL: ignore unmapped collections, even get-only
|
||||||
//
|
//
|
||||||
@ -50,6 +49,11 @@ namespace DataLayer.Configurations
|
|||||||
b_udi
|
b_udi
|
||||||
.Property(udi => udi.LastDownloadedVersion)
|
.Property(udi => udi.LastDownloadedVersion)
|
||||||
.HasConversion(ver => ver.ToString(), str => Version.Parse(str));
|
.HasConversion(ver => ver.ToString(), str => Version.Parse(str));
|
||||||
|
b_udi
|
||||||
|
.Property(udi => udi.LastDownloadedFormat)
|
||||||
|
.HasConversion(af => af.Serialize(), str => AudioFormat.Deserialize(str));
|
||||||
|
|
||||||
|
b_udi.Property(udi => udi.LastDownloadedFileVersion);
|
||||||
|
|
||||||
// owns it 1:1, store in same table
|
// owns it 1:1, store in same table
|
||||||
b_udi.OwnsOne(udi => udi.Rating);
|
b_udi.OwnsOne(udi => udi.Rating);
|
||||||
|
|||||||
@ -43,18 +43,13 @@ namespace DataLayer
|
|||||||
public ContentType ContentType { get; private set; }
|
public ContentType ContentType { get; private set; }
|
||||||
public string Locale { get; private set; }
|
public string Locale { get; private set; }
|
||||||
|
|
||||||
//This field is now unused, however, there is little sense in adding a
|
|
||||||
//database migration to remove an unused field. Leave it for compatibility.
|
|
||||||
#pragma warning disable CS0649 // Field 'Book._audioFormat' is never assigned to, and will always have its default value 0
|
|
||||||
internal long _audioFormat;
|
|
||||||
#pragma warning restore CS0649
|
|
||||||
|
|
||||||
// mutable
|
// mutable
|
||||||
public string PictureId { get; set; }
|
public string PictureId { get; set; }
|
||||||
public string PictureLarge { get; set; }
|
public string PictureLarge { get; set; }
|
||||||
|
|
||||||
// book details
|
// book details
|
||||||
public bool IsAbridged { get; private set; }
|
public bool IsAbridged { get; private set; }
|
||||||
|
public bool IsSpatial { get; private set; }
|
||||||
public DateTime? DatePublished { get; private set; }
|
public DateTime? DatePublished { get; private set; }
|
||||||
public string Language { get; private set; }
|
public string Language { get; private set; }
|
||||||
|
|
||||||
@ -242,10 +237,11 @@ namespace DataLayer
|
|||||||
public void UpdateProductRating(float overallRating, float performanceRating, float storyRating)
|
public void UpdateProductRating(float overallRating, float performanceRating, float storyRating)
|
||||||
=> Rating.Update(overallRating, performanceRating, storyRating);
|
=> Rating.Update(overallRating, performanceRating, storyRating);
|
||||||
|
|
||||||
public void UpdateBookDetails(bool isAbridged, DateTime? datePublished, string language)
|
public void UpdateBookDetails(bool isAbridged, bool? isSpatial, DateTime? datePublished, string language)
|
||||||
{
|
{
|
||||||
// don't overwrite with default values
|
// don't overwrite with default values
|
||||||
IsAbridged |= isAbridged;
|
IsAbridged |= isAbridged;
|
||||||
|
IsSpatial |= isSpatial ?? false;
|
||||||
DatePublished = datePublished ?? DatePublished;
|
DatePublished = datePublished ?? DatePublished;
|
||||||
Language = language?.FirstCharToUpper() ?? Language;
|
Language = language?.FirstCharToUpper() ?? Language;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,24 +24,52 @@ namespace DataLayer
|
|||||||
{
|
{
|
||||||
internal int BookId { get; private set; }
|
internal int BookId { get; private set; }
|
||||||
public Book Book { get; private set; }
|
public Book Book { get; private set; }
|
||||||
public DateTime? LastDownloaded { get; private set; }
|
/// <summary>
|
||||||
public Version LastDownloadedVersion { get; private set; }
|
/// Date the audio file was last downloaded.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastDownloaded { get; private set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Version of Libation used the last time the audio file was downloaded.
|
||||||
|
/// </summary>
|
||||||
|
public Version LastDownloadedVersion { get; private set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Audio format of the last downloaded audio file.
|
||||||
|
/// </summary>
|
||||||
|
public AudioFormat LastDownloadedFormat { get; private set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Version of the audio file that was last downloaded.
|
||||||
|
/// </summary>
|
||||||
|
public string LastDownloadedFileVersion { get; private set; }
|
||||||
|
|
||||||
public void SetLastDownloaded(Version version)
|
public void SetLastDownloaded(Version libationVersion, AudioFormat audioFormat, string audioVersion)
|
||||||
{
|
{
|
||||||
if (LastDownloadedVersion != version)
|
if (LastDownloadedVersion != libationVersion)
|
||||||
{
|
{
|
||||||
LastDownloadedVersion = version;
|
LastDownloadedVersion = libationVersion;
|
||||||
OnItemChanged(nameof(LastDownloadedVersion));
|
OnItemChanged(nameof(LastDownloadedVersion));
|
||||||
}
|
}
|
||||||
|
if (LastDownloadedFormat != audioFormat)
|
||||||
|
{
|
||||||
|
LastDownloadedFormat = audioFormat;
|
||||||
|
OnItemChanged(nameof(LastDownloadedFormat));
|
||||||
|
}
|
||||||
|
if (LastDownloadedFileVersion != audioVersion)
|
||||||
|
{
|
||||||
|
LastDownloadedFileVersion = audioVersion;
|
||||||
|
OnItemChanged(nameof(LastDownloadedFileVersion));
|
||||||
|
}
|
||||||
|
|
||||||
if (version is null)
|
if (libationVersion is null)
|
||||||
|
{
|
||||||
LastDownloaded = null;
|
LastDownloaded = null;
|
||||||
|
LastDownloadedFormat = null;
|
||||||
|
LastDownloadedFileVersion = null;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LastDownloaded = DateTime.Now;
|
LastDownloaded = DateTime.Now;
|
||||||
OnItemChanged(nameof(LastDownloaded));
|
OnItemChanged(nameof(LastDownloaded));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserDefinedItem() { }
|
private UserDefinedItem() { }
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using Dinah.EntityFrameworkCore;
|
using Dinah.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||||
|
|
||||||
namespace DataLayer
|
namespace DataLayer
|
||||||
{
|
{
|
||||||
@ -7,6 +8,7 @@ namespace DataLayer
|
|||||||
{
|
{
|
||||||
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
|
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
|
||||||
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString)
|
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString)
|
||||||
=> optionsBuilder.UseSqlite(connectionString, ob => ob.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
|
=> optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
|
||||||
|
.UseSqlite(connectionString, ob => ob.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
474
Source/DataLayer/Migrations/20250725074123_AddAudioFormatData.Designer.cs
generated
Normal file
474
Source/DataLayer/Migrations/20250725074123_AddAudioFormatData.Designer.cs
generated
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using DataLayer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DataLayer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(LibationContext))]
|
||||||
|
[Migration("20250725074123_AddAudioFormatData")]
|
||||||
|
partial class AddAudioFormatData
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.7");
|
||||||
|
|
||||||
|
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("_categoriesCategoryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("_categoryLaddersCategoryLadderId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId");
|
||||||
|
|
||||||
|
b.HasIndex("_categoryLaddersCategoryLadderId");
|
||||||
|
|
||||||
|
b.ToTable("CategoryCategoryLadder");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleProductId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ContentType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DatePublished")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAbridged")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSpatial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LengthInMinutes")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Locale")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PictureId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PictureLarge")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Subtitle")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("AudibleProductId");
|
||||||
|
|
||||||
|
b.ToTable("Books");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.BookCategory", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("CategoryLadderId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("BookId", "CategoryLadderId");
|
||||||
|
|
||||||
|
b.HasIndex("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("CategoryLadderId");
|
||||||
|
|
||||||
|
b.ToTable("BookCategory");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ContributorId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte>("Order")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("BookId", "ContributorId", "Role");
|
||||||
|
|
||||||
|
b.HasIndex("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("ContributorId");
|
||||||
|
|
||||||
|
b.ToTable("BookContributor");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Category", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CategoryId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleCategoryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("CategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("AudibleCategoryId");
|
||||||
|
|
||||||
|
b.ToTable("Categories");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CategoryLadderId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("CategoryLadderId");
|
||||||
|
|
||||||
|
b.ToTable("CategoryLadders");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ContributorId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleContributorId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ContributorId");
|
||||||
|
|
||||||
|
b.HasIndex("Name");
|
||||||
|
|
||||||
|
b.ToTable("Contributors");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
ContributorId = -1,
|
||||||
|
Name = ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("AbsentFromLastScan")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Account")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateAdded")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("BookId");
|
||||||
|
|
||||||
|
b.ToTable("LibraryBooks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Series", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleSeriesId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("SeriesId");
|
||||||
|
|
||||||
|
b.HasIndex("AudibleSeriesId");
|
||||||
|
|
||||||
|
b.ToTable("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Order")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("SeriesId", "BookId");
|
||||||
|
|
||||||
|
b.HasIndex("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("SeriesBook");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Category", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("_categoriesCategoryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("DataLayer.CategoryLadder", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("_categoryLaddersCategoryLadderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<float>("OverallRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b1.Property<float>("PerformanceRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b1.Property<float>("StoryRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b1.HasKey("BookId");
|
||||||
|
|
||||||
|
b1.ToTable("Books");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("BookId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("SupplementId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<string>("Url")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.HasKey("SupplementId");
|
||||||
|
|
||||||
|
b1.HasIndex("BookId");
|
||||||
|
|
||||||
|
b1.ToTable("Supplement");
|
||||||
|
|
||||||
|
b1.WithOwner("Book")
|
||||||
|
.HasForeignKey("BookId");
|
||||||
|
|
||||||
|
b1.Navigation("Book");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<int>("BookStatus")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<bool>("IsFinished")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<DateTime?>("LastDownloaded")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<string>("LastDownloadedFileVersion")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<long?>("LastDownloadedFormat")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<string>("LastDownloadedVersion")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<int?>("PdfStatus")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<string>("Tags")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.HasKey("BookId");
|
||||||
|
|
||||||
|
b1.ToTable("UserDefinedItem", (string)null);
|
||||||
|
|
||||||
|
b1.WithOwner("Book")
|
||||||
|
.HasForeignKey("BookId");
|
||||||
|
|
||||||
|
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||||
|
{
|
||||||
|
b2.Property<int>("UserDefinedItemBookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b2.Property<float>("OverallRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b2.Property<float>("PerformanceRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b2.Property<float>("StoryRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b2.HasKey("UserDefinedItemBookId");
|
||||||
|
|
||||||
|
b2.ToTable("UserDefinedItem");
|
||||||
|
|
||||||
|
b2.WithOwner()
|
||||||
|
.HasForeignKey("UserDefinedItemBookId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b1.Navigation("Book");
|
||||||
|
|
||||||
|
b1.Navigation("Rating");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Rating");
|
||||||
|
|
||||||
|
b.Navigation("Supplements");
|
||||||
|
|
||||||
|
b.Navigation("UserDefinedItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.BookCategory", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithMany("CategoriesLink")
|
||||||
|
.HasForeignKey("BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("DataLayer.CategoryLadder", "CategoryLadder")
|
||||||
|
.WithMany("BooksLink")
|
||||||
|
.HasForeignKey("CategoryLadderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
|
||||||
|
b.Navigation("CategoryLadder");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithMany("ContributorsLink")
|
||||||
|
.HasForeignKey("BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||||
|
.WithMany("BooksLink")
|
||||||
|
.HasForeignKey("ContributorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
|
||||||
|
b.Navigation("Contributor");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithMany("SeriesLink")
|
||||||
|
.HasForeignKey("BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("DataLayer.Series", "Series")
|
||||||
|
.WithMany("BooksLink")
|
||||||
|
.HasForeignKey("SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CategoriesLink");
|
||||||
|
|
||||||
|
b.Navigation("ContributorsLink");
|
||||||
|
|
||||||
|
b.Navigation("SeriesLink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("BooksLink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("BooksLink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("BooksLink");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DataLayer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAudioFormatData : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "_audioFormat",
|
||||||
|
table: "Books",
|
||||||
|
newName: "IsSpatial");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "LastDownloadedFileVersion",
|
||||||
|
table: "UserDefinedItem",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "LastDownloadedFormat",
|
||||||
|
table: "UserDefinedItem",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastDownloadedFileVersion",
|
||||||
|
table: "UserDefinedItem");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastDownloadedFormat",
|
||||||
|
table: "UserDefinedItem");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "IsSpatial",
|
||||||
|
table: "Books",
|
||||||
|
newName: "_audioFormat");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@ namespace DataLayer.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.5");
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.7");
|
||||||
|
|
||||||
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||||
{
|
{
|
||||||
@ -53,6 +53,9 @@ namespace DataLayer.Migrations
|
|||||||
b.Property<bool>("IsAbridged")
|
b.Property<bool>("IsAbridged")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSpatial")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Language")
|
b.Property<string>("Language")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -74,9 +77,6 @@ namespace DataLayer.Migrations
|
|||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<long>("_audioFormat")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("BookId");
|
b.HasKey("BookId");
|
||||||
|
|
||||||
b.HasIndex("AudibleProductId");
|
b.HasIndex("AudibleProductId");
|
||||||
@ -318,6 +318,12 @@ namespace DataLayer.Migrations
|
|||||||
b1.Property<DateTime?>("LastDownloaded")
|
b1.Property<DateTime?>("LastDownloaded")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<string>("LastDownloadedFileVersion")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<long?>("LastDownloadedFormat")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b1.Property<string>("LastDownloadedVersion")
|
b1.Property<string>("LastDownloadedVersion")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
|||||||
@ -137,8 +137,6 @@ namespace DtoImporterService
|
|||||||
book.ReplacePublisher(publisher);
|
book.ReplacePublisher(publisher);
|
||||||
}
|
}
|
||||||
|
|
||||||
book.UpdateBookDetails(item.IsAbridged, item.DatePublished, item.Language);
|
|
||||||
|
|
||||||
if (item.PdfUrl is not null)
|
if (item.PdfUrl is not null)
|
||||||
book.AddSupplementDownloadUrl(item.PdfUrl.ToString());
|
book.AddSupplementDownloadUrl(item.PdfUrl.ToString());
|
||||||
|
|
||||||
@ -166,8 +164,9 @@ namespace DtoImporterService
|
|||||||
|
|
||||||
// 2023-02-01
|
// 2023-02-01
|
||||||
// updateBook must update language on books which were imported before the migration which added language.
|
// updateBook must update language on books which were imported before the migration which added language.
|
||||||
// Can eventually delete this
|
// 2025-07-30
|
||||||
book.UpdateBookDetails(item.IsAbridged, item.DatePublished, item.Language);
|
// updateBook must update isSpatial on books which were imported before the migration which added isSpatial.
|
||||||
|
book.UpdateBookDetails(item.IsAbridged, item.AssetDetails?.Any(a => a.IsSpatial), item.DatePublished, item.Language);
|
||||||
|
|
||||||
book.UpdateProductRating(
|
book.UpdateProductRating(
|
||||||
(float)(item.Rating?.OverallDistribution?.AverageRating ?? 0),
|
(float)(item.Rating?.OverallDistribution?.AverageRating ?? 0),
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using AaxDecrypter;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationFileManager.Templates;
|
using LibationFileManager.Templates;
|
||||||
@ -34,30 +35,17 @@ namespace FileLiberator
|
|||||||
return Templates.Folder.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, "");
|
return Templates.Folder.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DownloadDecryptBook:
|
|
||||||
/// Path: in progress directory.
|
|
||||||
/// File name: final file name.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetInProgressFilename(this AudioFileStorage _, LibraryBookDto libraryBook, string extension)
|
|
||||||
=> Templates.File.GetFilename(libraryBook, AudibleFileStorage.DecryptInProgressDirectory, extension, returnFirstExisting: true);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PDF: audio file does not exist
|
/// PDF: audio file does not exist
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
|
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension, bool returnFirstExisting = false)
|
||||||
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, extension);
|
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.BooksDirectory, extension, returnFirstExisting: returnFirstExisting);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// PDF: audio file does not exist
|
|
||||||
/// </summary>
|
|
||||||
public static string GetBooksDirectoryFilename(this AudioFileStorage _, LibraryBookDto dto, string extension)
|
|
||||||
=> Templates.File.GetFilename(dto, AudibleFileStorage.BooksDirectory, extension);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PDF: audio file already exists
|
/// PDF: audio file already exists
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetCustomDirFilename(this AudioFileStorage _, LibraryBook libraryBook, string dirFullPath, string extension)
|
public static string GetCustomDirFilename(this AudioFileStorage _, LibraryBook libraryBook, string dirFullPath, string extension, MultiConvertFileProperties partProperties = null, bool returnFirstExisting = false)
|
||||||
=> Templates.File.GetFilename(libraryBook.ToDto(), dirFullPath, extension);
|
=> partProperties is null ? Templates.File.GetFilename(libraryBook.ToDto(), dirFullPath, extension, returnFirstExisting: returnFirstExisting)
|
||||||
|
: Templates.ChapterFile.GetFilename(libraryBook.ToDto(), partProperties, dirFullPath, extension, returnFirstExisting: returnFirstExisting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
242
Source/FileLiberator/AudioFormatDecoder.cs
Normal file
242
Source/FileLiberator/AudioFormatDecoder.cs
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
using AAXClean;
|
||||||
|
using DataLayer;
|
||||||
|
using FileManager;
|
||||||
|
using Mpeg4Lib.Boxes;
|
||||||
|
using Mpeg4Lib.Util;
|
||||||
|
using NAudio.Lame.ID3;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
namespace AaxDecrypter;
|
||||||
|
|
||||||
|
/// <summary> Read audio codec, bitrate, sample rate, and channel count from MP4 and MP3 audio files. </summary>
|
||||||
|
internal static class AudioFormatDecoder
|
||||||
|
{
|
||||||
|
public static AudioFormat FromMpeg4(string filename)
|
||||||
|
{
|
||||||
|
using var fileStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
return FromMpeg4(new Mp4File(fileStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AudioFormat FromMpeg4(Mp4File mp4File)
|
||||||
|
{
|
||||||
|
Codec codec;
|
||||||
|
if (mp4File.AudioSampleEntry.Dac4 is not null)
|
||||||
|
{
|
||||||
|
codec = Codec.AC_4;
|
||||||
|
}
|
||||||
|
else if (mp4File.AudioSampleEntry.Dec3 is not null)
|
||||||
|
{
|
||||||
|
codec = Codec.EC_3;
|
||||||
|
}
|
||||||
|
else if (mp4File.AudioSampleEntry.Esds is EsdsBox esds)
|
||||||
|
{
|
||||||
|
var objectType = esds.ES_Descriptor.DecoderConfig.AudioSpecificConfig.AudioObjectType;
|
||||||
|
codec
|
||||||
|
= objectType == 2 ? Codec.AAC_LC
|
||||||
|
: objectType == 42 ? Codec.xHE_AAC
|
||||||
|
: Codec.Unknown;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return AudioFormat.Default;
|
||||||
|
|
||||||
|
var bitrate = (int)Math.Round(mp4File.AverageBitrate / 1024d);
|
||||||
|
|
||||||
|
return new AudioFormat(codec, bitrate, mp4File.TimeScale, mp4File.AudioChannels);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AudioFormat FromMpeg3(LongPath mp3Filename)
|
||||||
|
{
|
||||||
|
using var mp3File = File.Open(mp3Filename, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
if (Id3Header.Create(mp3File) is Id3Header id3header)
|
||||||
|
id3header.SeekForwardToPosition(mp3File, mp3File.Position + id3header.Size);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Debug("File appears not to have ID3 tags.");
|
||||||
|
mp3File.Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SeekToFirstKeyFrame(mp3File))
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Warning("Invalid frame sync read from file at end of ID3 tag.");
|
||||||
|
return AudioFormat.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mpegSize = mp3File.Length - mp3File.Position;
|
||||||
|
if (mpegSize < 64)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Warning("Remaining file length is too short to contain any mp3 frames. {@File}", mp3Filename);
|
||||||
|
return AudioFormat.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region read first mp3 frame header
|
||||||
|
//https://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header#VBRIHeader
|
||||||
|
var reader = new BitReader(mp3File.ReadBlock(4));
|
||||||
|
reader.Position = 11; //Skip frame header magic bits
|
||||||
|
var versionId = (Version)reader.Read(2);
|
||||||
|
var layerDesc = (Layer)reader.Read(2);
|
||||||
|
|
||||||
|
if (layerDesc is not Layer.Layer_3)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Warning("Could not read mp3 data from {@layerVersion} file.", layerDesc.ToString());
|
||||||
|
return AudioFormat.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versionId is Version.Reserved)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Warning("Mp3 data data cannot be read from a file with version = 'Reserved'");
|
||||||
|
return AudioFormat.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var protectionBit = reader.ReadBool();
|
||||||
|
var bitrateIndex = reader.Read(4);
|
||||||
|
var freqIndex = reader.Read(2);
|
||||||
|
_ = reader.ReadBool(); //Padding bit
|
||||||
|
_ = reader.ReadBool(); //Private bit
|
||||||
|
var channelMode = reader.Read(2);
|
||||||
|
_ = reader.Read(2); //Mode extension
|
||||||
|
_ = reader.ReadBool(); //Copyright
|
||||||
|
_ = reader.ReadBool(); //Original
|
||||||
|
_ = reader.Read(2); //Emphasis
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
//Read the sample rate,and channels from the first frame's header.
|
||||||
|
var sampleRate = Mp3SampleRateIndex[versionId][freqIndex];
|
||||||
|
var channelCount = channelMode == 3 ? 1 : 2;
|
||||||
|
|
||||||
|
//Try to read variable bitrate info from the first frame.
|
||||||
|
//Revert to fixed bitrate from frame header if not found.
|
||||||
|
var bitrate
|
||||||
|
= TryReadXingBitrate(out var br) ? br
|
||||||
|
: TryReadVbriBitrate(out br) ? br
|
||||||
|
: Mp3BitrateIndex[versionId][bitrateIndex];
|
||||||
|
|
||||||
|
return new AudioFormat(Codec.Mp3, bitrate, sampleRate, channelCount);
|
||||||
|
|
||||||
|
#region Variable bitrate header readers
|
||||||
|
bool TryReadXingBitrate(out int bitrate)
|
||||||
|
{
|
||||||
|
const int XingHeader = 0x58696e67;
|
||||||
|
const int InfoHeader = 0x496e666f;
|
||||||
|
|
||||||
|
var sideInfoSize = GetSideInfo(channelCount == 2, versionId) + (protectionBit ? 0 : 2);
|
||||||
|
mp3File.Position += sideInfoSize;
|
||||||
|
|
||||||
|
if (mp3File.ReadUInt32BE() is XingHeader or InfoHeader)
|
||||||
|
{
|
||||||
|
//Xing or Info header (common)
|
||||||
|
var flags = mp3File.ReadUInt32BE();
|
||||||
|
bool hasFramesField = (flags & 1) == 1;
|
||||||
|
bool hasBytesField = (flags & 2) == 2;
|
||||||
|
|
||||||
|
if (hasFramesField)
|
||||||
|
{
|
||||||
|
var numFrames = mp3File.ReadUInt32BE();
|
||||||
|
if (hasBytesField)
|
||||||
|
{
|
||||||
|
mpegSize = mp3File.ReadUInt32BE();
|
||||||
|
}
|
||||||
|
|
||||||
|
var samplesPerFrame = GetSamplesPerFrame(sampleRate);
|
||||||
|
var duration = samplesPerFrame * numFrames / sampleRate;
|
||||||
|
bitrate = (short)(mpegSize / duration / 1024 * 8);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
mp3File.Position -= sideInfoSize + 4;
|
||||||
|
|
||||||
|
bitrate = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TryReadVbriBitrate(out int bitrate)
|
||||||
|
{
|
||||||
|
const int VBRIHeader = 0x56425249;
|
||||||
|
|
||||||
|
mp3File.Position += 32;
|
||||||
|
|
||||||
|
if (mp3File.ReadUInt32BE() is VBRIHeader)
|
||||||
|
{
|
||||||
|
//VBRI header (rare)
|
||||||
|
_ = mp3File.ReadBlock(6);
|
||||||
|
mpegSize = mp3File.ReadUInt32BE();
|
||||||
|
var numFrames = mp3File.ReadUInt32BE();
|
||||||
|
|
||||||
|
var samplesPerFrame = GetSamplesPerFrame(sampleRate);
|
||||||
|
var duration = samplesPerFrame * numFrames / sampleRate;
|
||||||
|
bitrate = (short)(mpegSize / duration / 1024 * 8);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bitrate = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
#region MP3 frame decoding helpers
|
||||||
|
private static bool SeekToFirstKeyFrame(Stream file)
|
||||||
|
{
|
||||||
|
//Frame headers begin with first 11 bits set.
|
||||||
|
const int MaxSeekBytes = 4096;
|
||||||
|
var maxPosition = Math.Min(file.Length, file.Position + MaxSeekBytes) - 2;
|
||||||
|
|
||||||
|
while (file.Position < maxPosition)
|
||||||
|
{
|
||||||
|
if (file.ReadByte() == 0xff)
|
||||||
|
{
|
||||||
|
if ((file.ReadByte() & 0xe0) == 0xe0)
|
||||||
|
{
|
||||||
|
file.Position -= 2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
file.Position--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Version
|
||||||
|
{
|
||||||
|
Version_2_5,
|
||||||
|
Reserved,
|
||||||
|
Version_2,
|
||||||
|
Version_1
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Layer
|
||||||
|
{
|
||||||
|
Reserved,
|
||||||
|
Layer_3,
|
||||||
|
Layer_2,
|
||||||
|
Layer_1
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double GetSamplesPerFrame(int sampleRate) => sampleRate >= 32000 ? 1152 : 576;
|
||||||
|
|
||||||
|
private static byte GetSideInfo(bool stereo, Version version) => (stereo, version) switch
|
||||||
|
{
|
||||||
|
(true, Version.Version_1) => 32,
|
||||||
|
(true, Version.Version_2 or Version.Version_2_5) => 17,
|
||||||
|
(false, Version.Version_1) => 17,
|
||||||
|
(false, Version.Version_2 or Version.Version_2_5) => 9,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<Version, ushort[]> Mp3SampleRateIndex = new()
|
||||||
|
{
|
||||||
|
{ Version.Version_2_5, [11025, 12000, 8000] },
|
||||||
|
{ Version.Version_2, [22050, 24000, 16000] },
|
||||||
|
{ Version.Version_1, [44100, 48000, 32000] },
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<Version, short[]> Mp3BitrateIndex = new()
|
||||||
|
{
|
||||||
|
{ Version.Version_2_5, [-1, 8,16,24,32,40,48,56, 64, 80, 96,112,128,144,160,-1]},
|
||||||
|
{ Version.Version_2, [-1, 8,16,24,32,40,48,56, 64, 80, 96,112,128,144,160,-1]},
|
||||||
|
{ Version.Version_1, [-1,32,40,48,56,64,80,96,112,128,160,192,224,256,320,-1]}
|
||||||
|
};
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AAXClean;
|
using AAXClean;
|
||||||
using AAXClean.Codecs;
|
using AAXClean.Codecs;
|
||||||
@ -19,7 +20,13 @@ namespace FileLiberator
|
|||||||
private readonly AaxDecrypter.AverageSpeed averageSpeed = new();
|
private readonly AaxDecrypter.AverageSpeed averageSpeed = new();
|
||||||
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
||||||
|
|
||||||
public override Task CancelAsync() => Mp4Operation?.CancelAsync() ?? Task.CompletedTask;
|
private CancellationTokenSource CancellationTokenSource { get; set; }
|
||||||
|
public override async Task CancelAsync()
|
||||||
|
{
|
||||||
|
await CancellationTokenSource.CancelAsync();
|
||||||
|
if (Mp4Operation is not null)
|
||||||
|
await Mp4Operation.CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public static bool ValidateMp3(LibraryBook libraryBook)
|
public static bool ValidateMp3(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
@ -32,17 +39,29 @@ namespace FileLiberator
|
|||||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
OnBegin(libraryBook);
|
OnBegin(libraryBook);
|
||||||
|
var cancellationToken = (CancellationTokenSource = new()).Token;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var m4bPaths = AudibleFileStorage.Audio.GetPaths(libraryBook.Book.AudibleProductId);
|
var m4bPaths = AudibleFileStorage.Audio.GetPaths(libraryBook.Book.AudibleProductId)
|
||||||
|
.Where(m4bPath => File.Exists(m4bPath))
|
||||||
|
.Select(m4bPath => new { m4bPath, proposedMp3Path = Mp3FileName(m4bPath), m4bSize = new FileInfo(m4bPath).Length })
|
||||||
|
.Where(p => !File.Exists(p.proposedMp3Path))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
foreach (var m4bPath in m4bPaths)
|
long totalInputSize = m4bPaths.Sum(p => p.m4bSize);
|
||||||
|
long sizeOfCompletedFiles = 0L;
|
||||||
|
foreach (var entry in m4bPaths)
|
||||||
{
|
{
|
||||||
var proposedMp3Path = Mp3FileName(m4bPath);
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
if (File.Exists(proposedMp3Path) || !File.Exists(m4bPath)) continue;
|
if (File.Exists(entry.proposedMp3Path) || !File.Exists(entry.m4bPath))
|
||||||
|
{
|
||||||
|
sizeOfCompletedFiles += entry.m4bSize;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var m4bBook = await Task.Run(() => new Mp4File(m4bPath, FileAccess.Read));
|
using var m4bFileStream = File.Open(entry.m4bPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
var m4bBook = new Mp4File(m4bFileStream);
|
||||||
|
|
||||||
//AAXClean.Codecs only supports decoding AAC and E-AC-3 audio.
|
//AAXClean.Codecs only supports decoding AAC and E-AC-3 audio.
|
||||||
if (m4bBook.AudioSampleEntry.Esds is null && m4bBook.AudioSampleEntry.Dec3 is null)
|
if (m4bBook.AudioSampleEntry.Esds is null && m4bBook.AudioSampleEntry.Dec3 is null)
|
||||||
@ -69,74 +88,85 @@ namespace FileLiberator
|
|||||||
lameConfig.ID3.Track = trackCount > 0 ? $"{trackNum}/{trackCount}" : trackNum.ToString();
|
lameConfig.ID3.Track = trackCount > 0 ? $"{trackNum}/{trackCount}" : trackNum.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
using var mp3File = File.Open(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
long currentFileNumBytesProcessed = 0;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig, chapters);
|
var tempPath = Path.GetTempFileName();
|
||||||
Mp4Operation.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
|
using (var mp3File = File.Open(tempPath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
|
||||||
await Mp4Operation;
|
|
||||||
|
|
||||||
if (Mp4Operation.IsCanceled)
|
|
||||||
{
|
{
|
||||||
FileUtility.SaferDelete(mp3File.Name);
|
Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig, chapters);
|
||||||
return new StatusHandler { "Cancelled" };
|
Mp4Operation.ConversionProgressUpdate += m4bBook_ConversionProgressUpdate;
|
||||||
|
await Mp4Operation;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
if (cancellationToken.IsCancellationRequested)
|
||||||
var realMp3Path
|
FileUtility.SaferDelete(tempPath);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var realMp3Path
|
||||||
= FileUtility.SaferMoveToValidPath(
|
= FileUtility.SaferMoveToValidPath(
|
||||||
mp3File.Name,
|
tempPath,
|
||||||
proposedMp3Path,
|
entry.proposedMp3Path,
|
||||||
Configuration.Instance.ReplacementCharacters,
|
Configuration.Instance.ReplacementCharacters,
|
||||||
extension: "mp3",
|
extension: "mp3",
|
||||||
Configuration.Instance.OverwriteExisting);
|
Configuration.Instance.OverwriteExisting);
|
||||||
|
|
||||||
SetFileTime(libraryBook, realMp3Path);
|
SetFileTime(libraryBook, realMp3Path);
|
||||||
SetDirectoryTime(libraryBook, Path.GetDirectoryName(realMp3Path));
|
SetDirectoryTime(libraryBook, Path.GetDirectoryName(realMp3Path));
|
||||||
|
OnFileCreated(libraryBook, realMp3Path);
|
||||||
OnFileCreated(libraryBook, realMp3Path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Error(ex, "AAXClean error");
|
|
||||||
return new StatusHandler { "Conversion failed" };
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (Mp4Operation is not null)
|
if (Mp4Operation is not null)
|
||||||
Mp4Operation.ConversionProgressUpdate -= M4bBook_ConversionProgressUpdate;
|
Mp4Operation.ConversionProgressUpdate -= m4bBook_ConversionProgressUpdate;
|
||||||
|
|
||||||
m4bBook.InputStream.Close();
|
sizeOfCompletedFiles += entry.m4bSize;
|
||||||
mp3File.Close();
|
}
|
||||||
|
void m4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||||
|
{
|
||||||
|
currentFileNumBytesProcessed = (long)(e.FractionCompleted * entry.m4bSize);
|
||||||
|
var bytesCompleted = sizeOfCompletedFiles + currentFileNumBytesProcessed;
|
||||||
|
ConversionProgressUpdate(totalInputSize, bytesCompleted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return new StatusHandler();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Serilog.Log.Error(ex, "AAXClean error");
|
||||||
|
return new StatusHandler { "Conversion failed" };
|
||||||
|
}
|
||||||
|
return new StatusHandler { "Cancelled" };
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
OnCompleted(libraryBook);
|
OnCompleted(libraryBook);
|
||||||
|
CancellationTokenSource.Dispose();
|
||||||
|
CancellationTokenSource = null;
|
||||||
}
|
}
|
||||||
return new StatusHandler();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
|
private void ConversionProgressUpdate(long totalInputSize, long bytesCompleted)
|
||||||
{
|
{
|
||||||
averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds);
|
averageSpeed.AddPosition(bytesCompleted);
|
||||||
|
|
||||||
var remainingTimeToProcess = (e.EndTime - e.ProcessPosition).TotalSeconds;
|
var remainingBytes = (totalInputSize - bytesCompleted);
|
||||||
var estTimeRemaining = remainingTimeToProcess / averageSpeed.Average;
|
var estTimeRemaining = remainingBytes / averageSpeed.Average;
|
||||||
|
|
||||||
if (double.IsNormal(estTimeRemaining))
|
if (double.IsNormal(estTimeRemaining))
|
||||||
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||||
|
|
||||||
double progressPercent = 100 * e.FractionCompleted;
|
double progressPercent = 100 * bytesCompleted / totalInputSize;
|
||||||
|
|
||||||
OnStreamingProgressChanged(
|
OnStreamingProgressChanged(
|
||||||
new DownloadProgress
|
new DownloadProgress
|
||||||
{
|
{
|
||||||
ProgressPercentage = progressPercent,
|
ProgressPercentage = progressPercent,
|
||||||
BytesReceived = (long)(e.ProcessPosition - e.StartTime).TotalSeconds,
|
BytesReceived = bytesCompleted,
|
||||||
TotalBytesToReceive = (long)(e.EndTime - e.StartTime).TotalSeconds
|
TotalBytesToReceive = totalInputSize
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,116 +1,105 @@
|
|||||||
using System;
|
using AaxDecrypter;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using AaxDecrypter;
|
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using AudibleApi.Common;
|
using AudibleApi.Common;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Dinah.Core.ErrorHandling;
|
using Dinah.Core.ErrorHandling;
|
||||||
|
using Dinah.Core.Net.Http;
|
||||||
using FileManager;
|
using FileManager;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileLiberator
|
namespace FileLiberator
|
||||||
{
|
{
|
||||||
public class DownloadDecryptBook : AudioDecodable
|
public class DownloadDecryptBook : AudioDecodable
|
||||||
{
|
{
|
||||||
public override string Name => "Download & Decrypt";
|
public override string Name => "Download & Decrypt";
|
||||||
private AudiobookDownloadBase abDownloader;
|
private CancellationTokenSource? cancellationTokenSource;
|
||||||
private readonly CancellationTokenSource cancellationTokenSource = new();
|
private AudiobookDownloadBase? abDownloader;
|
||||||
|
|
||||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
||||||
public override async Task CancelAsync()
|
public override async Task CancelAsync()
|
||||||
{
|
{
|
||||||
cancellationTokenSource.Cancel();
|
if (abDownloader is not null) await abDownloader.CancelAsync();
|
||||||
if (abDownloader is not null)
|
if (cancellationTokenSource is not null) await cancellationTokenSource.CancelAsync();
|
||||||
await abDownloader.CancelAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
var entries = new List<FilePathCache.CacheEntry>();
|
OnBegin(libraryBook);
|
||||||
// these only work so minimally b/c CacheEntry is a record.
|
cancellationTokenSource = new CancellationTokenSource();
|
||||||
// in case of parallel decrypts, only capture the ones for this book id.
|
var cancellationToken = cancellationTokenSource.Token;
|
||||||
// if user somehow starts multiple decrypts of the same book in parallel: on their own head be it
|
|
||||||
void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e)
|
|
||||||
{
|
|
||||||
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
|
||||||
entries.Add(e);
|
|
||||||
}
|
|
||||||
void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e)
|
|
||||||
{
|
|
||||||
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
|
||||||
entries.Remove(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
OnBegin(libraryBook);
|
|
||||||
var cancellationToken = cancellationTokenSource.Token;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (libraryBook.Book.Audio_Exists())
|
if (libraryBook.Book.Audio_Exists())
|
||||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||||
|
|
||||||
|
DownloadValidation(libraryBook);
|
||||||
|
|
||||||
downloadValidation(libraryBook);
|
|
||||||
var api = await libraryBook.GetApiAsync();
|
var api = await libraryBook.GetApiAsync();
|
||||||
var config = Configuration.Instance;
|
using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, Configuration.Instance, libraryBook, cancellationToken);
|
||||||
using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, config, libraryBook, cancellationToken);
|
var result = await DownloadAudiobookAsync(api, downloadOptions, cancellationToken);
|
||||||
|
|
||||||
bool success = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
FilePathCache.Inserted += FilePathCache_Inserted;
|
|
||||||
FilePathCache.Removed += FilePathCache_Removed;
|
|
||||||
|
|
||||||
success = await downloadAudiobookAsync(api, config, downloadOptions);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
FilePathCache.Inserted -= FilePathCache_Inserted;
|
|
||||||
FilePathCache.Removed -= FilePathCache_Removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// decrypt failed
|
|
||||||
if (!success || getFirstAudioFile(entries) == default)
|
|
||||||
{
|
|
||||||
await Task.WhenAll(
|
|
||||||
entries
|
|
||||||
.Where(f => f.FileType != FileType.AAXC)
|
|
||||||
.Select(f => Task.Run(() => FileUtility.SaferDelete(f.Path))));
|
|
||||||
|
|
||||||
|
if (!result.Success || getFirstAudioFile(result.ResultFiles) is not TempFile audioFile)
|
||||||
|
{
|
||||||
|
// decrypt failed. Delete all output entries but leave the cache files.
|
||||||
|
result.ResultFiles.ForEach(f => FileUtility.SaferDelete(f.FilePath));
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
return new StatusHandler { "Decrypt failed" };
|
return new StatusHandler { "Decrypt failed" };
|
||||||
}
|
}
|
||||||
|
|
||||||
var finalStorageDir = getDestinationDirectory(libraryBook);
|
if (Configuration.Instance.RetainAaxFile)
|
||||||
|
{
|
||||||
|
//Add the cached aaxc and key files to the entries list to be moved to the Books directory.
|
||||||
|
result.ResultFiles.AddRange(getAaxcFiles(result.CacheFiles));
|
||||||
|
}
|
||||||
|
|
||||||
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries, cancellationToken));
|
//Set the last downloaded information on the book so that it can be used in the naming templates,
|
||||||
Task[] finalTasks =
|
//but don't persist it until everything completes successfully (in the finally block)
|
||||||
[
|
var audioFormat = GetFileFormatInfo(downloadOptions, audioFile);
|
||||||
Task.Run(() => downloadCoverArt(downloadOptions, cancellationToken)),
|
var audioVersion = downloadOptions.ContentMetadata.ContentReference.Version;
|
||||||
moveFilesTask,
|
libraryBook.Book.UserDefinedItem.SetLastDownloaded(Configuration.LibationVersion, audioFormat, audioVersion);
|
||||||
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir, cancellationToken))
|
|
||||||
];
|
var finalStorageDir = getDestinationDirectory(libraryBook);
|
||||||
|
|
||||||
|
//post-download tasks done in parallel.
|
||||||
|
var moveFilesTask = Task.Run(() => MoveFilesToBooksDir(libraryBook, finalStorageDir, result.ResultFiles, cancellationToken));
|
||||||
|
Task[] finalTasks =
|
||||||
|
[
|
||||||
|
moveFilesTask,
|
||||||
|
Task.Run(() => DownloadCoverArt(finalStorageDir, downloadOptions, cancellationToken)),
|
||||||
|
Task.Run(() => DownloadRecordsAsync(api, finalStorageDir, downloadOptions, cancellationToken)),
|
||||||
|
Task.Run(() => DownloadMetadataAsync(api, finalStorageDir, downloadOptions, cancellationToken)),
|
||||||
|
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir, cancellationToken))
|
||||||
|
];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.WhenAll(finalTasks);
|
await Task.WhenAll(finalTasks);
|
||||||
}
|
}
|
||||||
catch when (!moveFilesTask.IsFaulted)
|
catch when (!moveFilesTask.IsFaulted)
|
||||||
{
|
{
|
||||||
//Swallow downloadCoverArt and SetCoverAsFolderIcon exceptions.
|
//Swallow DownloadCoverArt, DownloadRecordsAsync, DownloadMetadataAsync, and SetCoverAsFolderIcon exceptions.
|
||||||
//Only fail if the downloaded audio files failed to move to Books directory
|
//Only fail if the downloaded audio files failed to move to Books directory
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (moveFilesTask.IsCompletedSuccessfully && !cancellationToken.IsCancellationRequested)
|
if (moveFilesTask.IsCompletedSuccessfully && !cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion));
|
libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion, audioFormat, audioVersion);
|
||||||
|
|
||||||
SetDirectoryTime(libraryBook, finalStorageDir);
|
SetDirectoryTime(libraryBook, finalStorageDir);
|
||||||
|
foreach (var cacheFile in result.CacheFiles.Where(f => File.Exists(f.FilePath)))
|
||||||
|
{
|
||||||
|
//Delete cache files only after the download/decrypt operation completes successfully.
|
||||||
|
FileUtility.SaferDelete(cacheFile.FilePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,59 +111,86 @@ namespace FileLiberator
|
|||||||
return new StatusHandler { "Cancelled" };
|
return new StatusHandler { "Cancelled" };
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
OnCompleted(libraryBook);
|
OnCompleted(libraryBook);
|
||||||
}
|
cancellationTokenSource.Dispose();
|
||||||
}
|
cancellationTokenSource = null;
|
||||||
|
}
|
||||||
private async Task<bool> downloadAudiobookAsync(AudibleApi.Api api, Configuration config, DownloadOptions dlOptions)
|
|
||||||
{
|
|
||||||
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(dlOptions.LibraryBookDto, dlOptions.OutputFormat.ToString().ToLower());
|
|
||||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
|
||||||
|
|
||||||
if (dlOptions.DrmType is not DrmType.Adrm and not DrmType.Widevine)
|
|
||||||
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, dlOptions);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AaxcDownloadConvertBase converter
|
|
||||||
= config.SplitFilesByChapter ?
|
|
||||||
new AaxcDownloadMultiConverter(outFileName, cacheDir, dlOptions) :
|
|
||||||
new AaxcDownloadSingleConverter(outFileName, cacheDir, dlOptions);
|
|
||||||
|
|
||||||
if (config.AllowLibationFixup)
|
|
||||||
converter.RetrievedMetadata += Converter_RetrievedMetadata;
|
|
||||||
|
|
||||||
abDownloader = converter;
|
|
||||||
}
|
|
||||||
|
|
||||||
abDownloader.DecryptProgressUpdate += OnStreamingProgressChanged;
|
|
||||||
abDownloader.DecryptTimeRemaining += OnStreamingTimeRemaining;
|
|
||||||
abDownloader.RetrievedTitle += OnTitleDiscovered;
|
|
||||||
abDownloader.RetrievedAuthors += OnAuthorsDiscovered;
|
|
||||||
abDownloader.RetrievedNarrators += OnNarratorsDiscovered;
|
|
||||||
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
|
||||||
abDownloader.FileCreated += (_, path) => OnFileCreated(dlOptions.LibraryBook, path);
|
|
||||||
|
|
||||||
// REAL WORK DONE HERE
|
|
||||||
var success = await abDownloader.RunAsync();
|
|
||||||
|
|
||||||
if (success && config.SaveMetadataToFile)
|
|
||||||
{
|
|
||||||
var metadataFile = LibationFileManager.Templates.Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json");
|
|
||||||
|
|
||||||
var item = await api.GetCatalogProductAsync(dlOptions.LibraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
|
||||||
item.SourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ChapterInfo));
|
|
||||||
item.SourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(dlOptions.ContentMetadata.ContentReference));
|
|
||||||
|
|
||||||
File.WriteAllText(metadataFile, item.SourceJson.ToString());
|
|
||||||
OnFileCreated(dlOptions.LibraryBook, metadataFile);
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Converter_RetrievedMetadata(object sender, AAXClean.AppleTags tags)
|
private record AudiobookDecryptResult(bool Success, List<TempFile> ResultFiles, List<TempFile> CacheFiles);
|
||||||
|
|
||||||
|
private async Task<AudiobookDecryptResult> DownloadAudiobookAsync(AudibleApi.Api api, DownloadOptions dlOptions, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (sender is not AaxcDownloadConvertBase converter || converter.DownloadOptions is not DownloadOptions options)
|
var outpoutDir = AudibleFileStorage.DecryptInProgressDirectory;
|
||||||
|
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||||
|
var result = new AudiobookDecryptResult(false, [], []);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (dlOptions.DrmType is not DrmType.Adrm and not DrmType.Widevine)
|
||||||
|
abDownloader = new UnencryptedAudiobookDownloader(outpoutDir, cacheDir, dlOptions);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AaxcDownloadConvertBase converter
|
||||||
|
= dlOptions.Config.SplitFilesByChapter && dlOptions.ChapterInfo.Count > 1 ?
|
||||||
|
new AaxcDownloadMultiConverter(outpoutDir, cacheDir, dlOptions) :
|
||||||
|
new AaxcDownloadSingleConverter(outpoutDir, cacheDir, dlOptions);
|
||||||
|
|
||||||
|
if (dlOptions.Config.AllowLibationFixup)
|
||||||
|
converter.RetrievedMetadata += Converter_RetrievedMetadata;
|
||||||
|
|
||||||
|
abDownloader = converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
abDownloader.DecryptProgressUpdate += OnStreamingProgressChanged;
|
||||||
|
abDownloader.DecryptTimeRemaining += OnStreamingTimeRemaining;
|
||||||
|
abDownloader.RetrievedTitle += OnTitleDiscovered;
|
||||||
|
abDownloader.RetrievedAuthors += OnAuthorsDiscovered;
|
||||||
|
abDownloader.RetrievedNarrators += OnNarratorsDiscovered;
|
||||||
|
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||||
|
abDownloader.TempFileCreated += AbDownloader_TempFileCreated;
|
||||||
|
|
||||||
|
// REAL WORK DONE HERE
|
||||||
|
bool success = await abDownloader.RunAsync();
|
||||||
|
return result with { Success = success };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
Serilog.Log.Logger.Error(ex, "Error downloading audiobook {@Book}", dlOptions.LibraryBook.LogFriendly());
|
||||||
|
//don't throw any exceptions so the caller can delete any temp files.
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
OnStreamingProgressChanged(new() { ProgressPercentage = 100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
void AbDownloader_TempFileCreated(object? sender, TempFile e)
|
||||||
|
{
|
||||||
|
if (Path.GetDirectoryName(e.FilePath) == outpoutDir)
|
||||||
|
{
|
||||||
|
result.ResultFiles.Add(e);
|
||||||
|
}
|
||||||
|
else if (Path.GetDirectoryName(e.FilePath) == cacheDir)
|
||||||
|
{
|
||||||
|
result.CacheFiles.Add(e);
|
||||||
|
// Notify that the aaxc file has been created so that
|
||||||
|
// the UI can know about partially-downloaded files
|
||||||
|
if (getFileType(e) is FileType.AAXC)
|
||||||
|
OnFileCreated(dlOptions.LibraryBook, e.FilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Decryptor event handlers
|
||||||
|
private void Converter_RetrievedMetadata(object? sender, AAXClean.AppleTags tags)
|
||||||
|
{
|
||||||
|
if (sender is not AaxcDownloadConvertBase converter ||
|
||||||
|
converter.AaxFile is not AAXClean.Mp4File aaxFile ||
|
||||||
|
converter.DownloadOptions is not DownloadOptions options ||
|
||||||
|
options.ChapterInfo.Chapters is not List<AAXClean.Chapter> chapters)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
#region Prevent erroneous truncation due to incorrect chapter info
|
#region Prevent erroneous truncation due to incorrect chapter info
|
||||||
@ -185,159 +201,312 @@ namespace FileLiberator
|
|||||||
//the chapter. This is never desirable, so pad the last chapter to match
|
//the chapter. This is never desirable, so pad the last chapter to match
|
||||||
//the original audio length.
|
//the original audio length.
|
||||||
|
|
||||||
var fileDuration = converter.AaxFile.Duration;
|
var fileDuration = aaxFile.Duration;
|
||||||
if (options.Config.StripAudibleBrandAudio)
|
if (options.Config.StripAudibleBrandAudio)
|
||||||
fileDuration -= TimeSpan.FromMilliseconds(options.ContentMetadata.ChapterInfo.BrandOutroDurationMs);
|
fileDuration -= TimeSpan.FromMilliseconds(options.ContentMetadata.ChapterInfo.BrandOutroDurationMs);
|
||||||
|
|
||||||
var durationDelta = fileDuration - options.ChapterInfo.EndOffset;
|
var durationDelta = fileDuration - options.ChapterInfo.EndOffset;
|
||||||
//Remove the last chapter and re-add it with the durationDelta that will
|
//Remove the last chapter and re-add it with the durationDelta that will
|
||||||
//make the chapter's end coincide with the end of the audio file.
|
//make the chapter's end coincide with the end of the audio file.
|
||||||
var chapters = options.ChapterInfo.Chapters as List<AAXClean.Chapter>;
|
|
||||||
var lastChapter = chapters[^1];
|
var lastChapter = chapters[^1];
|
||||||
|
|
||||||
chapters.Remove(lastChapter);
|
chapters.Remove(lastChapter);
|
||||||
options.ChapterInfo.Add(lastChapter.Title, lastChapter.Duration + durationDelta);
|
options.ChapterInfo.Add(lastChapter.Title, lastChapter.Duration + durationDelta);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
tags.Title ??= options.LibraryBookDto.TitleWithSubtitle;
|
tags.Title ??= options.LibraryBookDto.TitleWithSubtitle;
|
||||||
tags.Album ??= tags.Title;
|
tags.Album ??= tags.Title;
|
||||||
tags.Artist ??= string.Join("; ", options.LibraryBook.Book.Authors.Select(a => a.Name));
|
tags.Artist ??= string.Join("; ", options.LibraryBook.Book.Authors.Select(a => a.Name));
|
||||||
tags.AlbumArtists ??= tags.Artist;
|
tags.AlbumArtists ??= tags.Artist;
|
||||||
tags.Generes = string.Join(", ", options.LibraryBook.Book.LowestCategoryNames());
|
tags.Generes = string.Join(", ", options.LibraryBook.Book.LowestCategoryNames());
|
||||||
tags.ProductID ??= options.ContentMetadata.ContentReference.Sku;
|
tags.ProductID ??= options.ContentMetadata.ContentReference.Sku;
|
||||||
tags.Comment ??= options.LibraryBook.Book.Description;
|
tags.Comment ??= options.LibraryBook.Book.Description;
|
||||||
tags.LongDescription ??= tags.Comment;
|
tags.LongDescription ??= tags.Comment;
|
||||||
tags.Publisher ??= options.LibraryBook.Book.Publisher;
|
tags.Publisher ??= options.LibraryBook.Book.Publisher;
|
||||||
tags.Narrator ??= string.Join("; ", options.LibraryBook.Book.Narrators.Select(n => n.Name));
|
tags.Narrator ??= string.Join("; ", options.LibraryBook.Book.Narrators.Select(n => n.Name));
|
||||||
tags.Asin = options.LibraryBook.Book.AudibleProductId;
|
tags.Asin = options.LibraryBook.Book.AudibleProductId;
|
||||||
tags.Acr = options.ContentMetadata.ContentReference.Acr;
|
tags.Acr = options.ContentMetadata.ContentReference.Acr;
|
||||||
tags.Version = options.ContentMetadata.ContentReference.Version;
|
tags.Version = options.ContentMetadata.ContentReference.Version;
|
||||||
if (options.LibraryBook.Book.DatePublished is DateTime pubDate)
|
if (options.LibraryBook.Book.DatePublished is DateTime pubDate)
|
||||||
{
|
{
|
||||||
tags.Year ??= pubDate.Year.ToString();
|
tags.Year ??= pubDate.Year.ToString();
|
||||||
tags.ReleaseDate ??= pubDate.ToString("dd-MMM-yyyy");
|
tags.ReleaseDate ??= pubDate.ToString("dd-MMM-yyyy");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static void downloadValidation(LibraryBook libraryBook)
|
|
||||||
{
|
|
||||||
string errorString(string field)
|
|
||||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
|
||||||
|
|
||||||
string errorTitle()
|
|
||||||
{
|
|
||||||
var title
|
|
||||||
= (libraryBook.Book.TitleWithSubtitle.Length > 53)
|
|
||||||
? $"{libraryBook.Book.TitleWithSubtitle.Truncate(50)}..."
|
|
||||||
: libraryBook.Book.TitleWithSubtitle;
|
|
||||||
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
|
||||||
return errorBookTitle;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(libraryBook.Account))
|
|
||||||
throw new Exception(errorString("Account"));
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
|
|
||||||
throw new Exception(errorString("Locale"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e)
|
|
||||||
{
|
|
||||||
if (Configuration.Instance.AllowLibationFixup)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
e = OnRequestCoverArt();
|
|
||||||
abDownloader.SetCoverArt(e);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, "Failed to retrieve cover art from server.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e is not null)
|
|
||||||
OnCoverImageDiscovered(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Move new files to 'Books' directory</summary>
|
private void AaxcDownloader_RetrievedCoverArt(object? sender, byte[]? e)
|
||||||
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>
|
{
|
||||||
private static void moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries, CancellationToken cancellationToken)
|
if (Configuration.Instance.AllowLibationFixup && sender is AaxcDownloadConvertBase downloader)
|
||||||
{
|
{
|
||||||
// create final directory. move each file into it
|
try
|
||||||
var destinationDir = getDestinationDirectory(libraryBook);
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
e = OnRequestCoverArt();
|
||||||
|
downloader.SetCoverArt(e);
|
||||||
for (var i = 0; i < entries.Count; i++)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
var entry = entries[i];
|
{
|
||||||
|
Serilog.Log.Logger.Error(ex, "Failed to retrieve cover art from server.");
|
||||||
var realDest
|
}
|
||||||
= FileUtility.SaferMoveToValidPath(
|
|
||||||
entry.Path,
|
|
||||||
Path.Combine(destinationDir, Path.GetFileName(entry.Path)),
|
|
||||||
Configuration.Instance.ReplacementCharacters,
|
|
||||||
overwrite: Configuration.Instance.OverwriteExisting);
|
|
||||||
|
|
||||||
SetFileTime(libraryBook, realDest);
|
|
||||||
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
|
|
||||||
|
|
||||||
// propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
|
|
||||||
entries[i] = entry with { Path = realDest };
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
if (e is not null)
|
||||||
if (cue != default)
|
OnCoverImageDiscovered(e);
|
||||||
{
|
}
|
||||||
Cue.UpdateFileName(cue.Path, getFirstAudioFile(entries).Path);
|
#endregion
|
||||||
SetFileTime(libraryBook, cue.Path);
|
|
||||||
|
#region Validation
|
||||||
|
|
||||||
|
private static void DownloadValidation(LibraryBook libraryBook)
|
||||||
|
{
|
||||||
|
string errorString(string field)
|
||||||
|
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
||||||
|
|
||||||
|
string errorTitle()
|
||||||
|
{
|
||||||
|
var title
|
||||||
|
= (libraryBook.Book.TitleWithSubtitle.Length > 53)
|
||||||
|
? $"{libraryBook.Book.TitleWithSubtitle.Truncate(50)}..."
|
||||||
|
: libraryBook.Book.TitleWithSubtitle;
|
||||||
|
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
||||||
|
return errorBookTitle;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(libraryBook.Account))
|
||||||
|
throw new InvalidOperationException(errorString("Account"));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
|
||||||
|
throw new InvalidOperationException(errorString("Locale"));
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Post-success routines
|
||||||
|
/// <summary>Read the audio format from the audio file's metadata.</summary>
|
||||||
|
public AudioFormat GetFileFormatInfo(DownloadOptions options, TempFile firstAudioFile)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return firstAudioFile.Extension.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
".m4b" or ".m4a" or ".mp4" => GetMp4AudioFormat(),
|
||||||
|
".mp3" => AudioFormatDecoder.FromMpeg3(firstAudioFile.FilePath),
|
||||||
|
_ => AudioFormat.Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
//Failure to determine output audio format should not be considered a failure to download the book
|
||||||
|
Serilog.Log.Logger.Error(ex, "Error determining output audio format for {@Book}. File = '{@audioFile}'", options.LibraryBook, firstAudioFile);
|
||||||
|
return AudioFormat.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioFormat GetMp4AudioFormat()
|
||||||
|
=> abDownloader is AaxcDownloadConvertBase converter && converter.AaxFile is AAXClean.Mp4File mp4File
|
||||||
|
? AudioFormatDecoder.FromMpeg4(mp4File)
|
||||||
|
: AudioFormatDecoder.FromMpeg4(firstAudioFile.FilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Move new files to 'Books' directory</summary>
|
||||||
|
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>
|
||||||
|
private void MoveFilesToBooksDir(LibraryBook libraryBook, LongPath destinationDir, List<TempFile> entries, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
AverageSpeed averageSpeed = new();
|
||||||
|
|
||||||
|
var totalSizeToMove = entries.Sum(f => new FileInfo(f.FilePath).Length);
|
||||||
|
long totalBytesMoved = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < entries.Count; i++)
|
||||||
|
{
|
||||||
|
var entry = entries[i];
|
||||||
|
|
||||||
|
var destFileName
|
||||||
|
= AudibleFileStorage.Audio.GetCustomDirFilename(
|
||||||
|
libraryBook,
|
||||||
|
destinationDir,
|
||||||
|
entry.Extension,
|
||||||
|
entry.PartProperties,
|
||||||
|
Configuration.Instance.OverwriteExisting);
|
||||||
|
|
||||||
|
var realDest
|
||||||
|
= FileUtility.SaferMoveToValidPath(
|
||||||
|
entry.FilePath,
|
||||||
|
destFileName,
|
||||||
|
Configuration.Instance.ReplacementCharacters,
|
||||||
|
entry.Extension,
|
||||||
|
Configuration.Instance.OverwriteExisting);
|
||||||
|
|
||||||
|
#region File Move Progress
|
||||||
|
totalBytesMoved += new FileInfo(realDest).Length;
|
||||||
|
averageSpeed.AddPosition(totalBytesMoved);
|
||||||
|
var estSecsRemaining = (totalSizeToMove - totalBytesMoved) / averageSpeed.Average;
|
||||||
|
|
||||||
|
if (double.IsNormal(estSecsRemaining))
|
||||||
|
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estSecsRemaining));
|
||||||
|
|
||||||
|
OnStreamingProgressChanged(new DownloadProgress
|
||||||
|
{
|
||||||
|
ProgressPercentage = 100d * totalBytesMoved / totalSizeToMove,
|
||||||
|
BytesReceived = totalBytesMoved,
|
||||||
|
TotalBytesToReceive = totalSizeToMove
|
||||||
|
});
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// propagate corrected path for cue file (after this for-loop)
|
||||||
|
entries[i] = entry with { FilePath = realDest };
|
||||||
|
|
||||||
|
SetFileTime(libraryBook, realDest);
|
||||||
|
OnFileCreated(libraryBook, realDest);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.FirstOrDefault(f => getFileType(f) is FileType.Cue) is TempFile cue
|
||||||
|
&& getFirstAudioFile(entries)?.FilePath is LongPath audioFilePath)
|
||||||
|
{
|
||||||
|
Cue.UpdateFileName(cue.FilePath, audioFilePath);
|
||||||
|
SetFileTime(libraryBook, cue.FilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
AudibleFileStorage.Audio.Refresh();
|
AudibleFileStorage.Audio.Refresh();
|
||||||
}
|
|
||||||
|
|
||||||
private static string getDestinationDirectory(LibraryBook libraryBook)
|
|
||||||
{
|
|
||||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
|
||||||
if (!Directory.Exists(destinationDir))
|
|
||||||
Directory.CreateDirectory(destinationDir);
|
|
||||||
return destinationDir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> entries)
|
private void DownloadCoverArt(LongPath destinationDir, DownloadOptions options, CancellationToken cancellationToken)
|
||||||
=> entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
{
|
||||||
|
if (!options.Config.DownloadCoverArt) return;
|
||||||
|
|
||||||
private static void downloadCoverArt(DownloadOptions options, CancellationToken cancellationToken)
|
var coverPath = "[null]";
|
||||||
{
|
|
||||||
if (!Configuration.Instance.DownloadCoverArt) return;
|
|
||||||
|
|
||||||
var coverPath = "[null]";
|
try
|
||||||
|
{
|
||||||
|
coverPath
|
||||||
|
= AudibleFileStorage.Audio.GetCustomDirFilename(
|
||||||
|
options.LibraryBook,
|
||||||
|
destinationDir,
|
||||||
|
extension: ".jpg",
|
||||||
|
returnFirstExisting: Configuration.Instance.OverwriteExisting);
|
||||||
|
|
||||||
try
|
if (File.Exists(coverPath))
|
||||||
{
|
FileUtility.SaferDelete(coverPath);
|
||||||
var destinationDir = getDestinationDirectory(options.LibraryBook);
|
|
||||||
coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(options.LibraryBookDto, ".jpg");
|
|
||||||
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
|
|
||||||
|
|
||||||
if (File.Exists(coverPath))
|
var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native), cancellationToken);
|
||||||
FileUtility.SaferDelete(coverPath);
|
if (picBytes.Length > 0)
|
||||||
|
{
|
||||||
|
File.WriteAllBytes(coverPath, picBytes);
|
||||||
|
SetFileTime(options.LibraryBook, coverPath);
|
||||||
|
OnFileCreated(options.LibraryBook, coverPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
//Failure to download cover art should not be considered a failure to download the book
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
Serilog.Log.Logger.Error(ex, "Error downloading cover art for {@Book} to {@metadataFile}.", options.LibraryBook, coverPath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var picBytes = PictureStorage.GetPictureSynchronously(new(options.LibraryBook.Book.PictureLarge ?? options.LibraryBook.Book.PictureId, PictureSize.Native), cancellationToken);
|
public async Task DownloadRecordsAsync(AudibleApi.Api api, LongPath destinationDir, DownloadOptions options, CancellationToken cancellationToken)
|
||||||
if (picBytes.Length > 0)
|
{
|
||||||
{
|
if (!options.Config.DownloadClipsBookmarks) return;
|
||||||
File.WriteAllBytes(coverPath, picBytes);
|
|
||||||
SetFileTime(options.LibraryBook, coverPath);
|
var recordsPath = "[null]";
|
||||||
}
|
var format = options.Config.ClipsBookmarksFileFormat;
|
||||||
}
|
var formatExtension = FileUtility.GetStandardizedExtension(format.ToString().ToLowerInvariant());
|
||||||
catch (Exception ex)
|
|
||||||
{
|
try
|
||||||
//Failure to download cover art should not be considered a failure to download the book
|
{
|
||||||
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {options.LibraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
|
recordsPath
|
||||||
throw;
|
= AudibleFileStorage.Audio.GetCustomDirFilename(
|
||||||
}
|
options.LibraryBook,
|
||||||
}
|
destinationDir,
|
||||||
}
|
extension: formatExtension,
|
||||||
|
returnFirstExisting: Configuration.Instance.OverwriteExisting);
|
||||||
|
|
||||||
|
if (File.Exists(recordsPath))
|
||||||
|
FileUtility.SaferDelete(recordsPath);
|
||||||
|
|
||||||
|
var records = await api.GetRecordsAsync(options.AudibleProductId);
|
||||||
|
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case Configuration.ClipBookmarkFormat.CSV:
|
||||||
|
RecordExporter.ToCsv(recordsPath, records);
|
||||||
|
break;
|
||||||
|
case Configuration.ClipBookmarkFormat.Xlsx:
|
||||||
|
RecordExporter.ToXlsx(recordsPath, records);
|
||||||
|
break;
|
||||||
|
case Configuration.ClipBookmarkFormat.Json:
|
||||||
|
RecordExporter.ToJson(recordsPath, options.LibraryBook, records);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"Unsupported record export format: {format}");
|
||||||
|
}
|
||||||
|
|
||||||
|
SetFileTime(options.LibraryBook, recordsPath);
|
||||||
|
OnFileCreated(options.LibraryBook, recordsPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
//Failure to download records should not be considered a failure to download the book
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
Serilog.Log.Logger.Error(ex, "Error downloading clips and bookmarks for {@Book} to {@recordsPath}.", options.LibraryBook, recordsPath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DownloadMetadataAsync(AudibleApi.Api api, LongPath destinationDir, DownloadOptions options, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!options.Config.SaveMetadataToFile) return;
|
||||||
|
|
||||||
|
string metadataPath = "[null]";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
metadataPath
|
||||||
|
= AudibleFileStorage.Audio.GetCustomDirFilename(
|
||||||
|
options.LibraryBook,
|
||||||
|
destinationDir,
|
||||||
|
extension: ".metadata.json",
|
||||||
|
returnFirstExisting: Configuration.Instance.OverwriteExisting);
|
||||||
|
|
||||||
|
if (File.Exists(metadataPath))
|
||||||
|
FileUtility.SaferDelete(metadataPath);
|
||||||
|
|
||||||
|
var item = await api.GetCatalogProductAsync(options.LibraryBook.Book.AudibleProductId, AudibleApi.CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||||
|
item.SourceJson.Add(nameof(ContentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(options.ContentMetadata.ChapterInfo));
|
||||||
|
item.SourceJson.Add(nameof(ContentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(options.ContentMetadata.ContentReference));
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
File.WriteAllText(metadataPath, item.SourceJson.ToString());
|
||||||
|
SetFileTime(options.LibraryBook, metadataPath);
|
||||||
|
OnFileCreated(options.LibraryBook, metadataPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
//Failure to download metadata should not be considered a failure to download the book
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
Serilog.Log.Logger.Error(ex, "Error downloading metdatat of {@Book} to {@metadataFile}.", options.LibraryBook, metadataPath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Macros
|
||||||
|
private static string getDestinationDirectory(LibraryBook libraryBook)
|
||||||
|
{
|
||||||
|
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||||
|
if (!Directory.Exists(destinationDir))
|
||||||
|
Directory.CreateDirectory(destinationDir);
|
||||||
|
return destinationDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FileType getFileType(TempFile file)
|
||||||
|
=> FileTypes.GetFileTypeFromPath(file.FilePath);
|
||||||
|
private static TempFile? getFirstAudioFile(IEnumerable<TempFile> entries)
|
||||||
|
=> entries.FirstOrDefault(f => File.Exists(f.FilePath) && getFileType(f) is FileType.Audio);
|
||||||
|
private static IEnumerable<TempFile> getAaxcFiles(IEnumerable<TempFile> entries)
|
||||||
|
=> entries.Where(f => File.Exists(f.FilePath) && (getFileType(f) is FileType.AAXC || f.Extension.Equals(".key", StringComparison.OrdinalIgnoreCase)));
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -112,7 +111,6 @@ public partial class DownloadOptions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, LicenseInfo licInfo)
|
private static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, LicenseInfo licInfo)
|
||||||
{
|
{
|
||||||
long chapterStartMs
|
long chapterStartMs
|
||||||
@ -126,13 +124,6 @@ public partial class DownloadOptions
|
|||||||
RuntimeLength = TimeSpan.FromMilliseconds(licInfo.ContentMetadata.ChapterInfo.RuntimeLengthMs),
|
RuntimeLength = TimeSpan.FromMilliseconds(licInfo.ContentMetadata.ChapterInfo.RuntimeLengthMs),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (TryGetAudioInfo(licInfo.ContentMetadata.ContentUrl, out int? bitrate, out int? sampleRate, out int? channels))
|
|
||||||
{
|
|
||||||
dlOptions.LibraryBookDto.BitRate = bitrate;
|
|
||||||
dlOptions.LibraryBookDto.SampleRate = sampleRate;
|
|
||||||
dlOptions.LibraryBookDto.Channels = channels;
|
|
||||||
}
|
|
||||||
|
|
||||||
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
|
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
|
||||||
var chapters
|
var chapters
|
||||||
= flattenChapters(licInfo.ContentMetadata.ChapterInfo.Chapters, titleConcat)
|
= flattenChapters(licInfo.ContentMetadata.ChapterInfo.Chapters, titleConcat)
|
||||||
@ -159,43 +150,6 @@ public partial class DownloadOptions
|
|||||||
return dlOptions;
|
return dlOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The most reliable way to get these audio file properties is from the filename itself.
|
|
||||||
/// Using AAXClean to read the metadata works well for everything except AC-4 bitrate.
|
|
||||||
/// </summary>
|
|
||||||
private static bool TryGetAudioInfo(ContentUrl? contentUrl, out int? bitrate, out int? sampleRate, out int? channels)
|
|
||||||
{
|
|
||||||
bitrate = sampleRate = channels = null;
|
|
||||||
|
|
||||||
if (contentUrl?.OfflineUrl is not string url || !Uri.TryCreate(url, default, out var uri))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var file = Path.GetFileName(uri.LocalPath);
|
|
||||||
|
|
||||||
var match = AdrmAudioProperties().Match(file);
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
bitrate = int.Parse(match.Groups[1].Value);
|
|
||||||
sampleRate = int.Parse(match.Groups[2].Value);
|
|
||||||
channels = int.Parse(match.Groups[3].Value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if ((match = WidevineAudioProperties().Match(file)).Success)
|
|
||||||
{
|
|
||||||
bitrate = int.Parse(match.Groups[2].Value);
|
|
||||||
sampleRate = int.Parse(match.Groups[1].Value) * 1000;
|
|
||||||
channels = match.Groups[3].Value switch
|
|
||||||
{
|
|
||||||
"ec3" => 6,
|
|
||||||
"ac4" => 3,
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LameConfig GetLameOptions(Configuration config)
|
public static LameConfig GetLameOptions(Configuration config)
|
||||||
{
|
{
|
||||||
LameConfig lameConfig = new()
|
LameConfig lameConfig = new()
|
||||||
@ -350,12 +304,4 @@ public partial class DownloadOptions
|
|||||||
chapters.Remove(chapters[^1]);
|
chapters.Remove(chapters[^1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static double RelativePercentDifference(long num1, long num2)
|
|
||||||
=> Math.Abs(num1 - num2) / (double)(num1 + num2);
|
|
||||||
|
|
||||||
[GeneratedRegex(@".+_(\d+)_(\d+)-(\w+).mp4", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
|
|
||||||
private static partial Regex WidevineAudioProperties();
|
|
||||||
[GeneratedRegex(@".+_lc_(\d+)_(\d+)_(\d+).aax", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
|
|
||||||
private static partial Regex AdrmAudioProperties();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,8 @@ using AAXClean;
|
|||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using ApplicationServices;
|
|
||||||
using LibationFileManager.Templates;
|
using LibationFileManager.Templates;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@ -31,12 +29,9 @@ namespace FileLiberator
|
|||||||
public float? SeriesNumber => LibraryBookDto.FirstSeries?.Number;
|
public float? SeriesNumber => LibraryBookDto.FirstSeries?.Number;
|
||||||
public NAudio.Lame.LameConfig? LameConfig { get; }
|
public NAudio.Lame.LameConfig? LameConfig { get; }
|
||||||
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 StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged;
|
public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged;
|
||||||
public bool CreateCueSheet => Config.CreateCueSheet;
|
public bool CreateCueSheet => Config.CreateCueSheet;
|
||||||
public bool DownloadClipsBookmarks => Config.DownloadClipsBookmarks;
|
|
||||||
public long DownloadSpeedBps => Config.DownloadSpeedLimit;
|
public long DownloadSpeedBps => Config.DownloadSpeedLimit;
|
||||||
public bool RetainEncryptedFile => Config.RetainAaxFile;
|
|
||||||
public bool FixupFile => Config.AllowLibationFixup;
|
public bool FixupFile => Config.AllowLibationFixup;
|
||||||
public bool Downsample => Config.AllowLibationFixup && Config.LameDownsampleMono;
|
public bool Downsample => Config.AllowLibationFixup && Config.LameDownsampleMono;
|
||||||
public bool MatchSourceBitrate => Config.AllowLibationFixup && Config.LameMatchSourceBR && Config.LameTargetBitrate;
|
public bool MatchSourceBitrate => Config.AllowLibationFixup && Config.LameMatchSourceBR && Config.LameTargetBitrate;
|
||||||
@ -45,45 +40,9 @@ namespace FileLiberator
|
|||||||
public AudibleApi.Common.DrmType DrmType { get; }
|
public AudibleApi.Common.DrmType DrmType { get; }
|
||||||
public AudibleApi.Common.ContentMetadata ContentMetadata { get; }
|
public AudibleApi.Common.ContentMetadata ContentMetadata { get; }
|
||||||
|
|
||||||
public string GetMultipartFileName(MultiConvertFileProperties props)
|
|
||||||
{
|
|
||||||
var baseDir = Path.GetDirectoryName(props.OutputFileName);
|
|
||||||
var extension = Path.GetExtension(props.OutputFileName);
|
|
||||||
return Templates.ChapterFile.GetFilename(LibraryBookDto, props, baseDir!, extension);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetMultipartTitle(MultiConvertFileProperties props)
|
public string GetMultipartTitle(MultiConvertFileProperties props)
|
||||||
=> Templates.ChapterTitle.GetName(LibraryBookDto, props);
|
=> Templates.ChapterTitle.GetName(LibraryBookDto, props);
|
||||||
|
|
||||||
public async Task<string> SaveClipsAndBookmarksAsync(string fileName)
|
|
||||||
{
|
|
||||||
if (DownloadClipsBookmarks)
|
|
||||||
{
|
|
||||||
var format = Config.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Configuration Config { get; }
|
public Configuration Config { get; }
|
||||||
private readonly IDisposable cancellation;
|
private readonly IDisposable cancellation;
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@ -123,7 +82,6 @@ namespace FileLiberator
|
|||||||
|
|
||||||
// no null/empty check for key/iv. unencrypted files do not have them
|
// no null/empty check for key/iv. unencrypted files do not have them
|
||||||
LibraryBookDto = LibraryBook.ToDto();
|
LibraryBookDto = LibraryBook.ToDto();
|
||||||
LibraryBookDto.Codec = licInfo.ContentMetadata.ContentReference.Codec;
|
|
||||||
|
|
||||||
cancellation =
|
cancellation =
|
||||||
config
|
config
|
||||||
|
|||||||
@ -61,7 +61,13 @@ namespace FileLiberator
|
|||||||
IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
|
IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
|
||||||
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
|
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
|
||||||
|
|
||||||
Language = libraryBook.Book.Language
|
Language = libraryBook.Book.Language,
|
||||||
|
Codec = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.CodecString,
|
||||||
|
BitRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.BitRate,
|
||||||
|
SampleRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.SampleRate,
|
||||||
|
Channels = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.ChannelCount,
|
||||||
|
LibationVersion = libraryBook.Book.UserDefinedItem.LastDownloadedVersion?.ToString(3),
|
||||||
|
FileVersion = libraryBook.Book.UserDefinedItem.LastDownloadedFileVersion
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,9 +24,8 @@
|
|||||||
CanUserSortColumns="False"
|
CanUserSortColumns="False"
|
||||||
AutoGenerateColumns="False"
|
AutoGenerateColumns="False"
|
||||||
IsReadOnly="False"
|
IsReadOnly="False"
|
||||||
ItemsSource="{Binding Filters}"
|
ItemsSource="{CompiledBinding Filters}"
|
||||||
GridLinesVisibility="All">
|
GridLinesVisibility="All">
|
||||||
|
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
|
|
||||||
<DataGridTemplateColumn Header="Delete">
|
<DataGridTemplateColumn Header="Delete">
|
||||||
@ -38,7 +37,7 @@
|
|||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Center"
|
HorizontalContentAlignment="Center"
|
||||||
IsEnabled="{Binding !IsDefault}"
|
IsEnabled="{CompiledBinding !IsDefault}"
|
||||||
Click="DeleteButton_Clicked" />
|
Click="DeleteButton_Clicked" />
|
||||||
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
@ -48,14 +47,13 @@
|
|||||||
<DataGridTextColumn
|
<DataGridTextColumn
|
||||||
Width="*"
|
Width="*"
|
||||||
IsReadOnly="False"
|
IsReadOnly="False"
|
||||||
Binding="{Binding Name, Mode=TwoWay}"
|
Binding="{CompiledBinding Name, Mode=TwoWay}"
|
||||||
Header="Name"/>
|
Header="Name"/>
|
||||||
|
|
||||||
|
|
||||||
<DataGridTextColumn
|
<DataGridTextColumn
|
||||||
Width="*"
|
Width="*"
|
||||||
IsReadOnly="False"
|
IsReadOnly="False"
|
||||||
Binding="{Binding FilterString, Mode=TwoWay}"
|
Binding="{CompiledBinding FilterString, Mode=TwoWay}"
|
||||||
Header="Filter"/>
|
Header="Filter"/>
|
||||||
|
|
||||||
<DataGridTemplateColumn Header="Move
Up">
|
<DataGridTemplateColumn Header="Move
Up">
|
||||||
@ -67,16 +65,19 @@
|
|||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Center"
|
HorizontalContentAlignment="Center"
|
||||||
IsEnabled="{Binding !IsDefault}"
|
Click="MoveUpButton_Clicked">
|
||||||
ToolTip.Tip="Export account authorization to audible-cli"
|
<Button.IsEnabled>
|
||||||
Click="MoveUpButton_Clicked" />
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<CompiledBinding Path="!IsTop" />
|
||||||
|
<CompiledBinding Path="!IsDefault" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
</Button>
|
||||||
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<DataGridTemplateColumn Header="Move
Down">
|
<DataGridTemplateColumn Header="Move
Down">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
@ -86,15 +87,18 @@
|
|||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Center"
|
HorizontalContentAlignment="Center"
|
||||||
IsEnabled="{Binding !IsDefault}"
|
Click="MoveDownButton_Clicked">
|
||||||
ToolTip.Tip="Export account authorization to audible-cli"
|
<Button.IsEnabled>
|
||||||
Click="MoveDownButton_Clicked" />
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<CompiledBinding Path="!IsBottom" />
|
||||||
|
<CompiledBinding Path="!IsDefault" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
</Button>
|
||||||
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
<Grid
|
<Grid
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
using AudibleUtilities;
|
using AudibleUtilities;
|
||||||
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace LibationAvalonia.Dialogs
|
namespace LibationAvalonia.Dialogs
|
||||||
{
|
{
|
||||||
public partial class EditQuickFilters : DialogWindow
|
public partial class EditQuickFilters : DialogWindow
|
||||||
{
|
{
|
||||||
public ObservableCollection<Filter> Filters { get; } = new();
|
public AvaloniaList<Filter> Filters { get; } = new();
|
||||||
|
|
||||||
public class Filter : ViewModels.ViewModelBase
|
public class Filter : ViewModels.ViewModelBase
|
||||||
{
|
{
|
||||||
@ -17,11 +17,8 @@ namespace LibationAvalonia.Dialogs
|
|||||||
public string Name
|
public string Name
|
||||||
{
|
{
|
||||||
get => _name;
|
get => _name;
|
||||||
set
|
set => this.RaiseAndSetIfChanged(ref _name, value);
|
||||||
{
|
}
|
||||||
this.RaiseAndSetIfChanged(ref _name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string _filterString;
|
private string _filterString;
|
||||||
public string FilterString
|
public string FilterString
|
||||||
@ -35,6 +32,10 @@ namespace LibationAvalonia.Dialogs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool IsDefault { get; private set; } = true;
|
public bool IsDefault { get; private set; } = true;
|
||||||
|
private bool _isTop;
|
||||||
|
private bool _isBottom;
|
||||||
|
public bool IsTop { get => _isTop; set => this.RaiseAndSetIfChanged(ref _isTop, value); }
|
||||||
|
public bool IsBottom { get => _isBottom; set => this.RaiseAndSetIfChanged(ref _isBottom, value); }
|
||||||
|
|
||||||
public QuickFilters.NamedFilter AsNamedFilter() => new(FilterString, Name);
|
public QuickFilters.NamedFilter AsNamedFilter() => new(FilterString, Name);
|
||||||
|
|
||||||
@ -44,12 +45,12 @@ namespace LibationAvalonia.Dialogs
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
if (Design.IsDesignMode)
|
if (Design.IsDesignMode)
|
||||||
{
|
{
|
||||||
Filters = new ObservableCollection<Filter>([
|
Filters = [
|
||||||
new Filter { Name = "Filter 1", FilterString = "[filter1 string]" },
|
new Filter { Name = "Filter 1", FilterString = "[filter1 string]", IsTop = true },
|
||||||
new Filter { Name = "Filter 2", FilterString = "[filter2 string]" },
|
new Filter { Name = "Filter 2", FilterString = "[filter2 string]" },
|
||||||
new Filter { Name = "Filter 3", FilterString = "[filter3 string]" },
|
new Filter { Name = "Filter 3", FilterString = "[filter3 string]" },
|
||||||
new Filter { Name = "Filter 4", FilterString = "[filter4 string]" }
|
new Filter { Name = "Filter 4", FilterString = "[filter4 string]", IsBottom = true },
|
||||||
]);
|
new Filter()];
|
||||||
DataContext = this;
|
DataContext = this;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -65,6 +66,8 @@ namespace LibationAvalonia.Dialogs
|
|||||||
ControlToFocusOnShow = this.FindControl<Button>(nameof(saveBtn));
|
ControlToFocusOnShow = this.FindControl<Button>(nameof(saveBtn));
|
||||||
|
|
||||||
var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f.Filter, Name = f.Name }).ToList();
|
var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f.Filter, Name = f.Name }).ToList();
|
||||||
|
allFilters[0].IsTop = true;
|
||||||
|
allFilters[^1].IsBottom = true;
|
||||||
allFilters.Add(new Filter());
|
allFilters.Add(new Filter());
|
||||||
|
|
||||||
foreach (var f in allFilters)
|
foreach (var f in allFilters)
|
||||||
@ -81,6 +84,7 @@ namespace LibationAvalonia.Dialogs
|
|||||||
var newBlank = new Filter();
|
var newBlank = new Filter();
|
||||||
newBlank.PropertyChanged += Filter_PropertyChanged;
|
newBlank.PropertyChanged += Filter_PropertyChanged;
|
||||||
Filters.Insert(Filters.Count, newBlank);
|
Filters.Insert(Filters.Count, newBlank);
|
||||||
|
ReIndexFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void SaveAndClose()
|
protected override void SaveAndClose()
|
||||||
@ -98,30 +102,54 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
filter.PropertyChanged -= Filter_PropertyChanged;
|
filter.PropertyChanged -= Filter_PropertyChanged;
|
||||||
Filters.Remove(filter);
|
Filters.Remove(filter);
|
||||||
|
ReIndexFilters();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MoveUpButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public void MoveUpButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Source is Button btn && btn.DataContext is Filter filter)
|
if (e.Source is not Button btn || btn.DataContext is not Filter filter || filter.IsDefault)
|
||||||
{
|
return;
|
||||||
var index = Filters.IndexOf(filter);
|
|
||||||
if (index < 1) return;
|
|
||||||
|
|
||||||
Filters.Remove(filter);
|
var oldIndex = Filters.IndexOf(filter);
|
||||||
Filters.Insert(index - 1, filter);
|
if (oldIndex < 1) return;
|
||||||
}
|
|
||||||
|
var filterCount = Filters.Count(f => !f.IsDefault);
|
||||||
|
|
||||||
|
MoveFilter(oldIndex, oldIndex - 1, filterCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MoveDownButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public void MoveDownButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Source is Button btn && btn.DataContext is Filter filter)
|
if (e.Source is not Button btn || btn.DataContext is not Filter filter || filter.IsDefault)
|
||||||
{
|
return;
|
||||||
var index = Filters.IndexOf(filter);
|
|
||||||
if (index >= Filters.Count - 2) return;
|
|
||||||
|
|
||||||
Filters.Remove(filter);
|
var filterCount = Filters.Count(f => !f.IsDefault);
|
||||||
Filters.Insert(index + 1, filter);
|
var oldIndex = Filters.IndexOf(filter);
|
||||||
|
if (oldIndex >= filterCount - 1) return;
|
||||||
|
|
||||||
|
MoveFilter(oldIndex, oldIndex + 1, filterCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveFilter(int oldIndex, int newIndex, int filterCount)
|
||||||
|
{
|
||||||
|
var filter = Filters[oldIndex];
|
||||||
|
Filters.RemoveAt(oldIndex);
|
||||||
|
Filters.Insert(newIndex, filter);
|
||||||
|
|
||||||
|
Filters[oldIndex].IsTop = oldIndex == 0;
|
||||||
|
Filters[newIndex].IsTop = newIndex == 0;
|
||||||
|
Filters[newIndex].IsBottom = newIndex == filterCount - 1;
|
||||||
|
Filters[oldIndex].IsBottom = oldIndex == filterCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReIndexFilters()
|
||||||
|
{
|
||||||
|
var filterCount = Filters.Count(f => !f.IsDefault);
|
||||||
|
for (int i = filterCount - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
Filters[i].IsTop = i == 0;
|
||||||
|
Filters[i].IsBottom = i == filterCount - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,71 +2,73 @@
|
|||||||
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="800" d:DesignHeight="650"
|
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||||
MinWidth="800" MinHeight="650"
|
x:DataType="dialogs:SearchSyntaxDialog"
|
||||||
MaxWidth="800" MaxHeight="650"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="50"
|
||||||
|
MinWidth="500" MinHeight="650"
|
||||||
Width="800" Height="650"
|
Width="800" Height="650"
|
||||||
x:Class="LibationAvalonia.Dialogs.SearchSyntaxDialog"
|
x:Class="LibationAvalonia.Dialogs.SearchSyntaxDialog"
|
||||||
Title="Filter Options"
|
Title="Filter Options"
|
||||||
WindowStartupLocation="CenterOwner">
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
Margin="10,0,10,10"
|
RowDefinitions="Auto,*"
|
||||||
RowDefinitions="Auto,Auto,*"
|
ColumnDefinitions="*,*,*,*">
|
||||||
ColumnDefinitions="Auto,Auto,Auto,Auto">
|
|
||||||
|
|
||||||
<Grid.Styles>
|
<Grid.Styles>
|
||||||
|
<Style Selector="Grid > Grid">
|
||||||
|
<Setter Property="Margin" Value="10,0" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Grid > TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
</Style>
|
||||||
<Style Selector="TextBlock">
|
<Style Selector="TextBlock">
|
||||||
<Setter Property="FontSize" Value="12" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
<Setter Property="Margin" Value="10" />
|
<Setter Property="Margin" Value="0,5" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBox">
|
||||||
|
<Setter Property="Margin" Value="0,5,0,10"/>
|
||||||
|
<Style Selector="^ > ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Style Selector="^ TextBlock">
|
||||||
|
<Setter Property="Margin" Value="8,1"/>
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
</Style>
|
</Style>
|
||||||
</Grid.Styles>
|
</Grid.Styles>
|
||||||
|
|
||||||
<TextBlock
|
<Grid
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="0"
|
|
||||||
Grid.ColumnSpan="4"
|
Grid.ColumnSpan="4"
|
||||||
Text="Full Lucene query syntax is supported
Fields with similar names are synomyns (eg: Author, Authors, AuthorNames)

TAG FORMAT: [tagName]" />
|
RowDefinitions="Auto,Auto">
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="1"
|
Text="Full Lucene query syntax is supported
Fields with similar names are synomyns (eg: Author, Authors, AuthorNames)" />
|
||||||
Grid.Column="0"
|
|
||||||
Text="STRING FIELDS" />
|
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock Grid.Row="1" Text="TAG FORMAT: [tagName]" />
|
||||||
Grid.Row="1"
|
</Grid>
|
||||||
Grid.Column="1"
|
|
||||||
Text="NUMBER FIELDS" />
|
|
||||||
|
|
||||||
<TextBlock
|
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,*">
|
||||||
Grid.Row="1"
|
<TextBlock Text="NUMBER FIELDS" />
|
||||||
Grid.Column="2"
|
<TextBlock Grid.Row="1" Text="{CompiledBinding StringUsage}" />
|
||||||
Text="BOOLEAN (TRUE/FALSE) FIELDS" />
|
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding StringFields}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<TextBlock
|
<Grid Grid.Row="1" Grid.Column="1" RowDefinitions="Auto,Auto,*">
|
||||||
Grid.Row="1"
|
<TextBlock Text="STRING FIELDS" />
|
||||||
Grid.Column="3"
|
<TextBlock Grid.Row="1" Text="{CompiledBinding NumberUsage}" />
|
||||||
Text="ID FIELDS" />
|
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding NumberFields}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<TextBlock
|
<Grid Grid.Row="1" Grid.Column="2" RowDefinitions="Auto,Auto,*">
|
||||||
Grid.Row="2"
|
<TextBlock Text="BOOLEAN (TRUE/FALSE) FIELDS" />
|
||||||
Grid.Column="0"
|
<TextBlock Grid.Row="1" Text="{CompiledBinding BoolUsage}" />
|
||||||
Text="{Binding StringFields}" />
|
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding BoolFields}"/>
|
||||||
|
</Grid>
|
||||||
<TextBlock
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="1"
|
|
||||||
Text="{Binding NumberFields}" />
|
|
||||||
|
|
||||||
<TextBlock
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="2"
|
|
||||||
Text="{Binding BoolFields}" />
|
|
||||||
|
|
||||||
<TextBlock
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="3"
|
|
||||||
Text="{Binding IdFields}" />
|
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" Grid.Column="3" RowDefinitions="Auto,Auto,*">
|
||||||
|
<TextBlock Text="ID FIELDS" />
|
||||||
|
<TextBlock Grid.Row="1" Text="{CompiledBinding IdUsage}" />
|
||||||
|
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding IdFields}"/>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@ -1,59 +1,55 @@
|
|||||||
using LibationSearchEngine;
|
using LibationSearchEngine;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace LibationAvalonia.Dialogs
|
namespace LibationAvalonia.Dialogs
|
||||||
{
|
{
|
||||||
public partial class SearchSyntaxDialog : DialogWindow
|
public partial class SearchSyntaxDialog : DialogWindow
|
||||||
{
|
{
|
||||||
public string StringFields { get; init; }
|
public string StringUsage { get; }
|
||||||
public string NumberFields { get; init; }
|
public string NumberUsage { get; }
|
||||||
public string BoolFields { get; init; }
|
public string BoolUsage { get; }
|
||||||
public string IdFields { get; init; }
|
public string IdUsage { get; }
|
||||||
|
public string[] StringFields { get; } = SearchEngine.FieldIndexRules.StringFieldNames.ToArray();
|
||||||
|
public string[] NumberFields { get; } = SearchEngine.FieldIndexRules.NumberFieldNames.ToArray();
|
||||||
|
public string[] BoolFields { get; } = SearchEngine.FieldIndexRules.BoolFieldNames.ToArray();
|
||||||
|
public string[] IdFields { get; } = SearchEngine.FieldIndexRules.IdFieldNames.ToArray();
|
||||||
|
|
||||||
public SearchSyntaxDialog()
|
public SearchSyntaxDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
StringFields = @"
|
StringUsage = """
|
||||||
Search for wizard of oz:
|
Search for wizard of oz:
|
||||||
title:oz
|
title:oz
|
||||||
title:""wizard of oz""
|
title:"wizard of oz"
|
||||||
|
""";
|
||||||
|
|
||||||
|
NumberUsage = """
|
||||||
|
Find books between 1-100 minutes long
|
||||||
|
length:[1 TO 100]
|
||||||
|
Find books exactly 1 hr long
|
||||||
|
length:60
|
||||||
|
Find books published from 2020-1-1 to
|
||||||
|
2023-12-31
|
||||||
|
datepublished:[20200101 TO 20231231]
|
||||||
|
""";
|
||||||
|
|
||||||
" + string.Join("\r\n", SearchEngine.FieldIndexRules.StringFieldNames);
|
BoolUsage = """
|
||||||
|
Find books that you haven't rated:
|
||||||
|
-IsRated
|
||||||
|
""";
|
||||||
|
|
||||||
NumberFields = @"
|
IdUsage = """
|
||||||
Find books between 1-100 minutes long
|
Alice's Adventures in
|
||||||
length:[1 TO 100]
|
Wonderland (ID: B015D78L0U)
|
||||||
Find books exactly 1 hr long
|
|
||||||
length:60
|
|
||||||
Find books published from 2020-1-1 to
|
|
||||||
2023-12-31
|
|
||||||
datepublished:[20200101 TO 20231231]
|
|
||||||
|
|
||||||
|
id:B015D78L0U
|
||||||
|
|
||||||
" + string.Join("\r\n", SearchEngine.FieldIndexRules.NumberFieldNames);
|
All of these are synonyms
|
||||||
|
for the ID field
|
||||||
BoolFields = @"
|
""";
|
||||||
Find books that you haven't rated:
|
|
||||||
-IsRated
|
|
||||||
|
|
||||||
|
|
||||||
" + string.Join("\r\n", SearchEngine.FieldIndexRules.BoolFieldNames);
|
|
||||||
|
|
||||||
IdFields = @"
|
|
||||||
Alice's Adventures in
|
|
||||||
Wonderland (ID: B015D78L0U)
|
|
||||||
|
|
||||||
id:B015D78L0U
|
|
||||||
|
|
||||||
All of these are synonyms
|
|
||||||
for the ID field
|
|
||||||
|
|
||||||
|
|
||||||
" + string.Join("\r\n", SearchEngine.FieldIndexRules.IdFieldNames);
|
|
||||||
|
|
||||||
|
|
||||||
DataContext = this;
|
DataContext = this;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,6 @@ namespace LibationAvalonia.ViewModels
|
|||||||
/// <summary> Indicates if the first quick filter is the default filter </summary>
|
/// <summary> Indicates if the first quick filter is the default filter </summary>
|
||||||
public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); }
|
public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); }
|
||||||
|
|
||||||
|
|
||||||
private void Configure_Filters()
|
private void Configure_Filters()
|
||||||
{
|
{
|
||||||
FirstFilterIsDefault = QuickFilters.UseDefault;
|
FirstFilterIsDefault = QuickFilters.UseDefault;
|
||||||
@ -55,7 +54,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void AddQuickFilterBtn() { if (SelectedNamedFilter != null) QuickFilters.Add(SelectedNamedFilter); }
|
public void AddQuickFilterBtn() { if (SelectedNamedFilter != null) QuickFilters.Add(SelectedNamedFilter); }
|
||||||
public async Task FilterBtn() => await PerformFilter(SelectedNamedFilter);
|
public async Task FilterBtn(string filterString) => await PerformFilter(new(filterString, null));
|
||||||
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
|
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
|
||||||
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
|
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
|
||||||
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
|
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
|
||||||
|
|||||||
@ -479,6 +479,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
public DataGridLength MiscWidth { get => getColumnWidth("Misc", 140); set => setColumnWidth("Misc", value); }
|
public DataGridLength MiscWidth { get => getColumnWidth("Misc", 140); set => setColumnWidth("Misc", value); }
|
||||||
public DataGridLength LastDownloadWidth { get => getColumnWidth("LastDownload", 100); set => setColumnWidth("LastDownload", value); }
|
public DataGridLength LastDownloadWidth { get => getColumnWidth("LastDownload", 100); set => setColumnWidth("LastDownload", value); }
|
||||||
public DataGridLength BookTagsWidth { get => getColumnWidth("BookTags", 100); set => setColumnWidth("BookTags", value); }
|
public DataGridLength BookTagsWidth { get => getColumnWidth("BookTags", 100); set => setColumnWidth("BookTags", value); }
|
||||||
|
public DataGridLength IsSpatialWidth { get => getColumnWidth("IsSpatial", 100); set => setColumnWidth("IsSpatial", value); }
|
||||||
|
|
||||||
private static DataGridLength getColumnWidth(string columnName, double defaultWidth)
|
private static DataGridLength getColumnWidth(string columnName, double defaultWidth)
|
||||||
=> Configuration.Instance.GridColumnsWidths.TryGetValue(columnName, out var val)
|
=> Configuration.Instance.GridColumnsWidths.TryGetValue(columnName, out var val)
|
||||||
|
|||||||
@ -191,10 +191,10 @@
|
|||||||
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" Command="{CompiledBinding DoneRemovingBtn}" Content="Done Removing Books"/>
|
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" Command="{CompiledBinding DoneRemovingBtn}" Content="Done Removing Books"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding SelectedNamedFilter.Filter, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
|
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding SelectedNamedFilter.Filter, Mode=OneWay}" KeyDown="filterSearchTb_KeyPress" />
|
||||||
|
|
||||||
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
|
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
|
||||||
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
|
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" CommandParameter="{CompiledBinding #filterSearchTb.Text}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
|
||||||
<Button Padding="2,6,2,6" VerticalAlignment="Stretch" Command="{CompiledBinding ToggleQueueHideBtn}">
|
<Button Padding="2,6,2,6" VerticalAlignment="Stretch" Command="{CompiledBinding ToggleQueueHideBtn}">
|
||||||
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource LeftArrows}">
|
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource LeftArrows}">
|
||||||
<Path.RenderTransform>
|
<Path.RenderTransform>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using AudibleUtilities;
|
using AudibleUtilities;
|
||||||
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
@ -21,6 +22,9 @@ namespace LibationAvalonia.Views
|
|||||||
{
|
{
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
|
if (Design.IsDesignMode)
|
||||||
|
_ = Configuration.Instance.LibationFiles;
|
||||||
|
|
||||||
DataContext = new MainVM(this);
|
DataContext = new MainVM(this);
|
||||||
ApiExtended.LoginChoiceFactory = account => Dispatcher.UIThread.Invoke(() => new Dialogs.Login.AvaloniaLoginChoiceEager(account));
|
ApiExtended.LoginChoiceFactory = account => Dispatcher.UIThread.Invoke(() => new Dialogs.Login.AvaloniaLoginChoiceEager(account));
|
||||||
|
|
||||||
@ -156,7 +160,7 @@ namespace LibationAvalonia.Views
|
|||||||
{
|
{
|
||||||
if (e.Key == Key.Return)
|
if (e.Key == Key.Return)
|
||||||
{
|
{
|
||||||
await ViewModel.PerformFilter(ViewModel.SelectedNamedFilter);
|
await ViewModel.FilterBtn(filterSearchTb.Text);
|
||||||
|
|
||||||
// silence the 'ding'
|
// silence the 'ding'
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
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"
|
||||||
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
||||||
|
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels"
|
||||||
xmlns:uibase="clr-namespace:LibationUiBase.GridView;assembly=LibationUiBase"
|
xmlns:uibase="clr-namespace:LibationUiBase.GridView;assembly=LibationUiBase"
|
||||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||||
mc:Ignorable="d" d:DesignWidth="1560" d:DesignHeight="400"
|
mc:Ignorable="d" d:DesignWidth="1560" d:DesignHeight="400"
|
||||||
|
x:DataType="vm:ProductsDisplayViewModel"
|
||||||
x:Class="LibationAvalonia.Views.ProductsDisplay">
|
x:Class="LibationAvalonia.Views.ProductsDisplay">
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
@ -15,7 +17,7 @@
|
|||||||
ClipboardCopyMode="IncludeHeader"
|
ClipboardCopyMode="IncludeHeader"
|
||||||
GridLinesVisibility="All"
|
GridLinesVisibility="All"
|
||||||
AutoGenerateColumns="False"
|
AutoGenerateColumns="False"
|
||||||
ItemsSource="{Binding GridEntries}"
|
ItemsSource="{CompiledBinding GridEntries}"
|
||||||
CanUserSortColumns="True" BorderThickness="3"
|
CanUserSortColumns="True" BorderThickness="3"
|
||||||
CanUserResizeColumns="True"
|
CanUserResizeColumns="True"
|
||||||
LoadingRow="ProductsDisplay_LoadingRow"
|
LoadingRow="ProductsDisplay_LoadingRow"
|
||||||
@ -51,7 +53,7 @@
|
|||||||
<DataGridTemplateColumn
|
<DataGridTemplateColumn
|
||||||
CanUserSort="True"
|
CanUserSort="True"
|
||||||
CanUserResize="False"
|
CanUserResize="False"
|
||||||
IsVisible="{Binding RemoveColumnVisible}"
|
IsVisible="{CompiledBinding RemoveColumnVisible}"
|
||||||
PropertyChanged="RemoveColumn_PropertyChanged"
|
PropertyChanged="RemoveColumn_PropertyChanged"
|
||||||
Header="Remove"
|
Header="Remove"
|
||||||
IsReadOnly="False"
|
IsReadOnly="False"
|
||||||
@ -83,7 +85,7 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt CanUserResize="False" CanUserSort="False" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
|
<controls:DataGridTemplateColumnExt Header="Cover" CanUserResize="False" CanUserSort="False" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Image Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Cover_Click" Source="{CompiledBinding Cover}" ToolTip.Tip="Click to see full size" />
|
<Image Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Cover_Click" Source="{CompiledBinding Cover}" ToolTip.Tip="Click to see full size" />
|
||||||
@ -91,7 +93,7 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Title" MinWidth="10" Width="{Binding TitleWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
|
<controls:DataGridTemplateColumnExt Header="Title" MinWidth="10" Width="{CompiledBinding TitleWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||||
@ -101,7 +103,7 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Authors" MinWidth="10" Width="{Binding AuthorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Authors" ClipboardContentBinding="{Binding Authors}">
|
<controls:DataGridTemplateColumnExt Header="Authors" MinWidth="10" Width="{CompiledBinding AuthorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Authors" ClipboardContentBinding="{Binding Authors}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||||
@ -111,7 +113,7 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Narrators" MinWidth="10" Width="{Binding NarratorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Narrators" ClipboardContentBinding="{Binding Narrators}">
|
<controls:DataGridTemplateColumnExt Header="Narrators" MinWidth="10" Width="{CompiledBinding NarratorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Narrators" ClipboardContentBinding="{Binding Narrators}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||||
@ -121,7 +123,7 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Length" MinWidth="10" Width="{Binding LengthWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Length" ClipboardContentBinding="{Binding Length}">
|
<controls:DataGridTemplateColumnExt Header="Length" MinWidth="10" Width="{CompiledBinding LengthWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Length" ClipboardContentBinding="{Binding Length}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||||
@ -131,7 +133,7 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Series" MinWidth="10" Width="{Binding SeriesWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Series" ClipboardContentBinding="{Binding Series}">
|
<controls:DataGridTemplateColumnExt Header="Series" MinWidth="10" Width="{CompiledBinding SeriesWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Series" ClipboardContentBinding="{Binding Series}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||||
@ -141,7 +143,7 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Series
Order" MinWidth="10" Width="{Binding SeriesOrderWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="SeriesOrder" ClipboardContentBinding="{Binding Series}">
|
<controls:DataGridTemplateColumnExt Header="Series
Order" MinWidth="10" Width="{CompiledBinding SeriesOrderWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="SeriesOrder" ClipboardContentBinding="{Binding Series}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||||
@ -151,7 +153,7 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Description" MinWidth="10" Width="{Binding DescriptionWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding Description}">
|
<controls:DataGridTemplateColumnExt Header="Description" MinWidth="10" Width="{CompiledBinding DescriptionWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding Description}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
|
||||||
@ -161,7 +163,7 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Category" MinWidth="10" Width="{Binding CategoryWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Category" ClipboardContentBinding="{Binding Category}">
|
<controls:DataGridTemplateColumnExt Header="Category" MinWidth="10" Width="{CompiledBinding CategoryWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Category" ClipboardContentBinding="{Binding Category}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||||
@ -181,7 +183,7 @@
|
|||||||
ClipboardContentBinding="{CompiledBinding ProductRating}"
|
ClipboardContentBinding="{CompiledBinding ProductRating}"
|
||||||
Binding="{CompiledBinding ProductRating}" />
|
Binding="{CompiledBinding ProductRating}" />
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Purchase
Date" MinWidth="10" Width="{Binding PurchaseDateWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="PurchaseDate" ClipboardContentBinding="{Binding PurchaseDate}">
|
<controls:DataGridTemplateColumnExt Header="Purchase
Date" MinWidth="10" Width="{CompiledBinding PurchaseDateWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="PurchaseDate" ClipboardContentBinding="{Binding PurchaseDate}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||||
@ -201,7 +203,7 @@
|
|||||||
ClipboardContentBinding="{CompiledBinding MyRating}"
|
ClipboardContentBinding="{CompiledBinding MyRating}"
|
||||||
Binding="{CompiledBinding MyRating, Mode=TwoWay}" />
|
Binding="{CompiledBinding MyRating, Mode=TwoWay}" />
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Misc" MinWidth="10" Width="{Binding MiscWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}">
|
<controls:DataGridTemplateColumnExt Header="Misc" MinWidth="10" Width="{CompiledBinding MiscWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
|
||||||
@ -211,7 +213,7 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Last
Download" MinWidth="10" Width="{Binding LastDownloadWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="LastDownload" ClipboardContentBinding="{Binding LastDownload}">
|
<controls:DataGridTemplateColumnExt Header="Last
Download" MinWidth="10" Width="{CompiledBinding LastDownloadWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="LastDownload" ClipboardContentBinding="{Binding LastDownload}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Panel Opacity="{CompiledBinding Liberate.Opacity}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}" DoubleTapped="Version_DoubleClick">
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}" DoubleTapped="Version_DoubleClick">
|
||||||
@ -221,7 +223,17 @@
|
|||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</controls:DataGridTemplateColumnExt>
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
<controls:DataGridTemplateColumnExt Header="Tags" MinWidth="10" Width="{Binding BookTagsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="BookTags" ClipboardContentBinding="{Binding BookTags}">
|
<controls:DataGridTemplateColumnExt Header="Is
Spatial" MinWidth="10" Width="{CompiledBinding IsSpatialWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="IsSpatial" ClipboardContentBinding="{Binding IsSpatial}">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
|
<Panel Opacity="{CompiledBinding Liberate.Opacity}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}">
|
||||||
|
<CheckBox IsChecked="{CompiledBinding IsSpatial}" IsEnabled="False" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</Panel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</controls:DataGridTemplateColumnExt>
|
||||||
|
|
||||||
|
<controls:DataGridTemplateColumnExt Header="Tags" MinWidth="10" Width="{CompiledBinding BookTagsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="BookTags" ClipboardContentBinding="{Binding BookTags}">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="uibase:GridEntry">
|
<DataTemplate x:DataType="uibase:GridEntry">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -426,7 +426,6 @@ namespace LibationAvalonia.Views
|
|||||||
productsGrid.ColumnDisplayIndexChanged += ProductsGrid_ColumnDisplayIndexChanged;
|
productsGrid.ColumnDisplayIndexChanged += ProductsGrid_ColumnDisplayIndexChanged;
|
||||||
|
|
||||||
var config = Configuration.Instance;
|
var config = Configuration.Instance;
|
||||||
var gridColumnsVisibilities = config.GridColumnsVisibilities;
|
|
||||||
var displayIndices = config.GridColumnsDisplayIndices;
|
var displayIndices = config.GridColumnsDisplayIndices;
|
||||||
|
|
||||||
var contextMenu = new ContextMenu();
|
var contextMenu = new ContextMenu();
|
||||||
@ -464,7 +463,7 @@ namespace LibationAvalonia.Views
|
|||||||
if (headerCell is not null)
|
if (headerCell is not null)
|
||||||
headerCell.ContextMenu = contextMenu;
|
headerCell.ContextMenu = contextMenu;
|
||||||
|
|
||||||
column.IsVisible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
|
column.IsVisible = config.GetColumnVisibility(itemName);
|
||||||
}
|
}
|
||||||
|
|
||||||
//We must set DisplayIndex properties in ascending order
|
//We must set DisplayIndex properties in ascending order
|
||||||
|
|||||||
@ -179,12 +179,14 @@ namespace LibationFileManager
|
|||||||
[Description("Lame target VBR quality [10,100]")]
|
[Description("Lame target VBR quality [10,100]")]
|
||||||
public int LameVBRQuality { get => GetNonString(defaultValue: 2); set => SetNonString(value); }
|
public int LameVBRQuality { get => GetNonString(defaultValue: 2); set => SetNonString(value); }
|
||||||
|
|
||||||
private static readonly EquatableDictionary<string, bool> DefaultColumns = new(
|
private static readonly EquatableDictionary<string, bool> DefaultColumns = new([
|
||||||
new KeyValuePair<string, bool>[]
|
|
||||||
{
|
|
||||||
new ("SeriesOrder", false),
|
new ("SeriesOrder", false),
|
||||||
new ("LastDownload", false)
|
new ("LastDownload", false),
|
||||||
});
|
new ("IsSpatial", false)
|
||||||
|
]);
|
||||||
|
public bool GetColumnVisibility(string columnName)
|
||||||
|
=> GridColumnsVisibilities.TryGetValue(columnName, out var isVisible) ? isVisible
|
||||||
|
:DefaultColumns.GetValueOrDefault(columnName, true);
|
||||||
|
|
||||||
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
|
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
|
||||||
public Dictionary<string, bool> GridColumnsVisibilities { get => GetNonString(defaultValue: DefaultColumns).Clone(); set => SetNonString(value); }
|
public Dictionary<string, bool> GridColumnsVisibilities { get => GetNonString(defaultValue: DefaultColumns).Clone(); set => SetNonString(value); }
|
||||||
|
|||||||
@ -46,7 +46,9 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
public static List<(FileType fileType, LongPath path)> GetFiles(string id)
|
public static List<(FileType fileType, LongPath path)> GetFiles(string id)
|
||||||
{
|
{
|
||||||
var matchingFiles = Cache.GetIdEntries(id);
|
List<CacheEntry> matchingFiles;
|
||||||
|
lock(locker)
|
||||||
|
matchingFiles = Cache.GetIdEntries(id);
|
||||||
|
|
||||||
bool cacheChanged = false;
|
bool cacheChanged = false;
|
||||||
|
|
||||||
@ -68,7 +70,9 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
public static LongPath? GetFirstPath(string id, FileType type)
|
public static LongPath? GetFirstPath(string id, FileType type)
|
||||||
{
|
{
|
||||||
var matchingFiles = Cache.GetIdEntries(id).Where(e => e.FileType == type).ToList();
|
List<CacheEntry> matchingFiles;
|
||||||
|
lock (locker)
|
||||||
|
matchingFiles = Cache.GetIdEntries(id).Where(e => e.FileType == type).ToList();
|
||||||
|
|
||||||
bool cacheChanged = false;
|
bool cacheChanged = false;
|
||||||
try
|
try
|
||||||
@ -96,7 +100,10 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
private static bool Remove(CacheEntry entry)
|
private static bool Remove(CacheEntry entry)
|
||||||
{
|
{
|
||||||
if (Cache.Remove(entry.Id, entry))
|
bool removed;
|
||||||
|
lock (locker)
|
||||||
|
removed = Cache.Remove(entry.Id, entry);
|
||||||
|
if (removed)
|
||||||
{
|
{
|
||||||
Removed?.Invoke(null, entry);
|
Removed?.Invoke(null, entry);
|
||||||
return true;
|
return true;
|
||||||
@ -112,7 +119,8 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
public static void Insert(CacheEntry entry)
|
public static void Insert(CacheEntry entry)
|
||||||
{
|
{
|
||||||
Cache.Add(entry.Id, entry);
|
lock(locker)
|
||||||
|
Cache.Add(entry.Id, entry);
|
||||||
Inserted?.Invoke(null, entry);
|
Inserted?.Invoke(null, entry);
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,7 +133,16 @@ namespace LibationFileManager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sizeStr = def.Size == PictureSize.Native ? "" : $"._SL{(int)def.Size}_";
|
var sizeStr = def.Size == PictureSize.Native ? "" : $"._SL{(int)def.Size}_";
|
||||||
var bytes = imageDownloadClient.GetByteArrayAsync("ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}{sizeStr}.jpg", cancellationToken).Result;
|
|
||||||
|
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, "ht" + $"tps://images-na.ssl-images-amazon.com/images/I/{def.PictureId}{sizeStr}.jpg");
|
||||||
|
using var response = imageDownloadClient.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
if (response.Content.Headers.ContentLength is not long size)
|
||||||
|
return GetDefaultImage(def.Size);
|
||||||
|
|
||||||
|
var bytes = new byte[size];
|
||||||
|
using var respStream = response.Content.ReadAsStream(cancellationToken);
|
||||||
|
respStream.ReadExactly(bytes);
|
||||||
|
|
||||||
// save image file. make sure to not save default image
|
// save image file. make sure to not save default image
|
||||||
var path = getPath(def);
|
var path = getPath(def);
|
||||||
|
|||||||
@ -34,6 +34,8 @@ public class BookDto
|
|||||||
public DateTime FileDate { get; set; } = DateTime.Now;
|
public DateTime FileDate { get; set; } = DateTime.Now;
|
||||||
public DateTime? DatePublished { get; set; }
|
public DateTime? DatePublished { get; set; }
|
||||||
public string? Language { get; set; }
|
public string? Language { get; set; }
|
||||||
|
public string? LibationVersion { get; set; }
|
||||||
|
public string? FileVersion { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LibraryBookDto : BookDto
|
public class LibraryBookDto : BookDto
|
||||||
|
|||||||
@ -69,6 +69,9 @@ namespace LibationFileManager.Templates
|
|||||||
Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")],
|
Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")],
|
||||||
Narrators = [new("Stephen Fry", null)],
|
Narrators = [new("Stephen Fry", null)],
|
||||||
Series = [new("Sherlock Holmes", 1, "B08376S3R2"), new("Some Other Series", 1, "B000000000")],
|
Series = [new("Sherlock Holmes", 1, "B08376S3R2"), new("Some Other Series", 1, "B000000000")],
|
||||||
|
Codec = "AAC-LC",
|
||||||
|
LibationVersion = Configuration.LibationVersion?.ToString(3),
|
||||||
|
FileVersion = "36217811",
|
||||||
BitRate = 128,
|
BitRate = 128,
|
||||||
SampleRate = 44100,
|
SampleRate = 44100,
|
||||||
Channels = 2,
|
Channels = 2,
|
||||||
|
|||||||
@ -36,10 +36,12 @@ namespace LibationFileManager.Templates
|
|||||||
public static TemplateTags Series { get; } = new TemplateTags("series", "All series to which the book belongs (if any)");
|
public static TemplateTags Series { get; } = new TemplateTags("series", "All series to which the book belongs (if any)");
|
||||||
public static TemplateTags FirstSeries { get; } = new TemplateTags("first series", "First series");
|
public static TemplateTags FirstSeries { get; } = new TemplateTags("first series", "First series");
|
||||||
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series (alias for <first series[{#}]>");
|
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series (alias for <first series[{#}]>");
|
||||||
public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "Audiobook's source bitrate");
|
public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "Bitrate (kbps) of the last downloaded audiobook");
|
||||||
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "Audiobook's source sample rate");
|
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "Sample rate (Hz) of the last downloaded audiobook");
|
||||||
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Audiobook's source audio channel count");
|
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels in the last downloaded audiobook");
|
||||||
public static TemplateTags Codec { get; } = new TemplateTags("codec", "Audiobook's source codec");
|
public static TemplateTags Codec { get; } = new TemplateTags("codec", "Audio codec of the last downloaded audiobook");
|
||||||
|
public static TemplateTags FileVersion { get; } = new TemplateTags("file version", "Audible's file version number of the last downloaded audiobook");
|
||||||
|
public static TemplateTags LibationVersion { get; } = new TemplateTags("libation version", "Libation version used during last download of the audiobook");
|
||||||
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
|
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
|
||||||
public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book");
|
public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book");
|
||||||
public static TemplateTags Locale { get; } = new("locale", "Region/country");
|
public static TemplateTags Locale { get; } = new("locale", "Region/country");
|
||||||
|
|||||||
@ -287,6 +287,8 @@ namespace LibationFileManager.Templates
|
|||||||
{ TemplateTags.SampleRate, lb => lb.SampleRate },
|
{ TemplateTags.SampleRate, lb => lb.SampleRate },
|
||||||
{ TemplateTags.Channels, lb => lb.Channels },
|
{ TemplateTags.Channels, lb => lb.Channels },
|
||||||
{ TemplateTags.Codec, lb => lb.Codec },
|
{ TemplateTags.Codec, lb => lb.Codec },
|
||||||
|
{ TemplateTags.FileVersion, lb => lb.FileVersion },
|
||||||
|
{ TemplateTags.LibationVersion, lb => lb.LibationVersion },
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly List<TagCollection> chapterPropertyTags = new()
|
private static readonly List<TagCollection> chapterPropertyTags = new()
|
||||||
@ -382,7 +384,7 @@ namespace LibationFileManager.Templates
|
|||||||
public static string Name { get; } = "Folder Template";
|
public static string Name { get; } = "Folder Template";
|
||||||
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? "";
|
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? "";
|
||||||
public static string DefaultTemplate { get; } = "<title short> [<id>]";
|
public static string DefaultTemplate { get; } = "<title short> [<id>]";
|
||||||
public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, conditionalTags, folderConditionalTags];
|
public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, audioFilePropertyTags, conditionalTags, folderConditionalTags];
|
||||||
|
|
||||||
public override IEnumerable<string> Errors
|
public override IEnumerable<string> Errors
|
||||||
=> TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors;
|
=> TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors;
|
||||||
|
|||||||
@ -50,6 +50,7 @@ namespace LibationSearchEngine
|
|||||||
{ FieldType.Bool, lb => (lb.Book.UserDefinedItem.Rating.OverallRating > 0f).ToString(), "IsRated", "Rated" },
|
{ FieldType.Bool, lb => (lb.Book.UserDefinedItem.Rating.OverallRating > 0f).ToString(), "IsRated", "Rated" },
|
||||||
{ FieldType.Bool, lb => isAuthorNarrated(lb.Book).ToString(), "IsAuthorNarrated", "AuthorNarrated" },
|
{ FieldType.Bool, lb => isAuthorNarrated(lb.Book).ToString(), "IsAuthorNarrated", "AuthorNarrated" },
|
||||||
{ FieldType.Bool, lb => lb.Book.IsAbridged.ToString(), nameof(Book.IsAbridged), "Abridged" },
|
{ FieldType.Bool, lb => lb.Book.IsAbridged.ToString(), nameof(Book.IsAbridged), "Abridged" },
|
||||||
|
{ FieldType.Bool, lb => lb.Book.IsSpatial.ToString(), nameof(Book.IsSpatial), "Spatial" },
|
||||||
{ FieldType.Bool, lb => (lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated).ToString(), "IsLiberated", "Liberated" },
|
{ FieldType.Bool, lb => (lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated).ToString(), "IsLiberated", "Liberated" },
|
||||||
{ FieldType.Bool, lb => (lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.Error).ToString(), "LiberatedError" },
|
{ FieldType.Bool, lb => (lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.Error).ToString(), "LiberatedError" },
|
||||||
{ FieldType.Bool, lb => lb.Book.IsEpisodeChild().ToString(), "Podcast", "Podcasts", "IsPodcast", "Episode", "Episodes", "IsEpisode" },
|
{ FieldType.Bool, lb => lb.Book.IsEpisodeChild().ToString(), "Podcast", "Podcasts", "IsPodcast", "Episode", "Episodes", "IsEpisode" },
|
||||||
|
|||||||
@ -48,7 +48,7 @@ namespace LibationUiBase.GridView
|
|||||||
private Rating _productrating;
|
private Rating _productrating;
|
||||||
private string _bookTags;
|
private string _bookTags;
|
||||||
private Rating _myRating;
|
private Rating _myRating;
|
||||||
|
private bool _isSpatial;
|
||||||
public abstract bool? Remove { get; set; }
|
public abstract bool? Remove { get; set; }
|
||||||
public EntryStatus Liberate { get => _liberate; private set => RaiseAndSetIfChanged(ref _liberate, value); }
|
public EntryStatus Liberate { get => _liberate; private set => RaiseAndSetIfChanged(ref _liberate, value); }
|
||||||
public string PurchaseDate { get => _purchasedate; protected set => RaiseAndSetIfChanged(ref _purchasedate, value); }
|
public string PurchaseDate { get => _purchasedate; protected set => RaiseAndSetIfChanged(ref _purchasedate, value); }
|
||||||
@ -65,6 +65,7 @@ namespace LibationUiBase.GridView
|
|||||||
public string Description { get => _description; private set => RaiseAndSetIfChanged(ref _description, value); }
|
public string Description { get => _description; private set => RaiseAndSetIfChanged(ref _description, value); }
|
||||||
public Rating ProductRating { get => _productrating; private set => RaiseAndSetIfChanged(ref _productrating, value); }
|
public Rating ProductRating { get => _productrating; private set => RaiseAndSetIfChanged(ref _productrating, value); }
|
||||||
public string BookTags { get => _bookTags; private set => RaiseAndSetIfChanged(ref _bookTags, value); }
|
public string BookTags { get => _bookTags; private set => RaiseAndSetIfChanged(ref _bookTags, value); }
|
||||||
|
public bool IsSpatial { get => _isSpatial; protected set => RaiseAndSetIfChanged(ref _isSpatial, value); }
|
||||||
|
|
||||||
public Rating MyRating
|
public Rating MyRating
|
||||||
{
|
{
|
||||||
@ -118,6 +119,7 @@ namespace LibationUiBase.GridView
|
|||||||
Description = GetDescriptionDisplay(Book);
|
Description = GetDescriptionDisplay(Book);
|
||||||
SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
|
SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
|
||||||
BookTags = GetBookTags();
|
BookTags = GetBookTags();
|
||||||
|
IsSpatial = Book.IsSpatial;
|
||||||
|
|
||||||
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
|
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
|
||||||
}
|
}
|
||||||
@ -205,6 +207,7 @@ namespace LibationUiBase.GridView
|
|||||||
nameof(BookTags) => BookTags ?? string.Empty,
|
nameof(BookTags) => BookTags ?? string.Empty,
|
||||||
nameof(Liberate) => Liberate,
|
nameof(Liberate) => Liberate,
|
||||||
nameof(DateAdded) => DateAdded,
|
nameof(DateAdded) => DateAdded,
|
||||||
|
nameof(IsSpatial) => IsSpatial,
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -6,6 +6,8 @@ namespace LibationUiBase.GridView
|
|||||||
public class LastDownloadStatus : IComparable
|
public class LastDownloadStatus : IComparable
|
||||||
{
|
{
|
||||||
public bool IsValid => LastDownloadedVersion is not null && LastDownloaded.HasValue;
|
public bool IsValid => LastDownloadedVersion is not null && LastDownloaded.HasValue;
|
||||||
|
public AudioFormat LastDownloadedFormat { get; }
|
||||||
|
public string LastDownloadedFileVersion { get; }
|
||||||
public Version LastDownloadedVersion { get; }
|
public Version LastDownloadedVersion { get; }
|
||||||
public DateTime? LastDownloaded { get; }
|
public DateTime? LastDownloaded { get; }
|
||||||
public string ToolTipText => IsValid ? $"Double click to open v{LastDownloadedVersion.ToString(3)} release notes" : "";
|
public string ToolTipText => IsValid ? $"Double click to open v{LastDownloadedVersion.ToString(3)} release notes" : "";
|
||||||
@ -14,6 +16,8 @@ namespace LibationUiBase.GridView
|
|||||||
public LastDownloadStatus(UserDefinedItem udi)
|
public LastDownloadStatus(UserDefinedItem udi)
|
||||||
{
|
{
|
||||||
LastDownloadedVersion = udi.LastDownloadedVersion;
|
LastDownloadedVersion = udi.LastDownloadedVersion;
|
||||||
|
LastDownloadedFormat = udi.LastDownloadedFormat;
|
||||||
|
LastDownloadedFileVersion = udi.LastDownloadedFileVersion;
|
||||||
LastDownloaded = udi.LastDownloaded;
|
LastDownloaded = udi.LastDownloaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,7 +28,13 @@ namespace LibationUiBase.GridView
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
=> IsValid ? $"{dateString()}\n\nLibation v{LastDownloadedVersion.ToString(3)}" : "";
|
=> IsValid ? $"""
|
||||||
|
{dateString()} {versionString()}
|
||||||
|
{LastDownloadedFormat}
|
||||||
|
Libation v{LastDownloadedVersion.ToString(3)}
|
||||||
|
""" : "";
|
||||||
|
|
||||||
|
private string versionString() => LastDownloadedFileVersion is string ver ? $"(File v.{ver})" : "";
|
||||||
|
|
||||||
//Call ToShortDateString to use current culture's date format.
|
//Call ToShortDateString to use current culture's date format.
|
||||||
private string dateString() => $"{LastDownloaded.Value.ToShortDateString()} {LastDownloaded.Value:HH:mm}";
|
private string dateString() => $"{LastDownloaded.Value.ToShortDateString()} {LastDownloaded.Value:HH:mm}";
|
||||||
|
|||||||
@ -111,6 +111,8 @@ public class ProcessQueueViewModel : ReactiveObject
|
|||||||
var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray();
|
var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray();
|
||||||
if (preLiberated.Length > 0)
|
if (preLiberated.Length > 0)
|
||||||
{
|
{
|
||||||
|
if (preLiberated.Length == 1)
|
||||||
|
RemoveCompleted(preLiberated[0]);
|
||||||
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
|
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
|
||||||
AddConvertMp3(preLiberated);
|
AddConvertMp3(preLiberated);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -7,8 +7,14 @@ using System.Runtime.CompilerServices;
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
namespace LibationUiBase;
|
namespace LibationUiBase;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ReactiveObject is the base object for ViewModel classes, and it implements INotifyPropertyChanging
|
||||||
|
/// and INotifyPropertyChanged. Additionally
|
||||||
|
/// object changes.
|
||||||
|
/// </summary>
|
||||||
public class ReactiveObject : SynchronizeInvoker, INotifyPropertyChanged, INotifyPropertyChanging
|
public class ReactiveObject : SynchronizeInvoker, INotifyPropertyChanged, INotifyPropertyChanging
|
||||||
{
|
{
|
||||||
|
// see also notes in Libation/Source/_ARCHITECTURE NOTES.txt :: MVVM
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
public event PropertyChangingEventHandler? PropertyChanging;
|
public event PropertyChangingEventHandler? PropertyChanging;
|
||||||
|
|
||||||
|
|||||||
@ -117,7 +117,7 @@ namespace LibationUiBase.SeriesView
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void DownloadButton_ButtonEnabled(object sender, EventArgs e)
|
private void DownloadButton_ButtonEnabled(object sender, EventArgs e)
|
||||||
=> OnPropertyChanged(nameof(Enabled));
|
=> RaisePropertyChanged(nameof(Enabled));
|
||||||
|
|
||||||
public override int CompareTo(object ob)
|
public override int CompareTo(object ob)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
using AudibleApi.Common;
|
using AudibleApi.Common;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core.Threading;
|
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationUiBase.SeriesView
|
namespace LibationUiBase.SeriesView
|
||||||
@ -10,11 +8,9 @@ namespace LibationUiBase.SeriesView
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// base view model for the Series Viewer 'Availability' button column
|
/// base view model for the Series Viewer 'Availability' button column
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class SeriesButton : SynchronizeInvoker, IComparable, INotifyPropertyChanged
|
public abstract class SeriesButton : ReactiveObject, IComparable
|
||||||
{
|
{
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
|
||||||
private bool inLibrary;
|
private bool inLibrary;
|
||||||
|
|
||||||
protected Item Item { get; }
|
protected Item Item { get; }
|
||||||
public abstract string DisplayText { get; }
|
public abstract string DisplayText { get; }
|
||||||
public abstract bool HasButtonAction { get; }
|
public abstract bool HasButtonAction { get; }
|
||||||
@ -27,8 +23,8 @@ namespace LibationUiBase.SeriesView
|
|||||||
if (inLibrary != value)
|
if (inLibrary != value)
|
||||||
{
|
{
|
||||||
inLibrary = value;
|
inLibrary = value;
|
||||||
OnPropertyChanged(nameof(InLibrary));
|
RaisePropertyChanged(nameof(InLibrary));
|
||||||
OnPropertyChanged(nameof(DisplayText));
|
RaisePropertyChanged(nameof(DisplayText));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,9 +37,6 @@ namespace LibationUiBase.SeriesView
|
|||||||
|
|
||||||
public abstract Task PerformClickAsync(LibraryBook accountBook);
|
public abstract Task PerformClickAsync(LibraryBook accountBook);
|
||||||
|
|
||||||
protected void OnPropertyChanged(string propertyName)
|
|
||||||
=> Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
|
|
||||||
|
|
||||||
public override string ToString() => DisplayText;
|
public override string ToString() => DisplayText;
|
||||||
|
|
||||||
public abstract int CompareTo(object ob);
|
public abstract int CompareTo(object ob);
|
||||||
|
|||||||
@ -4,7 +4,6 @@ using AudibleApi.Common;
|
|||||||
using AudibleUtilities;
|
using AudibleUtilities;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Dinah.Core.Threading;
|
|
||||||
using FileLiberator;
|
using FileLiberator;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -15,7 +14,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace LibationUiBase.SeriesView
|
namespace LibationUiBase.SeriesView
|
||||||
{
|
{
|
||||||
public class SeriesItem : SynchronizeInvoker, INotifyPropertyChanged
|
public class SeriesItem : ReactiveObject
|
||||||
{
|
{
|
||||||
public object Cover { get; private set; }
|
public object Cover { get; private set; }
|
||||||
public SeriesOrder Order { get; }
|
public SeriesOrder Order { get; }
|
||||||
@ -23,8 +22,6 @@ namespace LibationUiBase.SeriesView
|
|||||||
public SeriesButton Button { get; }
|
public SeriesButton Button { get; }
|
||||||
public Item Item { get; }
|
public Item Item { get; }
|
||||||
|
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
|
||||||
|
|
||||||
private SeriesItem(Item item, string order, bool inLibrary, bool inWishList)
|
private SeriesItem(Item item, string order, bool inLibrary, bool inWishList)
|
||||||
{
|
{
|
||||||
Item = item;
|
Item = item;
|
||||||
@ -42,10 +39,7 @@ namespace LibationUiBase.SeriesView
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void DownloadButton_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
private void DownloadButton_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
=> OnPropertyChanged(nameof(Button));
|
=> RaisePropertyChanged(nameof(Button));
|
||||||
|
|
||||||
private void OnPropertyChanged(string propertyName)
|
|
||||||
=> Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
|
|
||||||
|
|
||||||
private void LoadCover(string pictureId)
|
private void LoadCover(string pictureId)
|
||||||
{
|
{
|
||||||
@ -66,7 +60,7 @@ namespace LibationUiBase.SeriesView
|
|||||||
{
|
{
|
||||||
Cover = BaseUtil.LoadImage(e.Picture, PictureSize._80x80);
|
Cover = BaseUtil.LoadImage(e.Picture, PictureSize._80x80);
|
||||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||||
OnPropertyChanged(nameof(Cover));
|
RaisePropertyChanged(nameof(Cover));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,14 +22,7 @@ namespace LibationUiBase.SeriesView
|
|||||||
public override bool Enabled
|
public override bool Enabled
|
||||||
{
|
{
|
||||||
get => instanceEnabled;
|
get => instanceEnabled;
|
||||||
protected set
|
protected set => RaiseAndSetIfChanged(ref instanceEnabled, value);
|
||||||
{
|
|
||||||
if (instanceEnabled != value)
|
|
||||||
{
|
|
||||||
instanceEnabled = value;
|
|
||||||
OnPropertyChanged(nameof(Enabled));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool InWishList
|
private bool InWishList
|
||||||
@ -40,8 +33,8 @@ namespace LibationUiBase.SeriesView
|
|||||||
if (inWishList != value)
|
if (inWishList != value)
|
||||||
{
|
{
|
||||||
inWishList = value;
|
inWishList = value;
|
||||||
OnPropertyChanged(nameof(InWishList));
|
RaisePropertyChanged(nameof(InWishList));
|
||||||
OnPropertyChanged(nameof(DisplayText));
|
RaisePropertyChanged(nameof(DisplayText));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -250,12 +250,12 @@ namespace LibationWinForms.Dialogs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class BookRecordEntry : GridView.AsyncNotifyPropertyChanged
|
private class BookRecordEntry : LibationUiBase.ReactiveObject
|
||||||
{
|
{
|
||||||
private const string DateFormat = "yyyy-MM-dd HH\\:mm";
|
private const string DateFormat = "yyyy-MM-dd HH\\:mm";
|
||||||
private bool _ischecked;
|
private bool _ischecked;
|
||||||
public IRecord Record { get; }
|
public IRecord Record { get; }
|
||||||
public bool IsChecked { get => _ischecked; set { _ischecked = value; NotifyPropertyChanged(); } }
|
public bool IsChecked { get => _ischecked; set => RaiseAndSetIfChanged(ref _ischecked, value); }
|
||||||
public string Type => Record.GetType().Name;
|
public string Type => Record.GetType().Name;
|
||||||
public string Start => formatTimeSpan(Record.Start);
|
public string Start => formatTimeSpan(Record.Start);
|
||||||
public string Created => Record.Created.ToString(DateFormat);
|
public string Created => Record.Created.ToString(DateFormat);
|
||||||
|
|||||||
@ -34,7 +34,26 @@
|
|||||||
label3 = new System.Windows.Forms.Label();
|
label3 = new System.Windows.Forms.Label();
|
||||||
label4 = new System.Windows.Forms.Label();
|
label4 = new System.Windows.Forms.Label();
|
||||||
label5 = new System.Windows.Forms.Label();
|
label5 = new System.Windows.Forms.Label();
|
||||||
closeBtn = new System.Windows.Forms.Button();
|
tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
|
||||||
|
tableLayoutPanel5 = new System.Windows.Forms.TableLayoutPanel();
|
||||||
|
lboxIdFields = new System.Windows.Forms.ListBox();
|
||||||
|
label9 = new System.Windows.Forms.Label();
|
||||||
|
tableLayoutPanel4 = new System.Windows.Forms.TableLayoutPanel();
|
||||||
|
lboxBoolFields = new System.Windows.Forms.ListBox();
|
||||||
|
label8 = new System.Windows.Forms.Label();
|
||||||
|
tableLayoutPanel3 = new System.Windows.Forms.TableLayoutPanel();
|
||||||
|
lboxNumberFields = new System.Windows.Forms.ListBox();
|
||||||
|
label7 = new System.Windows.Forms.Label();
|
||||||
|
tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel();
|
||||||
|
lboxStringFields = new System.Windows.Forms.ListBox();
|
||||||
|
label6 = new System.Windows.Forms.Label();
|
||||||
|
label10 = new System.Windows.Forms.Label();
|
||||||
|
label11 = new System.Windows.Forms.Label();
|
||||||
|
tableLayoutPanel1.SuspendLayout();
|
||||||
|
tableLayoutPanel5.SuspendLayout();
|
||||||
|
tableLayoutPanel4.SuspendLayout();
|
||||||
|
tableLayoutPanel3.SuspendLayout();
|
||||||
|
tableLayoutPanel2.SuspendLayout();
|
||||||
SuspendLayout();
|
SuspendLayout();
|
||||||
//
|
//
|
||||||
// label1
|
// label1
|
||||||
@ -43,75 +62,262 @@
|
|||||||
label1.Location = new System.Drawing.Point(14, 10);
|
label1.Location = new System.Drawing.Point(14, 10);
|
||||||
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
label1.Name = "label1";
|
label1.Name = "label1";
|
||||||
label1.Size = new System.Drawing.Size(410, 60);
|
label1.Size = new System.Drawing.Size(410, 30);
|
||||||
label1.TabIndex = 0;
|
label1.TabIndex = 0;
|
||||||
label1.Text = "Full Lucene query syntax is supported\r\nFields with similar names are synomyns (eg: Author, Authors, AuthorNames)\r\n\r\nTAG FORMAT: [tagName]";
|
label1.Text = "Full Lucene query syntax is supported\r\nFields with similar names are synomyns (eg: Author, Authors, AuthorNames)";
|
||||||
//
|
//
|
||||||
// label2
|
// label2
|
||||||
//
|
//
|
||||||
|
label2.Anchor = System.Windows.Forms.AnchorStyles.Top;
|
||||||
label2.AutoSize = true;
|
label2.AutoSize = true;
|
||||||
label2.Location = new System.Drawing.Point(14, 82);
|
label2.Location = new System.Drawing.Point(48, 18);
|
||||||
label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
label2.Name = "label2";
|
label2.Name = "label2";
|
||||||
label2.Size = new System.Drawing.Size(129, 75);
|
label2.Size = new System.Drawing.Size(129, 45);
|
||||||
label2.TabIndex = 1;
|
label2.TabIndex = 1;
|
||||||
label2.Text = "STRING FIELDS\r\n\r\nSearch for wizard of oz:\r\n title:oz\r\n title:\"wizard of oz\"";
|
label2.Text = "Search for wizard of oz:\r\n title:oz\r\n title:\"wizard of oz\"";
|
||||||
//
|
//
|
||||||
// label3
|
// label3
|
||||||
//
|
//
|
||||||
|
label3.Anchor = System.Windows.Forms.AnchorStyles.Top;
|
||||||
label3.AutoSize = true;
|
label3.AutoSize = true;
|
||||||
label3.Location = new System.Drawing.Point(272, 82);
|
label3.Location = new System.Drawing.Point(4, 18);
|
||||||
label3.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
label3.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
label3.Name = "label3";
|
label3.Name = "label3";
|
||||||
label3.Size = new System.Drawing.Size(224, 135);
|
label3.Size = new System.Drawing.Size(218, 120);
|
||||||
label3.TabIndex = 2;
|
label3.TabIndex = 2;
|
||||||
label3.Text = resources.GetString("label3.Text");
|
label3.Text = resources.GetString("label3.Text");
|
||||||
//
|
//
|
||||||
// label4
|
// label4
|
||||||
//
|
//
|
||||||
|
label4.Anchor = System.Windows.Forms.AnchorStyles.Top;
|
||||||
label4.AutoSize = true;
|
label4.AutoSize = true;
|
||||||
label4.Location = new System.Drawing.Point(530, 82);
|
label4.Location = new System.Drawing.Point(19, 18);
|
||||||
label4.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
label4.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
label4.Name = "label4";
|
label4.Name = "label4";
|
||||||
label4.Size = new System.Drawing.Size(187, 60);
|
label4.Size = new System.Drawing.Size(187, 30);
|
||||||
label4.TabIndex = 3;
|
label4.TabIndex = 3;
|
||||||
label4.Text = "BOOLEAN (TRUE/FALSE) FIELDS\r\n\r\nFind books that you haven't rated:\r\n -IsRated";
|
label4.Text = "Find books that you haven't rated:\r\n -IsRated";
|
||||||
//
|
//
|
||||||
// label5
|
// label5
|
||||||
//
|
//
|
||||||
|
label5.Anchor = System.Windows.Forms.AnchorStyles.Top;
|
||||||
label5.AutoSize = true;
|
label5.AutoSize = true;
|
||||||
label5.Location = new System.Drawing.Point(785, 82);
|
label5.Location = new System.Drawing.Point(8, 18);
|
||||||
label5.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
label5.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
label5.Name = "label5";
|
label5.Name = "label5";
|
||||||
label5.Size = new System.Drawing.Size(278, 90);
|
label5.Size = new System.Drawing.Size(209, 90);
|
||||||
label5.TabIndex = 4;
|
label5.TabIndex = 4;
|
||||||
label5.Text = "ID FIELDS\r\n\r\nAlice's Adventures in Wonderland (ID: B015D78L0U)\r\n id:B015D78L0U\r\n\r\nAll of these are synonyms for the ID field";
|
label5.Text = "Alice's Adventures in Wonderland (ID: B015D78L0U)\r\n id:B015D78L0U\r\n\r\nAll of these are synonyms for the ID field";
|
||||||
//
|
//
|
||||||
// closeBtn
|
// tableLayoutPanel1
|
||||||
//
|
//
|
||||||
closeBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
tableLayoutPanel1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||||
closeBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
tableLayoutPanel1.ColumnCount = 4;
|
||||||
closeBtn.Location = new System.Drawing.Point(1038, 537);
|
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 25F));
|
||||||
closeBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 25F));
|
||||||
closeBtn.Name = "closeBtn";
|
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 25F));
|
||||||
closeBtn.Size = new System.Drawing.Size(88, 27);
|
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 25F));
|
||||||
closeBtn.TabIndex = 5;
|
tableLayoutPanel1.Controls.Add(tableLayoutPanel5, 3, 0);
|
||||||
closeBtn.Text = "Close";
|
tableLayoutPanel1.Controls.Add(tableLayoutPanel4, 2, 0);
|
||||||
closeBtn.UseVisualStyleBackColor = true;
|
tableLayoutPanel1.Controls.Add(tableLayoutPanel3, 1, 0);
|
||||||
closeBtn.Click += CloseBtn_Click;
|
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 0);
|
||||||
|
tableLayoutPanel1.GrowStyle = System.Windows.Forms.TableLayoutPanelGrowStyle.FixedSize;
|
||||||
|
tableLayoutPanel1.Location = new System.Drawing.Point(12, 51);
|
||||||
|
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||||
|
tableLayoutPanel1.RowCount = 1;
|
||||||
|
tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
tableLayoutPanel1.Size = new System.Drawing.Size(928, 425);
|
||||||
|
tableLayoutPanel1.TabIndex = 6;
|
||||||
|
//
|
||||||
|
// tableLayoutPanel5
|
||||||
|
//
|
||||||
|
tableLayoutPanel5.ColumnCount = 1;
|
||||||
|
tableLayoutPanel5.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
tableLayoutPanel5.Controls.Add(lboxIdFields, 0, 2);
|
||||||
|
tableLayoutPanel5.Controls.Add(label5, 0, 1);
|
||||||
|
tableLayoutPanel5.Controls.Add(label9, 0, 0);
|
||||||
|
tableLayoutPanel5.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
tableLayoutPanel5.Location = new System.Drawing.Point(699, 3);
|
||||||
|
tableLayoutPanel5.Name = "tableLayoutPanel5";
|
||||||
|
tableLayoutPanel5.RowCount = 3;
|
||||||
|
tableLayoutPanel5.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tableLayoutPanel5.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tableLayoutPanel5.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
tableLayoutPanel5.Size = new System.Drawing.Size(226, 419);
|
||||||
|
tableLayoutPanel5.TabIndex = 10;
|
||||||
|
//
|
||||||
|
// lboxIdFields
|
||||||
|
//
|
||||||
|
lboxIdFields.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
lboxIdFields.FormattingEnabled = true;
|
||||||
|
lboxIdFields.Location = new System.Drawing.Point(3, 111);
|
||||||
|
lboxIdFields.Name = "lboxIdFields";
|
||||||
|
lboxIdFields.Size = new System.Drawing.Size(220, 305);
|
||||||
|
lboxIdFields.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// label9
|
||||||
|
//
|
||||||
|
label9.Anchor = System.Windows.Forms.AnchorStyles.Top;
|
||||||
|
label9.AutoSize = true;
|
||||||
|
label9.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Underline);
|
||||||
|
label9.Location = new System.Drawing.Point(86, 0);
|
||||||
|
label9.Margin = new System.Windows.Forms.Padding(3, 0, 3, 3);
|
||||||
|
label9.Name = "label9";
|
||||||
|
label9.Size = new System.Drawing.Size(54, 15);
|
||||||
|
label9.TabIndex = 7;
|
||||||
|
label9.Text = "ID Fields";
|
||||||
|
label9.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||||
|
//
|
||||||
|
// tableLayoutPanel4
|
||||||
|
//
|
||||||
|
tableLayoutPanel4.ColumnCount = 1;
|
||||||
|
tableLayoutPanel4.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
tableLayoutPanel4.Controls.Add(lboxBoolFields, 0, 2);
|
||||||
|
tableLayoutPanel4.Controls.Add(label4, 0, 1);
|
||||||
|
tableLayoutPanel4.Controls.Add(label8, 0, 0);
|
||||||
|
tableLayoutPanel4.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
tableLayoutPanel4.Location = new System.Drawing.Point(467, 3);
|
||||||
|
tableLayoutPanel4.Name = "tableLayoutPanel4";
|
||||||
|
tableLayoutPanel4.RowCount = 3;
|
||||||
|
tableLayoutPanel4.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tableLayoutPanel4.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tableLayoutPanel4.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
tableLayoutPanel4.Size = new System.Drawing.Size(226, 419);
|
||||||
|
tableLayoutPanel4.TabIndex = 9;
|
||||||
|
//
|
||||||
|
// lboxBoolFields
|
||||||
|
//
|
||||||
|
lboxBoolFields.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
lboxBoolFields.FormattingEnabled = true;
|
||||||
|
lboxBoolFields.Location = new System.Drawing.Point(3, 51);
|
||||||
|
lboxBoolFields.Name = "lboxBoolFields";
|
||||||
|
lboxBoolFields.Size = new System.Drawing.Size(220, 365);
|
||||||
|
lboxBoolFields.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// label8
|
||||||
|
//
|
||||||
|
label8.Anchor = System.Windows.Forms.AnchorStyles.Top;
|
||||||
|
label8.AutoSize = true;
|
||||||
|
label8.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Underline);
|
||||||
|
label8.Location = new System.Drawing.Point(36, 0);
|
||||||
|
label8.Margin = new System.Windows.Forms.Padding(3, 0, 3, 3);
|
||||||
|
label8.Name = "label8";
|
||||||
|
label8.Size = new System.Drawing.Size(154, 15);
|
||||||
|
label8.TabIndex = 7;
|
||||||
|
label8.Text = "Boolean (True/False) Fields";
|
||||||
|
label8.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||||
|
//
|
||||||
|
// tableLayoutPanel3
|
||||||
|
//
|
||||||
|
tableLayoutPanel3.ColumnCount = 1;
|
||||||
|
tableLayoutPanel3.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
tableLayoutPanel3.Controls.Add(lboxNumberFields, 0, 2);
|
||||||
|
tableLayoutPanel3.Controls.Add(label3, 0, 1);
|
||||||
|
tableLayoutPanel3.Controls.Add(label7, 0, 0);
|
||||||
|
tableLayoutPanel3.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
tableLayoutPanel3.Location = new System.Drawing.Point(235, 3);
|
||||||
|
tableLayoutPanel3.Name = "tableLayoutPanel3";
|
||||||
|
tableLayoutPanel3.RowCount = 3;
|
||||||
|
tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tableLayoutPanel3.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
tableLayoutPanel3.Size = new System.Drawing.Size(226, 419);
|
||||||
|
tableLayoutPanel3.TabIndex = 8;
|
||||||
|
//
|
||||||
|
// lboxNumberFields
|
||||||
|
//
|
||||||
|
lboxNumberFields.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
lboxNumberFields.FormattingEnabled = true;
|
||||||
|
lboxNumberFields.Location = new System.Drawing.Point(3, 141);
|
||||||
|
lboxNumberFields.Name = "lboxNumberFields";
|
||||||
|
lboxNumberFields.Size = new System.Drawing.Size(220, 275);
|
||||||
|
lboxNumberFields.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// label7
|
||||||
|
//
|
||||||
|
label7.Anchor = System.Windows.Forms.AnchorStyles.Top;
|
||||||
|
label7.AutoSize = true;
|
||||||
|
label7.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Underline);
|
||||||
|
label7.Location = new System.Drawing.Point(69, 0);
|
||||||
|
label7.Margin = new System.Windows.Forms.Padding(3, 0, 3, 3);
|
||||||
|
label7.Name = "label7";
|
||||||
|
label7.Size = new System.Drawing.Size(87, 15);
|
||||||
|
label7.TabIndex = 7;
|
||||||
|
label7.Text = "Number Fields";
|
||||||
|
label7.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||||
|
//
|
||||||
|
// tableLayoutPanel2
|
||||||
|
//
|
||||||
|
tableLayoutPanel2.ColumnCount = 1;
|
||||||
|
tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
tableLayoutPanel2.Controls.Add(lboxStringFields, 0, 2);
|
||||||
|
tableLayoutPanel2.Controls.Add(label2, 0, 1);
|
||||||
|
tableLayoutPanel2.Controls.Add(label6, 0, 0);
|
||||||
|
tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
tableLayoutPanel2.Location = new System.Drawing.Point(3, 3);
|
||||||
|
tableLayoutPanel2.Name = "tableLayoutPanel2";
|
||||||
|
tableLayoutPanel2.RowCount = 3;
|
||||||
|
tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||||
|
tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||||
|
tableLayoutPanel2.Size = new System.Drawing.Size(226, 419);
|
||||||
|
tableLayoutPanel2.TabIndex = 7;
|
||||||
|
//
|
||||||
|
// lboxStringFields
|
||||||
|
//
|
||||||
|
lboxStringFields.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
lboxStringFields.FormattingEnabled = true;
|
||||||
|
lboxStringFields.Location = new System.Drawing.Point(3, 66);
|
||||||
|
lboxStringFields.Name = "lboxStringFields";
|
||||||
|
lboxStringFields.Size = new System.Drawing.Size(220, 350);
|
||||||
|
lboxStringFields.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// label6
|
||||||
|
//
|
||||||
|
label6.Anchor = System.Windows.Forms.AnchorStyles.Top;
|
||||||
|
label6.AutoSize = true;
|
||||||
|
label6.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Underline);
|
||||||
|
label6.Location = new System.Drawing.Point(75, 0);
|
||||||
|
label6.Margin = new System.Windows.Forms.Padding(3, 0, 3, 3);
|
||||||
|
label6.Name = "label6";
|
||||||
|
label6.Size = new System.Drawing.Size(75, 15);
|
||||||
|
label6.TabIndex = 0;
|
||||||
|
label6.Text = "String Fields";
|
||||||
|
label6.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||||
|
//
|
||||||
|
// label10
|
||||||
|
//
|
||||||
|
label10.AutoSize = true;
|
||||||
|
label10.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Underline);
|
||||||
|
label10.Location = new System.Drawing.Point(515, 25);
|
||||||
|
label10.Margin = new System.Windows.Forms.Padding(3, 8, 3, 8);
|
||||||
|
label10.Name = "label10";
|
||||||
|
label10.Size = new System.Drawing.Size(72, 15);
|
||||||
|
label10.TabIndex = 7;
|
||||||
|
label10.Text = "Tag Format:";
|
||||||
|
label10.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||||
|
//
|
||||||
|
// label11
|
||||||
|
//
|
||||||
|
label11.AutoSize = true;
|
||||||
|
label11.Font = new System.Drawing.Font("Segoe UI", 9F);
|
||||||
|
label11.Location = new System.Drawing.Point(596, 25);
|
||||||
|
label11.Margin = new System.Windows.Forms.Padding(3, 8, 3, 8);
|
||||||
|
label11.Name = "label11";
|
||||||
|
label11.Size = new System.Drawing.Size(64, 15);
|
||||||
|
label11.TabIndex = 8;
|
||||||
|
label11.Text = "[tagName]";
|
||||||
|
label11.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||||
//
|
//
|
||||||
// SearchSyntaxDialog
|
// SearchSyntaxDialog
|
||||||
//
|
//
|
||||||
AcceptButton = closeBtn;
|
|
||||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||||
CancelButton = closeBtn;
|
ClientSize = new System.Drawing.Size(952, 488);
|
||||||
ClientSize = new System.Drawing.Size(1140, 577);
|
Controls.Add(label11);
|
||||||
Controls.Add(closeBtn);
|
Controls.Add(label10);
|
||||||
Controls.Add(label5);
|
Controls.Add(tableLayoutPanel1);
|
||||||
Controls.Add(label4);
|
|
||||||
Controls.Add(label3);
|
|
||||||
Controls.Add(label2);
|
|
||||||
Controls.Add(label1);
|
Controls.Add(label1);
|
||||||
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
MaximizeBox = false;
|
MaximizeBox = false;
|
||||||
@ -119,6 +325,15 @@
|
|||||||
Name = "SearchSyntaxDialog";
|
Name = "SearchSyntaxDialog";
|
||||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
Text = "Filter options";
|
Text = "Filter options";
|
||||||
|
tableLayoutPanel1.ResumeLayout(false);
|
||||||
|
tableLayoutPanel5.ResumeLayout(false);
|
||||||
|
tableLayoutPanel5.PerformLayout();
|
||||||
|
tableLayoutPanel4.ResumeLayout(false);
|
||||||
|
tableLayoutPanel4.PerformLayout();
|
||||||
|
tableLayoutPanel3.ResumeLayout(false);
|
||||||
|
tableLayoutPanel3.PerformLayout();
|
||||||
|
tableLayoutPanel2.ResumeLayout(false);
|
||||||
|
tableLayoutPanel2.PerformLayout();
|
||||||
ResumeLayout(false);
|
ResumeLayout(false);
|
||||||
PerformLayout();
|
PerformLayout();
|
||||||
}
|
}
|
||||||
@ -130,6 +345,20 @@
|
|||||||
private System.Windows.Forms.Label label3;
|
private System.Windows.Forms.Label label3;
|
||||||
private System.Windows.Forms.Label label4;
|
private System.Windows.Forms.Label label4;
|
||||||
private System.Windows.Forms.Label label5;
|
private System.Windows.Forms.Label label5;
|
||||||
private System.Windows.Forms.Button closeBtn;
|
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
|
||||||
|
private System.Windows.Forms.Label label7;
|
||||||
|
private System.Windows.Forms.Label label6;
|
||||||
|
private System.Windows.Forms.Label label8;
|
||||||
|
private System.Windows.Forms.Label label9;
|
||||||
|
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel5;
|
||||||
|
private System.Windows.Forms.ListBox lboxIdFields;
|
||||||
|
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel4;
|
||||||
|
private System.Windows.Forms.ListBox lboxBoolFields;
|
||||||
|
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel3;
|
||||||
|
private System.Windows.Forms.ListBox lboxNumberFields;
|
||||||
|
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel2;
|
||||||
|
private System.Windows.Forms.ListBox lboxStringFields;
|
||||||
|
private System.Windows.Forms.Label label10;
|
||||||
|
private System.Windows.Forms.Label label11;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
using LibationSearchEngine;
|
using LibationSearchEngine;
|
||||||
using System;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
|
||||||
@ -11,13 +11,17 @@ namespace LibationWinForms.Dialogs
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
label2.Text += "\r\n\r\n" + string.Join("\r\n", SearchEngine.FieldIndexRules.StringFieldNames);
|
lboxNumberFields.Items.AddRange(SearchEngine.FieldIndexRules.NumberFieldNames.ToArray());
|
||||||
label3.Text += "\r\n\r\n" + string.Join("\r\n", SearchEngine.FieldIndexRules.NumberFieldNames);
|
lboxStringFields.Items.AddRange(SearchEngine.FieldIndexRules.StringFieldNames.ToArray());
|
||||||
label4.Text += "\r\n\r\n" + string.Join("\r\n", SearchEngine.FieldIndexRules.BoolFieldNames);
|
lboxBoolFields.Items.AddRange(SearchEngine.FieldIndexRules.BoolFieldNames.ToArray());
|
||||||
label5.Text += "\r\n\r\n" + string.Join("\r\n", SearchEngine.FieldIndexRules.IdFieldNames);
|
lboxIdFields.Items.AddRange(SearchEngine.FieldIndexRules.IdFieldNames.ToArray());
|
||||||
this.SetLibationIcon();
|
this.SetLibationIcon();
|
||||||
|
this.RestoreSizeAndLocation(LibationFileManager.Configuration.Instance);
|
||||||
|
}
|
||||||
|
protected override void OnClosing(CancelEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnClosing(e);
|
||||||
|
this.SaveSizeAndLocation(LibationFileManager.Configuration.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseBtn_Click(object sender, EventArgs e) => this.Close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
using Dinah.Core.Threading;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace LibationWinForms.GridView
|
|
||||||
{
|
|
||||||
public abstract class AsyncNotifyPropertyChanged : SynchronizeInvoker, INotifyPropertyChanged
|
|
||||||
{
|
|
||||||
// see also notes in Libation/Source/_ARCHITECTURE NOTES.txt :: MVVM
|
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
|
||||||
|
|
||||||
// per standard INotifyPropertyChanged pattern:
|
|
||||||
// https://docs.microsoft.com/en-us/dotnet/desktop/wpf/data/how-to-implement-property-change-notification
|
|
||||||
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
|
||||||
=> this.UIThreadSync(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -34,6 +34,8 @@ namespace LibationWinForms.GridView
|
|||||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
|
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
|
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||||
gridEntryDataGridView = new System.Windows.Forms.DataGridView();
|
gridEntryDataGridView = new System.Windows.Forms.DataGridView();
|
||||||
|
showHideColumnsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components);
|
||||||
|
syncBindingSource = new SyncBindingSource(components);
|
||||||
removeGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
removeGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||||
liberateGVColumn = new LiberateDataGridViewImageButtonColumn();
|
liberateGVColumn = new LiberateDataGridViewImageButtonColumn();
|
||||||
coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
|
coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
|
||||||
@ -50,9 +52,8 @@ namespace LibationWinForms.GridView
|
|||||||
myRatingGVColumn = new MyRatingGridViewColumn();
|
myRatingGVColumn = new MyRatingGridViewColumn();
|
||||||
miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
lastDownloadedGVColumn = new LastDownloadedGridViewColumn();
|
lastDownloadedGVColumn = new LastDownloadedGridViewColumn();
|
||||||
|
isSpatialGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||||
tagAndDetailsGVColumn = new EditTagsDataGridViewImageButtonColumn();
|
tagAndDetailsGVColumn = new EditTagsDataGridViewImageButtonColumn();
|
||||||
showHideColumnsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components);
|
|
||||||
syncBindingSource = new SyncBindingSource(components);
|
|
||||||
((System.ComponentModel.ISupportInitialize)gridEntryDataGridView).BeginInit();
|
((System.ComponentModel.ISupportInitialize)gridEntryDataGridView).BeginInit();
|
||||||
((System.ComponentModel.ISupportInitialize)syncBindingSource).BeginInit();
|
((System.ComponentModel.ISupportInitialize)syncBindingSource).BeginInit();
|
||||||
SuspendLayout();
|
SuspendLayout();
|
||||||
@ -65,12 +66,12 @@ namespace LibationWinForms.GridView
|
|||||||
gridEntryDataGridView.AllowUserToResizeRows = false;
|
gridEntryDataGridView.AllowUserToResizeRows = false;
|
||||||
gridEntryDataGridView.AutoGenerateColumns = false;
|
gridEntryDataGridView.AutoGenerateColumns = false;
|
||||||
gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||||
gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { removeGVColumn, liberateGVColumn, coverGVColumn, titleGVColumn, authorsGVColumn, narratorsGVColumn, lengthGVColumn, seriesGVColumn, seriesOrderGVColumn, descriptionGVColumn, categoryGVColumn, productRatingGVColumn, purchaseDateGVColumn, myRatingGVColumn, miscGVColumn, lastDownloadedGVColumn, tagAndDetailsGVColumn });
|
gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { removeGVColumn, liberateGVColumn, coverGVColumn, titleGVColumn, authorsGVColumn, narratorsGVColumn, lengthGVColumn, seriesGVColumn, seriesOrderGVColumn, descriptionGVColumn, categoryGVColumn, productRatingGVColumn, purchaseDateGVColumn, myRatingGVColumn, miscGVColumn, lastDownloadedGVColumn, isSpatialGVColumn, tagAndDetailsGVColumn });
|
||||||
gridEntryDataGridView.ContextMenuStrip = showHideColumnsContextMenuStrip;
|
gridEntryDataGridView.ContextMenuStrip = showHideColumnsContextMenuStrip;
|
||||||
gridEntryDataGridView.DataSource = syncBindingSource;
|
gridEntryDataGridView.DataSource = syncBindingSource;
|
||||||
dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
||||||
dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window;
|
dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window;
|
||||||
dataGridViewCellStyle2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
dataGridViewCellStyle2.Font = new System.Drawing.Font("Segoe UI", 9F);
|
||||||
dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText;
|
dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||||
dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight;
|
dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight;
|
||||||
dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
|
dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
|
||||||
@ -84,11 +85,22 @@ namespace LibationWinForms.GridView
|
|||||||
gridEntryDataGridView.RowHeadersVisible = false;
|
gridEntryDataGridView.RowHeadersVisible = false;
|
||||||
gridEntryDataGridView.RowHeadersWidth = 82;
|
gridEntryDataGridView.RowHeadersWidth = 82;
|
||||||
gridEntryDataGridView.RowTemplate.Height = 82;
|
gridEntryDataGridView.RowTemplate.Height = 82;
|
||||||
gridEntryDataGridView.Size = new System.Drawing.Size(3140, 760);
|
gridEntryDataGridView.Size = new System.Drawing.Size(1992, 380);
|
||||||
gridEntryDataGridView.TabIndex = 0;
|
gridEntryDataGridView.TabIndex = 0;
|
||||||
gridEntryDataGridView.CellContentClick += DataGridView_CellContentClick;
|
gridEntryDataGridView.CellContentClick += DataGridView_CellContentClick;
|
||||||
gridEntryDataGridView.CellToolTipTextNeeded += gridEntryDataGridView_CellToolTipTextNeeded;
|
gridEntryDataGridView.CellToolTipTextNeeded += gridEntryDataGridView_CellToolTipTextNeeded;
|
||||||
//
|
//
|
||||||
|
// showHideColumnsContextMenuStrip
|
||||||
|
//
|
||||||
|
showHideColumnsContextMenuStrip.ImageScalingSize = new System.Drawing.Size(32, 32);
|
||||||
|
showHideColumnsContextMenuStrip.Name = "contextMenuStrip1";
|
||||||
|
showHideColumnsContextMenuStrip.ShowCheckMargin = true;
|
||||||
|
showHideColumnsContextMenuStrip.Size = new System.Drawing.Size(83, 4);
|
||||||
|
//
|
||||||
|
// syncBindingSource
|
||||||
|
//
|
||||||
|
syncBindingSource.DataSource = typeof(GridEntry);
|
||||||
|
//
|
||||||
// removeGVColumn
|
// removeGVColumn
|
||||||
//
|
//
|
||||||
removeGVColumn.DataPropertyName = "Remove";
|
removeGVColumn.DataPropertyName = "Remove";
|
||||||
@ -144,7 +156,6 @@ namespace LibationWinForms.GridView
|
|||||||
authorsGVColumn.MinimumWidth = 10;
|
authorsGVColumn.MinimumWidth = 10;
|
||||||
authorsGVColumn.Name = "authorsGVColumn";
|
authorsGVColumn.Name = "authorsGVColumn";
|
||||||
authorsGVColumn.ReadOnly = true;
|
authorsGVColumn.ReadOnly = true;
|
||||||
authorsGVColumn.Width = 100;
|
|
||||||
//
|
//
|
||||||
// narratorsGVColumn
|
// narratorsGVColumn
|
||||||
//
|
//
|
||||||
@ -153,7 +164,6 @@ namespace LibationWinForms.GridView
|
|||||||
narratorsGVColumn.MinimumWidth = 10;
|
narratorsGVColumn.MinimumWidth = 10;
|
||||||
narratorsGVColumn.Name = "narratorsGVColumn";
|
narratorsGVColumn.Name = "narratorsGVColumn";
|
||||||
narratorsGVColumn.ReadOnly = true;
|
narratorsGVColumn.ReadOnly = true;
|
||||||
narratorsGVColumn.Width = 100;
|
|
||||||
//
|
//
|
||||||
// lengthGVColumn
|
// lengthGVColumn
|
||||||
//
|
//
|
||||||
@ -163,7 +173,6 @@ namespace LibationWinForms.GridView
|
|||||||
lengthGVColumn.Name = "lengthGVColumn";
|
lengthGVColumn.Name = "lengthGVColumn";
|
||||||
lengthGVColumn.ReadOnly = true;
|
lengthGVColumn.ReadOnly = true;
|
||||||
lengthGVColumn.ToolTipText = "Recording Length";
|
lengthGVColumn.ToolTipText = "Recording Length";
|
||||||
lengthGVColumn.Width = 100;
|
|
||||||
//
|
//
|
||||||
// seriesGVColumn
|
// seriesGVColumn
|
||||||
//
|
//
|
||||||
@ -172,7 +181,6 @@ namespace LibationWinForms.GridView
|
|||||||
seriesGVColumn.MinimumWidth = 10;
|
seriesGVColumn.MinimumWidth = 10;
|
||||||
seriesGVColumn.Name = "seriesGVColumn";
|
seriesGVColumn.Name = "seriesGVColumn";
|
||||||
seriesGVColumn.ReadOnly = true;
|
seriesGVColumn.ReadOnly = true;
|
||||||
seriesGVColumn.Width = 100;
|
|
||||||
//
|
//
|
||||||
// seriesOrderGVColumn
|
// seriesOrderGVColumn
|
||||||
//
|
//
|
||||||
@ -192,7 +200,6 @@ namespace LibationWinForms.GridView
|
|||||||
descriptionGVColumn.MinimumWidth = 10;
|
descriptionGVColumn.MinimumWidth = 10;
|
||||||
descriptionGVColumn.Name = "descriptionGVColumn";
|
descriptionGVColumn.Name = "descriptionGVColumn";
|
||||||
descriptionGVColumn.ReadOnly = true;
|
descriptionGVColumn.ReadOnly = true;
|
||||||
descriptionGVColumn.Width = 100;
|
|
||||||
//
|
//
|
||||||
// categoryGVColumn
|
// categoryGVColumn
|
||||||
//
|
//
|
||||||
@ -201,7 +208,6 @@ namespace LibationWinForms.GridView
|
|||||||
categoryGVColumn.MinimumWidth = 10;
|
categoryGVColumn.MinimumWidth = 10;
|
||||||
categoryGVColumn.Name = "categoryGVColumn";
|
categoryGVColumn.Name = "categoryGVColumn";
|
||||||
categoryGVColumn.ReadOnly = true;
|
categoryGVColumn.ReadOnly = true;
|
||||||
categoryGVColumn.Width = 100;
|
|
||||||
//
|
//
|
||||||
// productRatingGVColumn
|
// productRatingGVColumn
|
||||||
//
|
//
|
||||||
@ -220,7 +226,6 @@ namespace LibationWinForms.GridView
|
|||||||
purchaseDateGVColumn.MinimumWidth = 10;
|
purchaseDateGVColumn.MinimumWidth = 10;
|
||||||
purchaseDateGVColumn.Name = "purchaseDateGVColumn";
|
purchaseDateGVColumn.Name = "purchaseDateGVColumn";
|
||||||
purchaseDateGVColumn.ReadOnly = true;
|
purchaseDateGVColumn.ReadOnly = true;
|
||||||
purchaseDateGVColumn.Width = 100;
|
|
||||||
//
|
//
|
||||||
// myRatingGVColumn
|
// myRatingGVColumn
|
||||||
//
|
//
|
||||||
@ -250,6 +255,17 @@ namespace LibationWinForms.GridView
|
|||||||
lastDownloadedGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
lastDownloadedGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||||
lastDownloadedGVColumn.Width = 108;
|
lastDownloadedGVColumn.Width = 108;
|
||||||
//
|
//
|
||||||
|
// isSpatialGVColumn
|
||||||
|
//
|
||||||
|
isSpatialGVColumn.DataPropertyName = "IsSpatial";
|
||||||
|
isSpatialGVColumn.HeaderText = "Is Spatial";
|
||||||
|
isSpatialGVColumn.MinimumWidth = 20;
|
||||||
|
isSpatialGVColumn.Name = "isSpatialGVColumn";
|
||||||
|
isSpatialGVColumn.ReadOnly = true;
|
||||||
|
isSpatialGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||||
|
isSpatialGVColumn.ToolTipText = "Indicates whether this title is available in Dolby Atmos \"spatial\" audio format. Note: Requires enabling \"Request Spatial Audio\" in Settings.";
|
||||||
|
isSpatialGVColumn.Width = 60;
|
||||||
|
//
|
||||||
// tagAndDetailsGVColumn
|
// tagAndDetailsGVColumn
|
||||||
//
|
//
|
||||||
tagAndDetailsGVColumn.DataPropertyName = "BookTags";
|
tagAndDetailsGVColumn.DataPropertyName = "BookTags";
|
||||||
@ -259,18 +275,6 @@ namespace LibationWinForms.GridView
|
|||||||
tagAndDetailsGVColumn.ReadOnly = true;
|
tagAndDetailsGVColumn.ReadOnly = true;
|
||||||
tagAndDetailsGVColumn.ScaleFactor = 0F;
|
tagAndDetailsGVColumn.ScaleFactor = 0F;
|
||||||
tagAndDetailsGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
tagAndDetailsGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||||
tagAndDetailsGVColumn.Width = 100;
|
|
||||||
//
|
|
||||||
// showHideColumnsContextMenuStrip
|
|
||||||
//
|
|
||||||
showHideColumnsContextMenuStrip.ImageScalingSize = new System.Drawing.Size(32, 32);
|
|
||||||
showHideColumnsContextMenuStrip.Name = "contextMenuStrip1";
|
|
||||||
showHideColumnsContextMenuStrip.ShowCheckMargin = true;
|
|
||||||
showHideColumnsContextMenuStrip.Size = new System.Drawing.Size(83, 4);
|
|
||||||
//
|
|
||||||
// syncBindingSource
|
|
||||||
//
|
|
||||||
syncBindingSource.DataSource = typeof(GridEntry);
|
|
||||||
//
|
//
|
||||||
// ProductsGrid
|
// ProductsGrid
|
||||||
//
|
//
|
||||||
@ -279,10 +283,10 @@ namespace LibationWinForms.GridView
|
|||||||
AutoScroll = true;
|
AutoScroll = true;
|
||||||
Controls.Add(gridEntryDataGridView);
|
Controls.Add(gridEntryDataGridView);
|
||||||
Name = "ProductsGrid";
|
Name = "ProductsGrid";
|
||||||
Size = new System.Drawing.Size(1570, 380);
|
Size = new System.Drawing.Size(1992, 380);
|
||||||
Load += new System.EventHandler(ProductsGrid_Load);
|
Load += ProductsGrid_Load;
|
||||||
((System.ComponentModel.ISupportInitialize)(gridEntryDataGridView)).EndInit();
|
((System.ComponentModel.ISupportInitialize)gridEntryDataGridView).EndInit();
|
||||||
((System.ComponentModel.ISupportInitialize)(syncBindingSource)).EndInit();
|
((System.ComponentModel.ISupportInitialize)syncBindingSource).EndInit();
|
||||||
ResumeLayout(false);
|
ResumeLayout(false);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -308,6 +312,7 @@ namespace LibationWinForms.GridView
|
|||||||
private MyRatingGridViewColumn myRatingGVColumn;
|
private MyRatingGridViewColumn myRatingGVColumn;
|
||||||
private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn;
|
private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn;
|
||||||
private LastDownloadedGridViewColumn lastDownloadedGVColumn;
|
private LastDownloadedGridViewColumn lastDownloadedGVColumn;
|
||||||
|
private System.Windows.Forms.DataGridViewCheckBoxColumn isSpatialGVColumn;
|
||||||
private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn;
|
private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -515,7 +515,6 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
//Restore Grid Display Settings
|
//Restore Grid Display Settings
|
||||||
var config = Configuration.Instance;
|
var config = Configuration.Instance;
|
||||||
var gridColumnsVisibilities = config.GridColumnsVisibilities;
|
|
||||||
var gridColumnsWidths = config.GridColumnsWidths;
|
var gridColumnsWidths = config.GridColumnsWidths;
|
||||||
var displayIndices = config.GridColumnsDisplayIndices;
|
var displayIndices = config.GridColumnsDisplayIndices;
|
||||||
|
|
||||||
@ -524,7 +523,7 @@ namespace LibationWinForms.GridView
|
|||||||
foreach (DataGridViewColumn column in gridEntryDataGridView.Columns)
|
foreach (DataGridViewColumn column in gridEntryDataGridView.Columns)
|
||||||
{
|
{
|
||||||
var itemName = column.DataPropertyName;
|
var itemName = column.DataPropertyName;
|
||||||
var visible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
|
var visible = config.GetColumnVisibility(itemName);
|
||||||
|
|
||||||
var menuItem = new ToolStripMenuItem(column.HeaderText)
|
var menuItem = new ToolStripMenuItem(column.HeaderText)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing"">Blue</data>
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
</data>
|
</data>
|
||||||
@ -120,6 +120,9 @@
|
|||||||
<metadata name="removeGVColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
<metadata name="removeGVColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||||
<value>True</value>
|
<value>True</value>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
<metadata name="isSpatialGVColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||||
|
<value>True</value>
|
||||||
|
</metadata>
|
||||||
<metadata name="showHideColumnsContextMenuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
<metadata name="showHideColumnsContextMenuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||||
<value>171, 17</value>
|
<value>171, 17</value>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
|||||||
@ -40,7 +40,6 @@
|
|||||||
this.runningTimeLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
this.runningTimeLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||||
this.tabControl1 = new System.Windows.Forms.TabControl();
|
this.tabControl1 = new System.Windows.Forms.TabControl();
|
||||||
this.tabPage1 = new System.Windows.Forms.TabPage();
|
this.tabPage1 = new System.Windows.Forms.TabPage();
|
||||||
this.panel3 = new System.Windows.Forms.Panel();
|
|
||||||
this.virtualFlowControl2 = new LibationWinForms.ProcessQueue.VirtualFlowControl();
|
this.virtualFlowControl2 = new LibationWinForms.ProcessQueue.VirtualFlowControl();
|
||||||
this.panel1 = new System.Windows.Forms.Panel();
|
this.panel1 = new System.Windows.Forms.Panel();
|
||||||
this.label1 = new System.Windows.Forms.Label();
|
this.label1 = new System.Windows.Forms.Label();
|
||||||
@ -134,7 +133,6 @@
|
|||||||
//
|
//
|
||||||
// tabPage1
|
// tabPage1
|
||||||
//
|
//
|
||||||
this.tabPage1.Controls.Add(this.panel3);
|
|
||||||
this.tabPage1.Controls.Add(this.virtualFlowControl2);
|
this.tabPage1.Controls.Add(this.virtualFlowControl2);
|
||||||
this.tabPage1.Controls.Add(this.panel1);
|
this.tabPage1.Controls.Add(this.panel1);
|
||||||
this.tabPage1.Location = new System.Drawing.Point(4, 24);
|
this.tabPage1.Location = new System.Drawing.Point(4, 24);
|
||||||
@ -145,14 +143,6 @@
|
|||||||
this.tabPage1.Text = "Process Queue";
|
this.tabPage1.Text = "Process Queue";
|
||||||
this.tabPage1.UseVisualStyleBackColor = true;
|
this.tabPage1.UseVisualStyleBackColor = true;
|
||||||
//
|
//
|
||||||
// panel3
|
|
||||||
//
|
|
||||||
this.panel3.Dock = System.Windows.Forms.DockStyle.Bottom;
|
|
||||||
this.panel3.Location = new System.Drawing.Point(3, 422);
|
|
||||||
this.panel3.Name = "panel3";
|
|
||||||
this.panel3.Size = new System.Drawing.Size(390, 5);
|
|
||||||
this.panel3.TabIndex = 4;
|
|
||||||
//
|
|
||||||
// virtualFlowControl2
|
// virtualFlowControl2
|
||||||
//
|
//
|
||||||
this.virtualFlowControl2.AccessibleRole = System.Windows.Forms.AccessibleRole.None;
|
this.virtualFlowControl2.AccessibleRole = System.Windows.Forms.AccessibleRole.None;
|
||||||
@ -174,14 +164,14 @@
|
|||||||
this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom;
|
this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||||
this.panel1.Location = new System.Drawing.Point(3, 427);
|
this.panel1.Location = new System.Drawing.Point(3, 427);
|
||||||
this.panel1.Name = "panel1";
|
this.panel1.Name = "panel1";
|
||||||
this.panel1.Size = new System.Drawing.Size(390, 25);
|
this.panel1.Size = new System.Drawing.Size(390, 29);
|
||||||
this.panel1.TabIndex = 2;
|
this.panel1.TabIndex = 2;
|
||||||
//
|
//
|
||||||
// label1
|
// label1
|
||||||
//
|
//
|
||||||
this.label1.Anchor = System.Windows.Forms.AnchorStyles.Right;
|
this.label1.Anchor = System.Windows.Forms.AnchorStyles.Right;
|
||||||
this.label1.AutoSize = true;
|
this.label1.AutoSize = true;
|
||||||
this.label1.Location = new System.Drawing.Point(148, 4);
|
this.label1.Location = new System.Drawing.Point(148, 6);
|
||||||
this.label1.Name = "label1";
|
this.label1.Name = "label1";
|
||||||
this.label1.Size = new System.Drawing.Size(54, 15);
|
this.label1.Size = new System.Drawing.Size(54, 15);
|
||||||
this.label1.TabIndex = 5;
|
this.label1.TabIndex = 5;
|
||||||
@ -196,7 +186,7 @@
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
65536});
|
65536});
|
||||||
this.numericUpDown1.Location = new System.Drawing.Point(208, 0);
|
this.numericUpDown1.Location = new System.Drawing.Point(208, 2);
|
||||||
this.numericUpDown1.Maximum = new decimal(new int[] {
|
this.numericUpDown1.Maximum = new decimal(new int[] {
|
||||||
999,
|
999,
|
||||||
0,
|
0,
|
||||||
@ -348,7 +338,6 @@
|
|||||||
this.panel2.ResumeLayout(false);
|
this.panel2.ResumeLayout(false);
|
||||||
this.ResumeLayout(false);
|
this.ResumeLayout(false);
|
||||||
this.PerformLayout();
|
this.PerformLayout();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -367,7 +356,6 @@
|
|||||||
private System.Windows.Forms.ToolStripStatusLabel queueNumberLbl;
|
private System.Windows.Forms.ToolStripStatusLabel queueNumberLbl;
|
||||||
private System.Windows.Forms.ToolStripStatusLabel completedNumberLbl;
|
private System.Windows.Forms.ToolStripStatusLabel completedNumberLbl;
|
||||||
private System.Windows.Forms.ToolStripStatusLabel errorNumberLbl;
|
private System.Windows.Forms.ToolStripStatusLabel errorNumberLbl;
|
||||||
private System.Windows.Forms.Panel panel3;
|
|
||||||
private System.Windows.Forms.Panel panel4;
|
private System.Windows.Forms.Panel panel4;
|
||||||
private System.Windows.Forms.ToolStripStatusLabel runningTimeLbl;
|
private System.Windows.Forms.ToolStripStatusLabel runningTimeLbl;
|
||||||
private System.Windows.Forms.DataGridView logDGV;
|
private System.Windows.Forms.DataGridView logDGV;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using LibationFileManager;
|
using LibationUiBase;
|
||||||
using LibationUiBase;
|
|
||||||
using LibationUiBase.ProcessQueue;
|
using LibationUiBase.ProcessQueue;
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
@ -35,23 +34,18 @@ internal partial class ProcessQueueControl : UserControl
|
|||||||
|
|
||||||
ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
|
ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
|
||||||
ViewModel.LogEntries.CollectionChanged += LogEntries_CollectionChanged;
|
ViewModel.LogEntries.CollectionChanged += LogEntries_CollectionChanged;
|
||||||
|
ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogEntries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
private void LogEntries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!IsDisposed && e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
|
if (!IsDisposed && e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
|
||||||
{
|
{
|
||||||
foreach(var entry in e.NewItems?.OfType<LogEntry>() ?? [])
|
foreach (var entry in e.NewItems?.OfType<LogEntry>() ?? [])
|
||||||
logDGV.Rows.Add(entry.LogDate, entry.LogMessage);
|
logDGV.Rows.Add(entry.LogDate, entry.LogMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnLoad(EventArgs e)
|
|
||||||
{
|
|
||||||
if (DesignMode) return;
|
|
||||||
ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void cancelAllBtn_Click(object? sender, EventArgs e)
|
private async void cancelAllBtn_Click(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
ViewModel.Queue.ClearQueue();
|
ViewModel.Queue.ClearQueue();
|
||||||
@ -155,7 +149,7 @@ internal partial class ProcessQueueControl : UserControl
|
|||||||
ViewModel.Queue.MoveQueuePosition(item, position.Value);
|
ViewModel.Queue.MoveQueuePosition(item, position.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Serilog.Log.Logger.Error(ex, "Error handling button click from queued item");
|
Serilog.Log.Logger.Error(ex, "Error handling button click from queued item");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,8 +39,19 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
|
|
||||||
public void RefreshDisplay()
|
public void RefreshDisplay()
|
||||||
{
|
{
|
||||||
AdjustScrollBar();
|
if (InvokeRequired)
|
||||||
DoVirtualScroll();
|
{
|
||||||
|
Invoke((MethodInvoker)delegate
|
||||||
|
{
|
||||||
|
AdjustScrollBar();
|
||||||
|
DoVirtualScroll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AdjustScrollBar();
|
||||||
|
DoVirtualScroll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Dynamic Properties
|
#region Dynamic Properties
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user