Compare commits

...

54 Commits

Author SHA1 Message Date
Robert McRackan
1ee73fa1a7 increment version 2022-05-09 07:58:01 -04:00
Robert McRackan
adbbff368f Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-05-09 07:56:25 -04:00
Robert McRackan
ee9d30bd56 Michael's "frame" fix from email 2022-05-09 07:56:16 -04:00
rmcrackan
5a822809a9 Merge pull request #239 from Mbucari/master
Fix temp file storage/deletion + a few new features:

* .tmp and .json files are again properly stored in DownloadsInProgressDirectory, and the are again used if resuming a download.
* Yellow stoplight works again.
* All audio files stored in DecryptInProgressDirectory are deleted if the conversion fails Partial / Complete files in %temp%\Libation are not tidied / reused #144
* Added option to retain aax file Feature Request: Allow saving the original aax #187
* Add option to remove "(Unabridged)" from metadata tags Remove "(Unabridged)" from the title? #223
* Added options for mp3 encoding
* Add option to create cue sheet
* Prevent same book being decrypted more than once at a time
* Made a new settings tab for all audio file editing/fixing options.
2022-05-08 22:13:47 -04:00
Michael Bucari-Tovo
4e587e0429 Add try block 2022-05-08 19:58:48 -06:00
Michael Bucari-Tovo
9a619186fd If keeping aaxc, write aaxc key to file 2022-05-08 19:56:59 -06:00
Michael Bucari-Tovo
eab6f71a4c Don't delete temp aaxc file if download failed. 2022-05-08 17:07:10 -06:00
Michael Bucari-Tovo
f68bf2d6b3 Better method for downloading only 1 book at a time. 2022-05-08 16:48:58 -06:00
Michael Bucari-Tovo
2afcaebb78 Prevent same bok being decrypted more than once at a time 2022-05-08 16:31:24 -06:00
Michael Bucari-Tovo
458ea6a377 Fix tmp file extension to aaxc 2022-05-08 16:13:35 -06:00
Michael Bucari-Tovo
0e2997d309 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-08 15:46:57 -06:00
Michael Bucari-Tovo
420f4b9d5d Add optional cue sheet 2022-05-08 15:46:33 -06:00
Mbucari
bcfa97219f Merge branch 'rmcrackan:master' into master 2022-05-08 14:49:07 -06:00
Michael Bucari-Tovo
4c66010afe Add user settings for mp3 encoding. 2022-05-08 14:48:43 -06:00
Michael Bucari-Tovo
05f25a88c6 Fix temp file reuse/cleanup. Add retain aax option. 2022-05-08 11:08:23 -06:00
rmcrackan
2c6c08fbb5 Merge pull request #238 from Mbucari/master
Add option to remove Audible branding audio
2022-05-08 12:57:21 -04:00
Michael Bucari-Tovo
8af60b56b6 Refactoring for clarity. 2022-05-08 09:40:21 -06:00
Michael Bucari-Tovo
c0516772a7 Move OutputFormat to DownloadLicense 2022-05-08 09:40:08 -06:00
Michael Bucari-Tovo
77de70762c Add default for StripAudibleBrandAudio option 2022-05-08 09:39:55 -06:00
Michael Bucari-Tovo
7164100cb1 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-07 23:46:55 -06:00
Michael Bucari-Tovo
9292a62015 Fix mutually exclusive setting options 2022-05-07 23:44:28 -06:00
Mbucari
5280e68da9 Update AaxDecrypter.csproj 2022-05-07 23:23:31 -06:00
Michael Bucari-Tovo
0f6b0bf9fe Add trim audio support to multipart files. 2022-05-07 22:34:28 -06:00
Michael Bucari-Tovo
510ed95590 Remove 500ms headroom 2022-05-07 21:42:23 -06:00
Michael Bucari-Tovo
9862593f4a Add option to strip Audible brand audio 2022-05-07 21:29:29 -06:00
Michael Bucari-Tovo
d595b62f13 Update aaxclean 2022-05-06 17:16:14 -06:00
Robert McRackan
12abbb79b1 Don't allow multiple simultaneous imports 2022-05-06 16:00:37 -04:00
Robert McRackan
ecaa3b9aab Scanning no longer blocks UI 2022-05-04 17:15:28 -04:00
Robert McRackan
ded175f2d2 Extract 'sortable' formatting from GridEntry 2022-05-04 13:50:23 -04:00
Robert McRackan
128facec21 add/modify grid without reload 2022-05-04 13:37:25 -04:00
Robert McRackan
0bde86ebfd QuickFilters on save event 2022-05-03 11:29:21 -04:00
Robert McRackan
28625029cd code clean up 2022-05-02 16:13:35 -04:00
Robert McRackan
1816bd721c Added initial 'About' menu with version number 2022-05-02 14:59:43 -04:00
Robert McRackan
68ad627159 update dependencies 2022-04-30 21:03:40 -04:00
Robert McRackan
878a5dd36c Libary import got a complete overhaul. On a library of 1,200 titles: initial scan is 80-85% faster. Subsequent imports are 60-70% faster 2022-04-29 16:35:49 -04:00
Robert McRackan
7c144b8277 Bug fix #234 : chapters were no longer included in the m4b file 2022-04-27 11:31:13 -04:00
Robert McRackan
bca8c3865b Expose a way to insert ad hoc library books to grid 2022-04-26 16:37:13 -04:00
Robert McRackan
58102acd35 Trivial refactoring 2022-04-26 16:34:59 -04:00
Robert McRackan
5e577843f7 Fixing genre metatag is conditional upon AllowLibationFixup setting 2022-04-26 09:49:21 -04:00
Robert McRackan
e1d549cead update dependencies 2022-04-26 09:27:13 -04:00
Robert McRackan
323b8f2fb9 minor refactors 2022-04-26 08:18:35 -04:00
Robert McRackan
3dcbcf42ed Fix 2 for issue #202 2022-04-25 22:21:36 -04:00
Robert McRackan
825078abc6 New feature: metadata correction in converted output files 2022-04-25 13:34:22 -04:00
Robert McRackan
6be44966ad * enhancement #202 : use audible category for file's genre metatag. Thanks @MBucari ! 2022-04-25 13:23:43 -04:00
rmcrackan
66da138556 Merge pull request #233 from Mbucari/master
Update Libation to work with new AAXClean.Codecs
2022-04-25 13:16:38 -04:00
Michael Bucari-Tovo
e5dd4b856e Update Libation to work with new AAXClean.Codecs 2022-04-24 19:40:34 -06:00
Robert McRackan
5caa9c5687 Improved logging for import errors 2022-04-16 16:36:49 -04:00
Robert McRackan
c8c0ffeb0d Bug fix #231 : Some books without categories are not getting imported 2022-04-15 16:30:43 -04:00
Robert McRackan
bfceb58d6b Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-04-12 09:16:10 -04:00
Robert McRackan
2e4c4cf5f7 bug fix #228 : recover from corrupt BookTags.json 2022-04-12 09:16:02 -04:00
rmcrackan
23966c9b00 Update README.md 2022-04-11 10:06:12 -04:00
rmcrackan
ef73d2243d add login details 2022-04-11 10:03:08 -04:00
Robert McRackan
c95feebd39 Add images for 'alternate login' readme 2022-04-11 09:56:59 -04:00
Robert McRackan
d6601fed83 Bug fix #225 : SaferEnumerateFiles will skip files with UnauthorizedAccessException 2022-04-08 09:11:36 -04:00
99 changed files with 2521 additions and 1781 deletions

View File

@@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean" Version="0.1.10" />
<PackageReference Include="AAXClean" Version="0.4.6" />
<PackageReference Include="AAXClean.Codecs" Version="0.2.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -6,33 +6,62 @@ namespace AaxDecrypter
{
public abstract class AaxcDownloadConvertBase : AudiobookDownloadBase
{
protected OutputFormat OutputFormat { get; }
public event EventHandler<AppleTags> RetrievedMetadata;
protected AaxFile AaxFile;
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
: base(outFileName, cacheDirectory, dlLic)
{
OutputFormat = outputFormat;
}
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, DownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic) { }
/// <summary>Setting cover art by this method will insert the art into the audiobook metadata</summary>
public override void SetCoverArt(byte[] coverArt)
{
base.SetCoverArt(coverArt);
if (coverArt is not null)
AaxFile?.AppleTags.SetCoverArt(coverArt);
if (coverArt is not null && AaxFile?.AppleTags is not null)
AaxFile.AppleTags.Cover = coverArt;
}
protected bool Step_GetMetadata()
{
AaxFile = new AaxFile(InputFileStream);
if (DownloadOptions.StripUnabridged)
{
AaxFile.AppleTags.Title = AaxFile.AppleTags.TitleSansUnabridged;
AaxFile.AppleTags.Album = AaxFile.AppleTags.Album?.Replace(" (Unabridged)", "");
}
//Finishing configuring lame encoder.
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
{
double bitrateMultiple = 1;
if (AaxFile.AudioChannels == 2)
{
if (DownloadOptions.Downsample)
bitrateMultiple = 0.5;
else
DownloadOptions.LameConfig.Mode = NAudio.Lame.MPEGMode.Stereo;
}
if (DownloadOptions.MatchSourceBitrate)
{
int kbps = (int)(AaxFile.AverageBitrate * bitrateMultiple / 1024);
if (DownloadOptions.LameConfig.VBR is null)
DownloadOptions.LameConfig.BitRate = kbps;
else if (DownloadOptions.LameConfig.VBR == NAudio.Lame.VBRMode.ABR)
DownloadOptions.LameConfig.ABRRateKbps = kbps;
}
}
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
OnRetrievedAuthors(AaxFile.AppleTags.FirstAuthor ?? "[unknown]");
OnRetrievedNarrators(AaxFile.AppleTags.Narrator ?? "[unknown]");
OnRetrievedCoverArt(AaxFile.AppleTags.Cover);
RetrievedMetadata?.Invoke(this, AaxFile.AppleTags);
return !IsCanceled;
}
@@ -47,7 +76,7 @@ namespace AaxDecrypter
OnDecryptProgressUpdate(zeroProgress);
AaxFile.SetDecryptionKey(DownloadLicense.AudibleKey, DownloadLicense.AudibleIV);
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
return zeroProgress;
}
@@ -69,7 +98,7 @@ namespace AaxDecrypter
if (double.IsNormal(estTimeRemaining))
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
var progressPercent = e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
var progressPercent = (e.ProcessPosition / e.TotalDuration);
OnDecryptProgressUpdate(
new DownloadProgress

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using AAXClean;
using AAXClean.Codecs;
using Dinah.Core.StepRunner;
using FileManager;
@@ -17,13 +18,13 @@ namespace AaxDecrypter
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
private List<string> multiPartFilePaths { get; } = new List<string>();
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat,
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, DownloadOptions dlLic,
Func<MultiConvertFileProperties, string> multipartFileNameCallback = null)
: base(outFileName, cacheDirectory, dlLic, outputFormat)
: base(outFileName, cacheDirectory, dlLic)
{
Steps = new StepSequence
{
Name = "Download and Convert Aaxc To " + OutputFormat,
Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat,
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsMultipleFilesPerChapter,
@@ -60,10 +61,10 @@ That naming may not be desirable for everyone, but it's an easy change to instea
{
var zeroProgress = Step_DownloadAudiobook_Start();
var chapters = DownloadLicense.ChapterInfo.Chapters.ToList();
var chapters = DownloadOptions.ChapterInfo.Chapters.ToList();
// Ensure split files are at least minChapterLength in duration.
var splitChapters = new ChapterInfo();
var splitChapters = new ChapterInfo(DownloadOptions.ChapterInfo.StartOffset);
var runningTotal = TimeSpan.Zero;
string title = "";
@@ -85,40 +86,36 @@ That naming may not be desirable for everyone, but it's an easy change to instea
// reset, just in case
multiPartFilePaths.Clear();
ConversionResult result;
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
if (OutputFormat == OutputFormat.M4b)
ConvertToMultiMp4a(splitChapters);
if (DownloadOptions.OutputFormat == OutputFormat.M4b)
result = ConvertToMultiMp4a(splitChapters);
else
ConvertToMultiMp3(splitChapters);
result = ConvertToMultiMp3(splitChapters);
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
Step_DownloadAudiobook_End(zeroProgress);
var success = !IsCanceled;
if (success)
foreach (var path in multiPartFilePaths)
OnFileCreated(path);
return success;
return result == ConversionResult.NoErrorsDetected;
}
private void ConvertToMultiMp4a(ChapterInfo splitChapters)
private ConversionResult ConvertToMultiMp4a(ChapterInfo splitChapters)
{
var chapterCount = 0;
AaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback)
);
return AaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback),
DownloadOptions.TrimOutputToChapterLength);
}
private void ConvertToMultiMp3(ChapterInfo splitChapters)
private ConversionResult ConvertToMultiMp3(ChapterInfo splitChapters)
{
var chapterCount = 0;
AaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
return AaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
{
createOutputFileStream(++chapterCount, splitChapters, newSplitCallback);
newSplitCallback.LameConfig.ID3.Track = chapterCount.ToString();
});
((NAudio.Lame.LameConfig)newSplitCallback.UserState).ID3.Track = chapterCount.ToString();
}, DownloadOptions.LameConfig, DownloadOptions.TrimOutputToChapterLength);
}
private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
@@ -137,6 +134,8 @@ That naming may not be desirable for everyone, but it's an easy change to instea
FileUtility.SaferDelete(fileName);
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
OnFileCreated(fileName);
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using AAXClean;
using AAXClean.Codecs;
using Dinah.Core.StepRunner;
using FileManager;
@@ -10,12 +11,12 @@ namespace AaxDecrypter
{
protected override StepSequence Steps { get; }
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
: base(outFileName, cacheDirectory, dlLic, outputFormat)
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, DownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic)
{
Steps = new StepSequence
{
Name = "Download and Convert Aaxc To " + OutputFormat,
Name = "Download and Convert Aaxc To " + DownloadOptions.OutputFormat,
["Step 1: Get Aaxc Metadata"] = Step_GetMetadata,
["Step 2: Download Decrypted Audiobook"] = Step_DownloadAudiobookAsSingleFile,
@@ -31,15 +32,16 @@ namespace AaxDecrypter
FileUtility.SaferDelete(OutputFileName);
var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
OnFileCreated(OutputFileName);
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
var decryptionResult
= OutputFormat == OutputFormat.M4b
? AaxFile.ConvertToMp4a(outputFile, DownloadLicense.ChapterInfo)
: AaxFile.ConvertToMp3(outputFile);
= DownloadOptions.OutputFormat == OutputFormat.M4b
? AaxFile.ConvertToMp4a(outputFile, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength)
: AaxFile.ConvertToMp3(outputFile, DownloadOptions.LameConfig, DownloadOptions.ChapterInfo, DownloadOptions.TrimOutputToChapterLength);
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
DownloadLicense.ChapterInfo = AaxFile.Chapters;
DownloadOptions.ChapterInfo = AaxFile.Chapters;
Step_DownloadAudiobook_End(zeroProgress);

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dinah.Core;
using Dinah.Core.Net.Http;
@@ -20,8 +21,9 @@ namespace AaxDecrypter
public event EventHandler<string> FileCreated;
protected bool IsCanceled { get; set; }
protected string OutputFileName { get; private set; }
protected DownloadLicense DownloadLicense { get; }
protected DownloadOptions DownloadOptions { get; }
protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream;
// Don't give the property a 'set'. This should have to be an obvious choice; not accidental
@@ -31,9 +33,9 @@ namespace AaxDecrypter
private NetworkFileStreamPersister nfsPersister;
private string jsonDownloadState { get; }
private string tempFile => Path.ChangeExtension(jsonDownloadState, ".tmp");
public string TempFilePath { get; }
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
protected AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadOptions dlLic)
{
OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
@@ -43,9 +45,11 @@ namespace AaxDecrypter
if (!Directory.Exists(cacheDirectory))
throw new DirectoryNotFoundException($"Directory does not exist: {nameof(cacheDirectory)}");
jsonDownloadState = Path.Combine(cacheDirectory, Path.ChangeExtension(OutputFileName, ".json"));
DownloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
jsonDownloadState = Path.Combine(cacheDirectory, Path.GetFileName(Path.ChangeExtension(OutputFileName, ".json")));
TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
DownloadOptions = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
// delete file after validation is complete
FileUtility.SaferDelete(OutputFileName);
@@ -67,16 +71,18 @@ namespace AaxDecrypter
Serilog.Log.Logger.Error("Conversion failed");
return IsSuccess;
}
}
protected void OnRetrievedTitle(string title)
protected void OnRetrievedTitle(string title)
=> RetrievedTitle?.Invoke(this, title);
protected void OnRetrievedAuthors(string authors)
protected void OnRetrievedAuthors(string authors)
=> RetrievedAuthors?.Invoke(this, authors);
protected void OnRetrievedNarrators(string narrators)
protected void OnRetrievedNarrators(string narrators)
=> RetrievedNarrators?.Invoke(this, narrators);
protected void OnRetrievedCoverArt(byte[] coverArt)
protected void OnRetrievedCoverArt(byte[] coverArt)
=> RetrievedCoverArt?.Invoke(this, coverArt);
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
@@ -92,12 +98,14 @@ namespace AaxDecrypter
protected bool Step_CreateCue()
{
if (!DownloadOptions.CreateCueSheet) return true;
// not a critical step. its failure should not prevent future steps from running
try
{
var path = Path.ChangeExtension(OutputFileName, ".cue");
path = FileUtility.GetValidFilename(path);
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo));
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo));
OnFileCreated(path);
}
catch (Exception ex)
@@ -109,9 +117,36 @@ namespace AaxDecrypter
protected bool Step_Cleanup()
{
FileUtility.SaferDelete(jsonDownloadState);
FileUtility.SaferDelete(tempFile);
return !IsCanceled;
bool success = !IsCanceled;
if (success)
{
FileUtility.SaferDelete(jsonDownloadState);
if (DownloadOptions.AudibleKey is not null &&
DownloadOptions.AudibleIV is not null &&
DownloadOptions.RetainEncryptedFile)
{
string aaxPath = Path.ChangeExtension(TempFilePath, ".aax");
FileUtility.SaferMove(TempFilePath, aaxPath);
//Write aax decryption key
string keyPath = Path.ChangeExtension(aaxPath, ".key");
FileUtility.SaferDelete(keyPath);
File.WriteAllText(keyPath, $"Key={DownloadOptions.AudibleKey}\r\nIV={DownloadOptions.AudibleIV}");
OnFileCreated(aaxPath);
OnFileCreated(keyPath);
}
else
FileUtility.SaferDelete(TempFilePath);
}
else
{
FileUtility.SaferDelete(OutputFileName);
}
return success;
}
private NetworkFileStreamPersister OpenNetworkFileStream()
@@ -124,13 +159,13 @@ namespace AaxDecrypter
var nfsp = new NetworkFileStreamPersister(jsonDownloadState);
// If More than ~1 hour has elapsed since getting the download url, it will expire.
// The new url will be to the same file.
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadLicense.DownloadUrl));
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl));
return nfsp;
}
catch
{
FileUtility.SaferDelete(jsonDownloadState);
FileUtility.SaferDelete(tempFile);
FileUtility.SaferDelete(TempFilePath);
return NewNetworkFilePersister();
}
}
@@ -139,10 +174,10 @@ namespace AaxDecrypter
{
var headers = new System.Net.WebHeaderCollection
{
{ "User-Agent", DownloadLicense.UserAgent }
{ "User-Agent", DownloadOptions.UserAgent }
};
var networkFileStream = new NetworkFileStream(tempFile, new Uri(DownloadLicense.DownloadUrl), 0, headers);
var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, headers);
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
}

View File

@@ -14,14 +14,17 @@ namespace AaxDecrypter
stringBuilder.AppendLine(GetFileLine(filePath, "MP3"));
var startOffset = chapters.StartOffset;
var trackCount = 0;
foreach (var c in chapters.Chapters)
{
var startTime = c.StartOffset - startOffset;
trackCount++;
stringBuilder.AppendLine($"TRACK {trackCount} AUDIO");
stringBuilder.AppendLine($" TITLE \"{c.Title}\"");
stringBuilder.AppendLine($" INDEX 01 {(int)c.StartOffset.TotalMinutes}:{c.StartOffset:ss\\:ff}");
stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss}:{(int)(startTime.Milliseconds / 1000d * 75)}");
}
return stringBuilder.ToString();

View File

@@ -1,24 +0,0 @@
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
{
public class DownloadLicense
{
public string DownloadUrl { get; }
public string AudibleKey { get; }
public string AudibleIV { get; }
public string UserAgent { get; }
public ChapterInfo ChapterInfo { get; set; }
public DownloadLicense(string downloadUrl, string audibleKey, string audibleIV, string userAgent)
{
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
// no null/empty check. unencrypted files do not have these
AudibleKey = audibleKey;
AudibleIV = audibleIV;
}
}
}

View File

@@ -0,0 +1,30 @@
using AAXClean;
using Dinah.Core;
namespace AaxDecrypter
{
public class DownloadOptions
{
public string DownloadUrl { get; }
public string UserAgent { get; }
public string AudibleKey { get; init; }
public string AudibleIV { get; init; }
public OutputFormat OutputFormat { get; init; }
public bool TrimOutputToChapterLength { get; init; }
public bool RetainEncryptedFile { get; init; }
public bool StripUnabridged { get; init; }
public bool CreateCueSheet { get; init; }
public ChapterInfo ChapterInfo { get; set; }
public NAudio.Lame.LameConfig LameConfig { get; set; }
public bool Downsample { get; set; }
public bool MatchSourceBitrate { get; set; }
public DownloadOptions(string downloadUrl, string userAgent)
{
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
// no null/empty check for key/iv. unencrypted files do not have them
}
}
}

View File

@@ -10,7 +10,7 @@ namespace AaxDecrypter
{
protected override StepSequence Steps { get; }
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadLicense dlLic)
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadOptions dlLic)
: base(outFileName, cacheDirectory, dlLic)
{
Steps = new StepSequence

View File

@@ -2,8 +2,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>6.7.4.1</Version>
<TargetFramework>net6.0-windows</TargetFramework>
<Version>7.2.0.1</Version>
</PropertyGroup>
<ItemGroup>
@@ -11,7 +11,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Octokit" Version="0.50.0" />
<PackageReference Include="Octokit" Version="0.51.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -66,20 +66,53 @@ namespace AppScaffolding
if (!config.Exists(nameof(config.AllowLibationFixup)))
config.AllowLibationFixup = true;
if (!config.Exists(nameof(config.CreateCueSheet)))
config.CreateCueSheet = true;
if (!config.Exists(nameof(config.RetainAaxFile)))
config.RetainAaxFile = false;
if (!config.Exists(nameof(config.SplitFilesByChapter)))
config.SplitFilesByChapter = false;
if (!config.Exists(nameof(config.StripUnabridged)))
config.StripUnabridged = false;
if (!config.Exists(nameof(config.StripAudibleBrandAudio)))
config.StripAudibleBrandAudio = false;
if (!config.Exists(nameof(config.DecryptToLossy)))
config.DecryptToLossy = false;
if (!config.Exists(nameof(config.LameTargetBitrate)))
config.LameTargetBitrate = false;
if (!config.Exists(nameof(config.LameDownsampleMono)))
config.LameDownsampleMono = true;
if (!config.Exists(nameof(config.LameBitrate)))
config.LameBitrate = 64;
if (!config.Exists(nameof(config.LameConstantBitrate)))
config.LameConstantBitrate = false;
if (!config.Exists(nameof(config.LameMatchSourceBR)))
config.LameMatchSourceBR = true;
if (!config.Exists(nameof(config.LameVBRQuality)))
config.LameVBRQuality = 2;
if (!config.Exists(nameof(config.BadBook)))
config.BadBook = Configuration.BadBookAction.Ask;
if (!config.Exists(nameof(config.DownloadEpisodes)))
config.DownloadEpisodes = true;
if (!config.Exists(nameof(config.ShowImportedStats)))
config.ShowImportedStats = true;
if (!config.Exists(nameof(config.ImportEpisodes)))
config.ImportEpisodes = true;
if (!config.Exists(nameof(config.SplitFilesByChapter)))
config.SplitFilesByChapter = false;
if (!config.Exists(nameof(config.DownloadEpisodes)))
config.DownloadEpisodes = true;
if (!config.Exists(nameof(config.FolderTemplate)))
config.FolderTemplate = Templates.Folder.DefaultTemplate;
@@ -89,9 +122,6 @@ namespace AppScaffolding
if (!config.Exists(nameof(config.ChapterFileTemplate)))
config.ChapterFileTemplate = Templates.ChapterFile.DefaultTemplate;
if (!config.Exists(nameof(config.ShowImportedStats)))
config.ShowImportedStats = true;
}
/// <summary>Initialize logging. Run after migration</summary>
@@ -104,7 +134,7 @@ namespace AppScaffolding
private static void ensureSerilogConfig(Configuration config)
{
if (config.GetObject("Serilog") != null)
if (config.GetObject("Serilog") is not null)
return;
var serilogObj = new JObject
@@ -230,10 +260,10 @@ namespace AppScaffolding
config.InProgress,
AudibleFileStorage.DownloadsInProgressDirectory,
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
DownloadsInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
AudibleFileStorage.DecryptInProgressDirectory,
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
DecryptInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
});
}

View File

@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="27.2.1" />
<PackageReference Include="NPOI" Version="2.5.5" />
<PackageReference Include="NPOI" Version="2.5.6" />
</ItemGroup>
<ItemGroup>

View File

@@ -15,7 +15,19 @@ namespace ApplicationServices
{
public static class LibraryCommands
{
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, List<LibraryBook> existingLibrary, params Account[] accounts)
public static event EventHandler<int> ScanBegin;
public static event EventHandler ScanEnd;
public static bool Scanning { get; private set; }
private static object _lock { get; } = new();
static LibraryCommands()
{
ScanBegin += (_, __) => Scanning = true;
ScanEnd += (_, __) => Scanning = false;
}
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, List<LibraryBook> existingLibrary, params Account[] accounts)
{
logRestart();
@@ -82,6 +94,13 @@ namespace ApplicationServices
try
{
lock (_lock)
{
if (Scanning)
return (0, 0);
ScanBegin?.Invoke(null, accounts.Length);
}
logTime($"pre {nameof(scanAccountsAsync)} all");
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
logTime($"post {nameof(scanAccountsAsync)} all");
@@ -89,6 +108,9 @@ namespace ApplicationServices
var totalCount = importItems.Count;
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
if (totalCount == 0)
return default;
Log.Logger.Information("Begin long-running import");
logTime($"pre {nameof(importIntoDbAsync)}");
var newCount = await importIntoDbAsync(importItems);
@@ -124,6 +146,7 @@ namespace ApplicationServices
{
stop();
var putBreakPointHere = logOutput;
ScanEnd?.Invoke(null, null);
}
}
@@ -164,25 +187,47 @@ namespace ApplicationServices
}
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
{
logTime("importIntoDbAsync -- pre db");
using var context = DbContexts.GetContext();
var libraryBookImporter = new LibraryBookImporter(context);
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
logTime("importIntoDbAsync -- post Import()");
int qtyChanges = saveChanges(context);
logTime("importIntoDbAsync -- post SaveChanges");
// this is any changes at all to the database, not just new books
if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange());
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
return newCount;
}
private static int saveChanges(LibationContext context)
{
logTime("importIntoDbAsync -- pre db");
using var context = DbContexts.GetContext();
var libraryBookImporter = new LibraryBookImporter(context);
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
logTime("importIntoDbAsync -- post Import()");
var qtyChanges = context.SaveChanges();
logTime("importIntoDbAsync -- post SaveChanges");
try
{
return context.SaveChanges();
}
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex)
{
// DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culpret is the "WithExceptionDetails" serilog extension
if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange());
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}";
return newCount;
}
#endregion
var msg = "Microsoft.EntityFrameworkCore.DbUpdateException";
if (ex.InnerException is null)
throw new Exception($"{msg}{format(ex)}");
throw new Exception(
$"{msg}{format(ex)}",
new Exception($"Inner Exception{format(ex.InnerException)}"));
}
}
#endregion
#region remove books
public static Task<List<LibraryBook>> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
#region remove books
public static Task<List<LibraryBook>> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
private static List<LibraryBook> removeBooks(List<string> idsToRemove)
{
using var context = DbContexts.GetContext();

View File

@@ -73,10 +73,10 @@ namespace AudibleUtilities
if (_identity is null && value is null)
return;
if (_identity != null)
if (_identity is not null)
_identity.Updated -= update;
if (value != null)
if (value is not null)
value.Updated += update;
_identity = value;

View File

@@ -66,7 +66,7 @@ namespace AudibleUtilities
{
var acct = GetAccount(accountId, locale);
if (acct != null)
if (acct is not null)
return acct;
var l = Localization.Get(locale);

View File

@@ -148,7 +148,7 @@ namespace AudibleUtilities
foreach (var v in validators)
{
var exceptions = v.Validate(items);
if (exceptions != null && exceptions.Any())
if (exceptions is not null && exceptions.Any())
throw new AggregateException(exceptions);
}

View File

@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="2.7.2.1" />
<PackageReference Include="AudibleApi" Version="2.7.3.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -9,8 +9,8 @@ namespace DataLayer.Configurations
{
entity.HasKey(bc => new { bc.BookId, bc.ContributorId, bc.Role });
entity.HasIndex(b => b.BookId);
entity.HasIndex(b => b.ContributorId);
entity.HasIndex(bc => bc.BookId);
entity.HasIndex(bc => bc.ContributorId);
entity
.HasOne(bc => bc.Book)

View File

@@ -21,12 +21,12 @@ namespace DataLayer.Configurations
// - update LibraryBook import code
// - would likely challenge assumptions throughout Libation which have been true up until now
entity.HasKey(b => b.BookId);
entity.HasKey(lb => lb.BookId);
entity
.HasOne(le => le.Book)
.HasOne(lb => lb.Book)
.WithOne()
.HasForeignKey<LibraryBook>(le => le.BookId);
.HasForeignKey<LibraryBook>(lb => lb.BookId);
}
}
}

View File

@@ -7,10 +7,10 @@ namespace DataLayer.Configurations
{
public void Configure(EntityTypeBuilder<SeriesBook> entity)
{
entity.HasKey(bc => new { bc.SeriesId, bc.BookId });
entity.HasKey(sb => new { sb.SeriesId, sb.BookId });
entity.HasIndex(b => b.SeriesId);
entity.HasIndex(b => b.BookId);
entity.HasIndex(sb => sb.SeriesId);
entity.HasIndex(sb => sb.BookId);
entity
.HasOne(sb => sb.Series)

View File

@@ -7,8 +7,8 @@ namespace DataLayer.Configurations
{
public void Configure(EntityTypeBuilder<Series> entity)
{
entity.HasKey(b => b.SeriesId);
entity.HasIndex(b => b.AudibleSeriesId);
entity.HasKey(s => s.SeriesId);
entity.HasIndex(s => s.AudibleSeriesId);
entity
.Metadata

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<PropertyGroup>
@@ -13,12 +13,12 @@
<ItemGroup>
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.0.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.3">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -43,14 +43,17 @@ namespace DataLayer
internal int CategoryId { get; private set; }
public Category Category { get; private set; }
public string[] CategoriesNames
=> Category == null ? new string[0]
: Category.ParentCategory == null ? new[] { Category.Name }
=> Category is null ? new string[0]
: Category.ParentCategory is null ? new[] { Category.Name }
: new[] { Category.ParentCategory.Name, Category.Name };
public string[] CategoriesIds
=> Category == null ? null
: Category.ParentCategory == null ? new[] { Category.AudibleCategoryId }
=> Category is null ? null
: Category.ParentCategory is null ? new[] { Category.AudibleCategoryId }
: new[] { Category.ParentCategory.AudibleCategoryId, Category.AudibleCategoryId };
public string TitleSortable => Formatters.GetSortName(Title);
public string SeriesSortable => Formatters.GetSortName(SeriesNames);
// is owned, not optional 1:1
public UserDefinedItem UserDefinedItem { get; private set; }
@@ -216,7 +219,7 @@ namespace DataLayer
getEntry(context).Collection(s => s.SeriesLink).Load();
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
if (singleSeriesBook == null)
if (singleSeriesBook is null)
_seriesLink.Add(new SeriesBook(series, this, order));
else
singleSeriesBook.UpdateOrder(order);

View File

@@ -18,7 +18,7 @@ namespace DataLayer
public class Category
{
// Empty is a special case. use private ctor w/o validation
public static Category GetEmpty() => new Category { CategoryId = -1, AudibleCategoryId = "", Name = "" };
public static Category GetEmpty() => new() { CategoryId = -1, AudibleCategoryId = "", Name = "" };
internal int CategoryId { get; private set; }
public string AudibleCategoryId { get; private set; }
@@ -44,7 +44,7 @@ namespace DataLayer
public void UpdateParentCategory(Category parentCategory)
{
// don't overwrite with null but not an error
if (parentCategory != null)
if (parentCategory is not null)
ParentCategory = parentCategory;
}

View File

@@ -7,7 +7,7 @@ namespace DataLayer
public class Contributor
{
// Empty is a special case. use private ctor w/o validation
public static Contributor GetEmpty() => new Contributor { ContributorId = -1, Name = "" };
public static Contributor GetEmpty() => new() { ContributorId = -1, Name = "" };
// contributors search links are just name with url-encoding. space can be + or %20
// author search link: /search?searchAuthor=Robert+Bevan
@@ -31,16 +31,12 @@ namespace DataLayer
public string AudibleContributorId { get; private set; }
private Contributor() { }
public Contributor(string name)
public Contributor(string name, string audibleContributorId = null)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
Name = ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
_booksLink = new HashSet<BookContributor>();
Name = name;
}
public Contributor(string name, string audibleContributorId) : this(name)
{
// don't overwrite with null or whitespace but not an error
if (!string.IsNullOrWhiteSpace(audibleContributorId))
AudibleContributorId = audibleContributorId;

View File

@@ -162,38 +162,6 @@ namespace DataLayer
}
#endregion
#region batch changes
public static event EventHandler<string> Batch_ItemChanged;
public void BatchMode_UpdateBookStatus(LiberatedStatus value)
{
if (_bookStatus != value)
{
_bookStatus = value;
batchFlag = true;
}
}
// don't overwrite current with null. Therefore input is "LiberatedStatus" not "LiberatedStatus?"
public void BatchMode_UpdatePdfStatus(LiberatedStatus value)
{
if (_pdfStatus != value)
{
_pdfStatus = value;
batchFlag = true;
}
}
private static bool batchFlag = false;
public static void BatchMode_Finalize()
{
if (batchFlag)
Batch_ItemChanged?.Invoke(null, null);
batchFlag = false;
}
#endregion
public override string ToString() => $"{Book} {Rating} {Tags}";
}
}

27
DataLayer/Formatters.cs Normal file
View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace DataLayer
{
internal class Formatters
{
private static string[] _sortPrefixIgnores { get; } = { "the", "a", "an" };
public static string GetSortName(string unformattedName)
{
var sortName = unformattedName
.Replace("|", "")
.Replace(":", "")
.ToLowerInvariant()
.Trim();
if (_sortPrefixIgnores.Any(prefix => sortName.StartsWith(prefix + " ")))
sortName = sortName
.Substring(sortName.IndexOf(" ") + 1)
.TrimStart();
return sortName;
}
}
}

View File

@@ -19,7 +19,8 @@ namespace DataLayer
public static Book GetBook(this IQueryable<Book> books, string productId)
=> books
.GetBooks()
.SingleOrDefault(b => b.AudibleProductId == productId);
// 'Single' is more accurate but 'First' is faster and less error prone
.FirstOrDefault(b => b.AudibleProductId == productId);
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
public static IQueryable<Book> GetBooks(this IQueryable<Book> books, Expression<Func<Book, bool>> predicate)

View File

@@ -19,7 +19,7 @@ namespace DataLayer
.Entries()
.Where(e => e.State.In(EntityState.Modified, EntityState.Added))
.Select(e => e.Entity as UserDefinedItem)
.Where(udi => udi != null)
.Where(udi => udi is not null)
// do NOT filter out entires with blank tags. blank is the valid way to show the absence of tags
.Select(t => (t.Book.AudibleProductId, t.Tags))
.ToList();

View File

@@ -4,62 +4,53 @@ using System.Linq;
using AudibleApi.Common;
using AudibleUtilities;
using DataLayer;
using Dinah.Core.Collections.Generic;
namespace DtoImporterService
{
public class BookImporter : ItemsImporterBase
{
public BookImporter(LibationContext context) : base(context) { }
protected override IValidator Validator => new BookValidator();
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new BookValidator().Validate(importItems.Select(i => i.DtoItem));
public Dictionary<string, Book> Cache { get; private set; } = new();
private ContributorImporter contributorImporter { get; }
private SeriesImporter seriesImporter { get; }
private CategoryImporter categoryImporter { get; }
public BookImporter(LibationContext context) : base(context)
{
contributorImporter = new ContributorImporter(DbContext);
seriesImporter = new SeriesImporter(DbContext);
categoryImporter = new CategoryImporter(DbContext);
}
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
// pre-req.s
new ContributorImporter(DbContext).Import(importItems);
new SeriesImporter(DbContext).Import(importItems);
new CategoryImporter(DbContext).Import(importItems);
// get distinct
var productIds = importItems.Select(i => i.DtoItem.ProductId).Distinct().ToList();
// load db existing => .Local
loadLocal_books(productIds);
contributorImporter.Import(importItems);
seriesImporter.Import(importItems);
categoryImporter.Import(importItems);
// load db existing => hash table
loadLocal_books(importItems);
// upsert
var qtyNew = upsertBooks(importItems);
return qtyNew;
}
private void loadLocal_books(List<string> productIds)
private void loadLocal_books(IEnumerable<ImportItem> importItems)
{
// if this context has already loaded books, don't need to reload them. vestige from when context was long-lived. in practice, we now typically use a fresh context. this is quick though so no harm in leaving it.
var localProductIds = DbContext.Books.Local.Select(b => b.AudibleProductId).ToList();
var remainingProductIds = productIds
.Except(localProductIds)
// get distinct
var productIds = importItems
.Select(i => i.DtoItem.ProductId)
.Distinct()
.ToList();
#region // explanation of DbContext.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
/*
articles suggest loading to Local with
context.Books.Load();
we want Books and associated fields
context.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
this is emulating Load() but with also getting associated fields
from: Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
// Summary:
// Enumerates the query. When using Entity Framework, this causes the results of
// the query to be loaded into the associated context. This is equivalent to calling
// ToList and then throwing away the list (without the overhead of actually creating
// the list).
public static void Load<TSource>([NotNullAttribute] this IQueryable<TSource> source);
*/
#endregion
// GetBooks() eager loads Series, category, et al
if (remainingProductIds.Any())
DbContext.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
Cache = DbContext.Books
.GetBooks(b => productIds.Contains(b.AudibleProductId))
.ToDictionarySafe(b => b.AudibleProductId);
}
private int upsertBooks(IEnumerable<ImportItem> importItems)
@@ -68,8 +59,7 @@ namespace DtoImporterService
foreach (var item in importItems)
{
var book = DbContext.Books.Local.FirstOrDefault(p => p.AudibleProductId == item.DtoItem.ProductId);
if (book is null)
if (!Cache.TryGetValue(item.DtoItem.ProductId, out var book))
{
book = createNewBook(item);
qtyNew++;
@@ -94,8 +84,7 @@ namespace DtoImporterService
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
var authors = item
.Authors
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive
.Select(a => DbContext.Contributors.Local.FirstOrDefault(c => a.Name == c.Name))
.Select(a => contributorImporter.Cache[a.Name])
.ToList();
var narrators
@@ -105,8 +94,7 @@ namespace DtoImporterService
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
: item
.Narrators
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive
.Select(n => DbContext.Contributors.Local.FirstOrDefault(c => n.Name == c.Name))
.Select(n => contributorImporter.Cache[n.Name])
.ToList();
// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
@@ -120,8 +108,7 @@ namespace DtoImporterService
// 2+
: item.Categories[1].CategoryId;
// This should properly be SingleOrDefault() not FirstOrDefault(), but FirstOrDefault is defensive
var category = DbContext.Categories.Local.FirstOrDefault(c => c.AudibleCategoryId == lastCategory);
var category = categoryImporter.Cache[lastCategory];
Book book;
try
@@ -137,6 +124,7 @@ namespace DtoImporterService
category,
importItem.LocaleName)
).Entity;
Cache.Add(book.AudibleProductId, book);
}
catch (Exception ex)
{
@@ -157,8 +145,7 @@ namespace DtoImporterService
var publisherName = item.Publisher;
if (!string.IsNullOrWhiteSpace(publisherName))
{
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive
var publisher = DbContext.Contributors.Local.FirstOrDefault(c => publisherName == c.Name);
var publisher = contributorImporter.Cache[publisherName];
book.ReplacePublisher(publisher);
}
@@ -185,11 +172,11 @@ namespace DtoImporterService
// update series even for existing books. these are occasionally updated
// these will upsert over library-scraped series, but will not leave orphans
if (item.Series != null)
if (item.Series is not null)
{
foreach (var seriesEntry in item.Series)
{
var series = DbContext.Series.Local.FirstOrDefault(s => seriesEntry.SeriesId == s.AudibleSeriesId);
var series = seriesImporter.Cache[seriesEntry.SeriesId];
book.UpsertSeries(series, seriesEntry.Sequence);
}
}

View File

@@ -4,14 +4,17 @@ using System.Linq;
using AudibleApi.Common;
using AudibleUtilities;
using DataLayer;
using Dinah.Core.Collections.Generic;
namespace DtoImporterService
{
public class CategoryImporter : ItemsImporterBase
{
public CategoryImporter(LibationContext context) : base(context) { }
protected override IValidator Validator => new CategoryValidator();
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new CategoryValidator().Validate(importItems.Select(i => i.DtoItem));
public Dictionary<string, Category> Cache { get; private set; } = new();
public CategoryImporter(LibationContext context) : base(context) { }
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
@@ -19,7 +22,9 @@ namespace DtoImporterService
var categoryIds = importItems
.Select(i => i.DtoItem)
.GetCategoriesDistinct()
.Select(c => c.CategoryId).ToList();
.Select(c => c.CategoryId)
.Distinct()
.ToList();
// load db existing => .Local
loadLocal_categories(categoryIds);
@@ -35,17 +40,13 @@ namespace DtoImporterService
private void loadLocal_categories(List<string> categoryIds)
{
var localIds = DbContext.Categories.Local.Select(c => c.AudibleCategoryId).ToList();
var remainingCategoryIds = categoryIds
.Distinct()
.Except(localIds)
.ToList();
// must include default/empty/missing
categoryIds.Add(Category.GetEmpty().AudibleCategoryId);
// load existing => local
// remember to include default/empty/missing
var emptyName = Contributor.GetEmpty().Name;
if (remainingCategoryIds.Any())
DbContext.Categories.Where(c => remainingCategoryIds.Contains(c.AudibleCategoryId) || c.Name == emptyName).ToList();
Cache = DbContext.Categories
.Where(c => categoryIds.Contains(c.AudibleCategoryId))
.ToDictionarySafe(c => c.AudibleCategoryId);
}
// only use after loading contributors => local
@@ -66,22 +67,11 @@ namespace DtoImporterService
Category parentCategory = null;
if (i == 1)
// should be "Single()" but user is getting a strange error
parentCategory = DbContext.Categories.Local.FirstOrDefault(c => c.AudibleCategoryId == pair[0].CategoryId);
Cache.TryGetValue(pair[0].CategoryId, out parentCategory);
// should be "SingleOrDefault()" but user is getting a strange error
var category = DbContext.Categories.Local.FirstOrDefault(c => c.AudibleCategoryId == id);
if (category is null)
if (!Cache.TryGetValue(id, out var category))
{
try
{
category = DbContext.Categories.Add(new Category(new AudibleCategoryId(id), name)).Entity;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding category. {@DebugInfo}", new { id, name });
throw;
}
category = addCategory(id, name);
qtyNew++;
}
@@ -91,5 +81,24 @@ namespace DtoImporterService
return qtyNew;
}
private Category addCategory(string id, string name)
{
try
{
var category = new Category(new AudibleCategoryId(id), name);
var entityEntry = DbContext.Categories.Add(category);
var entity = entityEntry.Entity;
Cache.Add(entity.AudibleCategoryId, entity);
return entity;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding category. {@DebugInfo}", new { id, name });
throw;
}
}
}
}

View File

@@ -4,14 +4,17 @@ using System.Linq;
using AudibleApi.Common;
using AudibleUtilities;
using DataLayer;
using Dinah.Core.Collections.Generic;
namespace DtoImporterService
{
public class ContributorImporter : ItemsImporterBase
{
public ContributorImporter(LibationContext context) : base(context) { }
protected override IValidator Validator => new ContributorValidator();
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new ContributorValidator().Validate(importItems.Select(i => i.DtoItem));
public Dictionary<string, Contributor> Cache { get; private set; } = new();
public ContributorImporter(LibationContext context) : base(context) { }
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
@@ -50,78 +53,61 @@ namespace DtoImporterService
// must include default/empty/missing
contributorNames.Add(Contributor.GetEmpty().Name);
//// BAD: very inefficient
// var x = context.Contributors.Local.Where(c => !contribNames.Contains(c.Name));
// GOOD: Except() is efficient. Due to hashing, it's close to O(n)
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
var remainingContribNames = contributorNames
.Distinct()
.Except(localNames)
.ToList();
// load existing => local
if (remainingContribNames.Any())
DbContext.Contributors.Where(c => remainingContribNames.Contains(c.Name)).ToList();
Cache = DbContext.Contributors
.Where(c => contributorNames.Contains(c.Name))
.ToDictionarySafe(c => c.Name);
}
// only use after loading contributors => local
private int upsertPeople(List<Person> people)
{
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
var newPeople = people
.Select(p => p.Name)
.Distinct()
.Except(localNames)
.ToList();
var hash = people
// new people only
.Where(p => !Cache.ContainsKey(p.Name))
// remove duplicates by Name. first in wins
.ToDictionarySafe(p => p.Name);
var groupby = people.GroupBy(
p => p.Name,
p => p,
(key, g) => new { Name = key, People = g.ToList() }
);
foreach (var name in newPeople)
foreach (var kvp in hash)
{
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive
var p = groupby.FirstOrDefault(g => g.Name == name).People.First();
try
{
DbContext.Contributors.Add(new Contributor(p.Name, p.Asin));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding person. {@DebugInfo}", new { p?.Name, p?.Asin });
throw;
}
var person = kvp.Value;
addContributor(person.Name, person.Asin);
}
return newPeople.Count;
return hash.Count;
}
// only use after loading contributors => local
private int upsertPublishers(List<string> publishers)
{
var localNames = DbContext.Contributors.Local.Select(c => c.Name).ToList();
var newPublishers = publishers
.Distinct()
.Except(localNames)
.ToList();
var hash = publishers
// new publishers only
.Where(p => !Cache.ContainsKey(p))
// remove duplicates
.ToHashSet();
foreach (var pub in newPublishers)
{
try
{
DbContext.Contributors.Add(new Contributor(pub));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding publisher. {@DebugInfo}", new { pub });
throw;
}
}
foreach (var pub in hash)
addContributor(pub);
return newPublishers.Count;
return hash.Count;
}
}
private Contributor addContributor(string name, string id = null)
{
try
{
var newContrib = new Contributor(name);
var entityEntry = DbContext.Contributors.Add(newContrib);
var entity = entityEntry.Entity;
Cache.Add(entity.Name, entity);
return entity;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding contributor. {@DebugInfo}", new { name, id });
throw;
}
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -25,7 +25,7 @@ namespace DtoImporterService
try
{
var exceptions = Validate(param);
if (exceptions != null && exceptions.Any())
if (exceptions is not null && exceptions.Any())
throw new AggregateException($"Importer validation failed", exceptions);
}
catch (Exception ex)
@@ -53,5 +53,9 @@ namespace DtoImporterService
public abstract class ItemsImporterBase : ImporterBase<IEnumerable<ImportItem>>
{
protected ItemsImporterBase(LibationContext context) : base(context) { }
protected abstract IValidator Validator { get; }
public sealed override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems)
=> Validator.Validate(importItems.Select(i => i.DtoItem));
}
}

View File

@@ -3,18 +3,24 @@ using System.Collections.Generic;
using System.Linq;
using AudibleUtilities;
using DataLayer;
using Dinah.Core.Collections.Generic;
namespace DtoImporterService
{
public class LibraryBookImporter : ItemsImporterBase
{
public LibraryBookImporter(LibationContext context) : base(context) { }
protected override IValidator Validator => new LibraryValidator();
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new LibraryValidator().Validate(importItems.Select(i => i.DtoItem));
private BookImporter bookImporter { get; }
public LibraryBookImporter(LibationContext context) : base(context)
{
bookImporter = new BookImporter(DbContext);
}
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
new BookImporter(DbContext).Import(importItems);
bookImporter.Import(importItems);
var qtyNew = upsertLibraryBooks(importItems);
return qtyNew;
@@ -36,25 +42,18 @@ namespace DtoImporterService
var currentLibraryProductIds = DbContext.LibraryBooks.Select(l => l.Book.AudibleProductId).ToList();
var newItems = importItems
.Where(dto => !currentLibraryProductIds
.Contains(dto.DtoItem.ProductId))
.Where(dto => !currentLibraryProductIds.Contains(dto.DtoItem.ProductId))
.ToList();
// if 2 accounts try to import the same book in the same transaction: error since we're only tracking and pulling by asin.
// just use the first
var groupby = newItems.GroupBy(
i => i.DtoItem.ProductId,
i => i,
(key, g) => new { ProductId = key, ImportItems = g.ToList() }
)
.ToList();
foreach (var gb in groupby)
{
var newItem = gb.ImportItems.First();
var hash = newItems.ToDictionarySafe(dto => dto.DtoItem.ProductId);
foreach (var kvp in hash)
{
var newItem = kvp.Value;
var libraryBook = new LibraryBook(
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive
DbContext.Books.Local.FirstOrDefault(b => b.AudibleProductId == newItem.DtoItem.ProductId),
bookImporter.Cache[newItem.DtoItem.ProductId],
newItem.DtoItem.DateAdded,
newItem.AccountId);
try
@@ -67,7 +66,7 @@ namespace DtoImporterService
}
}
var qtyNew = groupby.Count;
var qtyNew = hash.Count;
return qtyNew;
}
}

View File

@@ -8,8 +8,8 @@ namespace DtoImporterService
public record timeLogEntry(string msg, long totalElapsed, long delta);
public static class PerfLogger
{
private static Stopwatch sw = new Stopwatch();
private static List<timeLogEntry> __log { get; } = new List<timeLogEntry> { new("begin", 0, 0) };
private static Stopwatch sw { get; } = new();
private static List<timeLogEntry> __log { get; } = new() { new("begin", 0, 0) };
public static void logTime(string s)
{

View File

@@ -4,14 +4,17 @@ using System.Linq;
using AudibleApi.Common;
using AudibleUtilities;
using DataLayer;
using Dinah.Core.Collections.Generic;
namespace DtoImporterService
{
public class SeriesImporter : ItemsImporterBase
{
public SeriesImporter(LibationContext context) : base(context) { }
protected override IValidator Validator => new SeriesValidator();
public override IEnumerable<Exception> Validate(IEnumerable<ImportItem> importItems) => new SeriesValidator().Validate(importItems.Select(i => i.DtoItem));
public Dictionary<string, DataLayer.Series> Cache { get; private set; } = new();
public SeriesImporter(LibationContext context) : base(context) { }
protected override int DoImport(IEnumerable<ImportItem> importItems)
{
@@ -31,15 +34,12 @@ namespace DtoImporterService
private void loadLocal_series(List<AudibleApi.Common.Series> series)
{
var seriesIds = series.Select(s => s.SeriesId).ToList();
var localIds = DbContext.Series.Local.Select(s => s.AudibleSeriesId).ToList();
var remainingSeriesIds = seriesIds
.Distinct()
.Except(localIds)
.ToList();
var seriesIds = series.Select(s => s.SeriesId).Distinct().ToList();
if (remainingSeriesIds.Any())
DbContext.Series.Where(s => remainingSeriesIds.Contains(s.AudibleSeriesId)).ToList();
if (seriesIds.Any())
Cache = DbContext.Series
.Where(s => seriesIds.Contains(s.AudibleSeriesId))
.ToDictionarySafe(s => s.AudibleSeriesId);
}
private int upsertSeries(List<AudibleApi.Common.Series> requestedSeries)
@@ -48,18 +48,10 @@ namespace DtoImporterService
foreach (var s in requestedSeries)
{
var series = DbContext.Series.Local.FirstOrDefault(c => c.AudibleSeriesId == s.SeriesId);
if (series is null)
// AudibleApi.Common.Series.SeriesId == DataLayer.AudibleSeriesId
if (!Cache.TryGetValue(s.SeriesId, out var series))
{
try
{
series = DbContext.Series.Add(new DataLayer.Series(new AudibleSeriesId(s.SeriesId))).Entity;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding series. {@DebugInfo}", new { s?.SeriesId });
throw;
}
series = addSeries(s.SeriesId);
qtyNew++;
}
series.UpdateName(s.SeriesName);
@@ -67,5 +59,24 @@ namespace DtoImporterService
return qtyNew;
}
private DataLayer.Series addSeries(string seriesId)
{
try
{
var series = new DataLayer.Series(new AudibleSeriesId(seriesId));
var entityEntry = DbContext.Series.Add(series);
var entity = entityEntry.Entity;
Cache.Add(entity.AudibleSeriesId, entity);
return entity;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding series. {@DebugInfo}", new { seriesId });
throw;
}
}
}
}

View File

@@ -0,0 +1,37 @@
* Local (eg DbContext.Books.Local): indexes/hashes PK and nothing else. Local.Find(PK) is fast. All other searches (eg FirstOrDefault) have awful performance. It deceptively *feels* like we get this partially for free since added/modified entries live here.
* live db: for all importers, fields used for lookup are indexed
Using BookImporter as an example: since AudibleProductId is indexed, hitting the live db is much faster than using Local. Fastest is putting all in a local hash table
Note: GetBook/GetBooks eager loads Series, category, et al
for 1,200 iterations
* load to LocalView
DbContext.Books.Local.FirstOrDefault(p => p.AudibleProductId == item.DtoItem.ProductId)
27,125 ms
* read from live db
DbContext.Books.GetBook(item.DtoItem.ProductId)
12,224 ms
* load to hash table: Dictionary<string, Book>
dictionary[item.DtoItem.ProductId];
1 ms (yes: ONE)
With hashtable, somehow memory usage was not significantly affected
-----------------------------------
why were we using Local to begin with?
articles suggest loading to Local with
context.Books.Load();
this loads this table but not associated fields
we want Books and associated fields
context.Books.GetBooks(b => remainingProductIds.Contains(b.AudibleProductId)).ToList();
this is emulating Load() but with also getting associated fields
from: Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
// Summary:
// Enumerates the query. When using Entity Framework, this causes the results of
// the query to be loaded into the associated context. This is equivalent to calling
// ToList and then throwing away the list (without the overhead of actually creating
// the list).
public static void Load<TSource>([NotNullAttribute] this IQueryable<TSource> source);

View File

@@ -11,30 +11,33 @@ namespace FileLiberator
public event EventHandler<byte[]> CoverImageDiscovered;
public abstract void Cancel();
protected void OnRequestCoverArt(Action<byte[]> setCoverArtDel)
{
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
RequestCoverArt?.Invoke(this, setCoverArtDel);
}
protected void OnTitleDiscovered(string title)
protected void OnTitleDiscovered(string title) => OnTitleDiscovered(null, title);
protected void OnTitleDiscovered(object _, string title)
{
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(TitleDiscovered), Title = title });
TitleDiscovered?.Invoke(this, title);
}
protected void OnAuthorsDiscovered(string authors)
protected void OnAuthorsDiscovered(string authors) => OnAuthorsDiscovered(null, authors);
protected void OnAuthorsDiscovered(object _, string authors)
{
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(AuthorsDiscovered), Authors = authors });
AuthorsDiscovered?.Invoke(this, authors);
}
protected void OnNarratorsDiscovered(string narrators)
protected void OnNarratorsDiscovered(string narrators) => OnNarratorsDiscovered(null, narrators);
protected void OnNarratorsDiscovered(object _, string narrators)
{
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(NarratorsDiscovered), Narrators = narrators });
NarratorsDiscovered?.Invoke(this, narrators);
}
protected void OnRequestCoverArt(Action<byte[]> setCoverArtDel)
{
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
RequestCoverArt?.Invoke(this, setCoverArtDel);
}
protected void OnCoverImageDiscovered(byte[] coverImage)
{
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(CoverImageDiscovered), CoverImageBytes = coverImage?.Length });

View File

@@ -39,7 +39,7 @@ namespace FileLiberator
/// File name: final file name.
/// </summary>
public static string GetInProgressFilename(this AudioFileStorage _, LibraryBook libraryBook, string extension)
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension);
=> Templates.File.GetFilename(libraryBook.ToDto(), AudibleFileStorage.DecryptInProgressDirectory, extension, returnFirstExisting: true);
/// <summary>
/// PDF: audio file does not exist

View File

@@ -2,6 +2,7 @@
using System.IO;
using System.Threading.Tasks;
using AAXClean;
using AAXClean.Codecs;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
@@ -72,8 +73,8 @@ namespace FileLiberator
private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
{
var duration = m4bBook.Duration;
double remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
if (double.IsNormal(estTimeRemaining))
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));

View File

@@ -61,7 +61,12 @@ namespace FileLiberator
// decrypt failed
if (!success)
{
foreach (var tmpFile in entries.Where(f => f.FileType != FileType.AAXC))
FileUtility.SaferDelete(tmpFile.Path);
return new StatusHandler { "Decrypt failed" };
}
// moves new files from temp dir to final dest
var movedAudioFile = moveFilesToBooksDir(libraryBook, entries);
@@ -86,51 +91,38 @@ namespace FileLiberator
try
{
var config = Configuration.Instance;
downloadValidation(libraryBook);
var api = await libraryBook.GetApiAsync();
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
var audiobookDlLic = BuildDownloadOptions(config, contentLic);
var audiobookDlLic = new DownloadLicense
(
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
contentLic?.Voucher?.Key,
contentLic?.Voucher?.Iv,
Resources.USER_AGENT
);
//I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3.
//I also assume that if DrmType != Adrm, the file will be an mp3.
//These assumptions may be wrong, and only time and bug reports will tell.
var outputFormat =
contentLic.ContentMetadata.ContentReference.ContentFormat == "MPEG" ||
(Configuration.Instance.AllowLibationFixup && Configuration.Instance.DecryptToLossy) ?
OutputFormat.Mp3 : OutputFormat.M4b;
if (Configuration.Instance.AllowLibationFixup || outputFormat == OutputFormat.Mp3)
{
audiobookDlLic.ChapterInfo = new AAXClean.ChapterInfo();
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
}
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, outputFormat.ToString().ToLower());
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, audiobookDlLic.OutputFormat.ToString().ToLower());
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
abDownloader
= contentLic.DrmType != AudibleApi.Common.DrmType.Adrm ? new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic)
: Configuration.Instance.SplitFilesByChapter ? new AaxcDownloadMultiConverter(
outFileName, cacheDir, audiobookDlLic, outputFormat,
AudibleFileStorage.Audio.CreateMultipartRenamerFunc(libraryBook)
)
: new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic, outputFormat);
abDownloader.DecryptProgressUpdate += (_, progress) => OnStreamingProgressChanged(progress);
abDownloader.DecryptTimeRemaining += (_, remaining) => OnStreamingTimeRemaining(remaining);
abDownloader.RetrievedTitle += (_, title) => OnTitleDiscovered(title);
abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors);
abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators);
if (contentLic.DrmType != AudibleApi.Common.DrmType.Adrm)
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic);
else
{
AaxcDownloadConvertBase converter
= config.SplitFilesByChapter ? new AaxcDownloadMultiConverter(
outFileName, cacheDir, audiobookDlLic,
AudibleFileStorage.Audio.CreateMultipartRenamerFunc(libraryBook))
: new AaxcDownloadSingleConverter(outFileName, cacheDir, audiobookDlLic);
if (config.AllowLibationFixup)
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames);
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(libraryBook, path);
@@ -144,6 +136,85 @@ namespace FileLiberator
}
}
private static DownloadOptions BuildDownloadOptions(Configuration config, AudibleApi.Common.ContentLicense contentLic)
{
//I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3.
//I also assume that if DrmType != Adrm, the file will be an mp3.
//These assumptions may be wrong, and only time and bug reports will tell.
bool encrypted = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm;
var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ?
OutputFormat.Mp3 : OutputFormat.M4b;
var audiobookDlLic = new DownloadOptions
(
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
Resources.USER_AGENT
)
{
AudibleKey = contentLic?.Voucher?.Key,
AudibleIV = contentLic?.Voucher?.Iv,
OutputFormat = outputFormat,
TrimOutputToChapterLength = config.AllowLibationFixup && config.StripAudibleBrandAudio,
RetainEncryptedFile = config.RetainAaxFile && encrypted,
StripUnabridged = config.AllowLibationFixup && config.StripUnabridged,
Downsample = config.AllowLibationFixup && config.LameDownsampleMono,
MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate,
CreateCueSheet = config.CreateCueSheet
};
if (config.AllowLibationFixup || outputFormat == OutputFormat.Mp3)
{
long startMs = audiobookDlLic.TrimOutputToChapterLength ?
contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0;
audiobookDlLic.ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(startMs));
for (int i = 0; i < contentLic.ContentMetadata.ChapterInfo.Chapters.Length; i++)
{
var chapter = contentLic.ContentMetadata.ChapterInfo.Chapters[i];
long chapLenMs = chapter.LengthMs;
if (i == 0)
chapLenMs -= startMs;
if (config.StripAudibleBrandAudio && i == contentLic.ContentMetadata.ChapterInfo.Chapters.Length - 1)
chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
audiobookDlLic.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
}
}
NAudio.Lame.LameConfig lameConfig = new();
lameConfig.Mode = NAudio.Lame.MPEGMode.Mono;
if (config.LameTargetBitrate)
{
if (config.LameConstantBitrate)
lameConfig.BitRate = config.LameBitrate;
else
{
lameConfig.ABRRateKbps = config.LameBitrate;
lameConfig.VBR = NAudio.Lame.VBRMode.ABR;
lameConfig.WriteVBRTag = true;
}
}
else
{
lameConfig.VBR = NAudio.Lame.VBRMode.Default;
lameConfig.VBRQuality = config.LameVBRQuality;
lameConfig.WriteVBRTag = true;
}
audiobookDlLic.LameConfig = lameConfig;
return audiobookDlLic;
}
private static void downloadValidation(LibraryBook libraryBook)
{
string errorString(string field)
@@ -166,7 +237,7 @@ namespace FileLiberator
throw new Exception(errorString("Locale"));
}
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e)
{
if (e is not null)
OnCoverImageDiscovered(e);

View File

@@ -12,8 +12,7 @@ namespace FileLiberator
{
var client = new HttpClient();
var progress = new Progress<DownloadProgress>();
progress.ProgressChanged += (_, e) => OnStreamingProgressChanged(e);
var progress = new Progress<DownloadProgress>(OnStreamingProgressChanged);
OnStreamingBegin(proposedDownloadFilePath);

View File

@@ -62,8 +62,7 @@ namespace FileLiberator
var api = await libraryBook.GetApiAsync();
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
var progress = new Progress<DownloadProgress>();
progress.ProgressChanged += (_, e) => OnStreamingProgressChanged(e);
var progress = new Progress<DownloadProgress>(OnStreamingProgressChanged);
var client = new HttpClient();

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -18,12 +18,14 @@ namespace FileLiberator
StreamingBegin?.Invoke(this, filePath);
}
protected void OnStreamingProgressChanged(DownloadProgress progress)
protected void OnStreamingProgressChanged(DownloadProgress progress) => OnStreamingProgressChanged(null, progress);
protected void OnStreamingProgressChanged(object _, DownloadProgress progress)
{
StreamingProgressChanged?.Invoke(this, progress);
}
protected void OnStreamingTimeRemaining(TimeSpan timeRemaining)
protected void OnStreamingTimeRemaining(TimeSpan timeRemaining) => OnStreamingTimeRemaining(null, timeRemaining);
protected void OnStreamingTimeRemaining(object _, TimeSpan timeRemaining)
{
StreamingTimeRemaining?.Invoke(this, timeRemaining);
}

View File

@@ -43,7 +43,7 @@ namespace FileManager
lock (fsCacheLocker)
{
fsCache.Clear();
fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption));
fsCache.AddRange(FileUtility.SaferEnumerateFiles(RootDirectory, SearchPattern, SearchOption));
}
}
@@ -52,7 +52,7 @@ namespace FileManager
Stop();
lock (fsCacheLocker)
fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption));
fsCache.AddRange(FileUtility.SaferEnumerateFiles(RootDirectory, SearchPattern, SearchOption));
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
fileSystemWatcher = new FileSystemWatcher(RootDirectory)
@@ -135,7 +135,7 @@ namespace FileManager
private void AddPath(string path)
{
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
AddUniqueFiles(Directory.EnumerateFiles(path, SearchPattern, SearchOption));
AddUniqueFiles(FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption));
else
AddUniqueFile(path);
}

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="4.0.4.1" />
<PackageReference Include="Dinah.Core" Version="4.0.6.1" />
<PackageReference Include="Polly" Version="7.2.3" />
</ItemGroup>

View File

@@ -29,14 +29,14 @@ namespace FileManager
public string IllegalCharacterReplacements { get; set; }
/// <summary>Generate a valid path for this file or directory</summary>
public string GetFilePath()
public string GetFilePath(bool returnFirstExisting = false)
{
var filename = Template;
foreach (var r in ParameterReplacements)
filename = filename.Replace($"<{formatKey(r.Key)}>", formatValue(r.Value));
return FileUtility.GetValidFilename(filename, IllegalCharacterReplacements);
return FileUtility.GetValidFilename(filename, IllegalCharacterReplacements, returnFirstExisting);
}
private static string formatKey(string key)

View File

@@ -48,7 +48,7 @@ namespace FileManager
/// <br/>- ensure uniqueness
/// <br/>- enforce max file length
/// </summary>
public static string GetValidFilename(string path, string illegalCharacterReplacements = "")
public static string GetValidFilename(string path, string illegalCharacterReplacements = "", bool returnFirstExisting = false)
{
ArgumentValidator.EnsureNotNull(path, nameof(path));
@@ -69,7 +69,7 @@ namespace FileManager
fullfilename = removeInvalidWhitespace(fullfilename);
var i = 0;
while (File.Exists(fullfilename))
while (File.Exists(fullfilename) && !returnFirstExisting)
{
var increm = $" ({++i})";
fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - increm.Length - extension.Length) + increm + extension;
@@ -234,5 +234,39 @@ namespace FileManager
throw;
}
});
/// <summary>
/// A safer way to get all the files in a directory and sub directory without crashing on UnauthorizedException or PathTooLongException
/// </summary>
/// <param name="rootPath">Starting directory</param>
/// <param name="patternMatch">Filename pattern match</param>
/// <param name="searchOption">Search subdirectories or only top level directory for files</param>
/// <returns>List of files</returns>
public static IEnumerable<string> SaferEnumerateFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
var foundFiles = Enumerable.Empty<string>();
if (searchOption == SearchOption.AllDirectories)
{
try
{
IEnumerable<string> subDirs = Directory.EnumerateDirectories(path);
// Add files in subdirectories recursively to the list
foreach (string dir in subDirs)
foundFiles = foundFiles.Concat(SaferEnumerateFiles(dir, searchPattern, searchOption));
}
catch (UnauthorizedAccessException) { }
catch (PathTooLongException) { }
}
try
{
// Add files from the current directory
foundFiles = foundFiles.Concat(Directory.EnumerateFiles(path, searchPattern));
}
catch (UnauthorizedAccessException) { }
return foundFiles;
}
}
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
<PublishTrimmed>true</PublishTrimmed>
<PublishReadyToRun>true</PublishReadyToRun>

View File

@@ -35,7 +35,8 @@ namespace LibationCli
var (TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync((a) => ApiExtended.CreateAsync(a), _accounts);
Console.WriteLine("Scan complete.");
Console.WriteLine($"Total processed: {TotalBooksProcessed}\r\nNew: {NewBooksAdded}");
Console.WriteLine($"Total processed: {TotalBooksProcessed}");
Console.WriteLine($"New: {NewBooksAdded}");
}
private Account[] getAccounts()

View File

@@ -73,12 +73,12 @@ namespace LibationFileManager
protected override string GetFilePathCustom(string productId)
{
var regex = GetBookSearchRegex(productId);
return Directory
.EnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
return FileUtility
.SaferEnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
.FirstOrDefault(s => regex.IsMatch(s));
}
public bool Exists(string productId) => GetFilePath(productId) != null;
public bool Exists(string productId) => GetFilePath(productId) is not null;
}
public class AudioFileStorage : AudibleFileStorage

View File

@@ -96,18 +96,88 @@ namespace LibationFileManager
set => persistentDictionary.SetNonString(nameof(AllowLibationFixup), value);
}
[Description("Create a cue sheet (.cue)")]
public bool CreateCueSheet
{
get => persistentDictionary.GetNonString<bool>(nameof(CreateCueSheet));
set => persistentDictionary.SetNonString(nameof(CreateCueSheet), value);
}
[Description("Retain the Aax file after successfully decrypting")]
public bool RetainAaxFile
{
get => persistentDictionary.GetNonString<bool>(nameof(RetainAaxFile));
set => persistentDictionary.SetNonString(nameof(RetainAaxFile), value);
}
[Description("Split my books into multiple files by chapter")]
public bool SplitFilesByChapter
{
get => persistentDictionary.GetNonString<bool>(nameof(SplitFilesByChapter));
set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value);
}
[Description("Strip \"(Unabridged)\" from audiobook metadata tags")]
public bool StripUnabridged
{
get => persistentDictionary.GetNonString<bool>(nameof(StripUnabridged));
set => persistentDictionary.SetNonString(nameof(StripUnabridged), value);
}
[Description("Allow Libation to remove audible branding from the start\r\nand end of audiobooks. (e.g. \"This is Audible\")")]
public bool StripAudibleBrandAudio
{
get => persistentDictionary.GetNonString<bool>(nameof(StripAudibleBrandAudio));
set => persistentDictionary.SetNonString(nameof(StripAudibleBrandAudio), value);
}
[Description("Decrypt to lossy format?")]
public bool DecryptToLossy
{
get => persistentDictionary.GetNonString<bool>(nameof(DecryptToLossy));
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
}
[Description("Split my books into multiple files by chapter")]
public bool SplitFilesByChapter
[Description("Lame encoder target. true = Bitrate, false = Quality")]
public bool LameTargetBitrate
{
get => persistentDictionary.GetNonString<bool>(nameof(SplitFilesByChapter));
set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value);
get => persistentDictionary.GetNonString<bool>(nameof(LameTargetBitrate));
set => persistentDictionary.SetNonString(nameof(LameTargetBitrate), value);
}
[Description("Lame encoder downsamples to mono")]
public bool LameDownsampleMono
{
get => persistentDictionary.GetNonString<bool>(nameof(LameDownsampleMono));
set => persistentDictionary.SetNonString(nameof(LameDownsampleMono), value);
}
[Description("Lame target bitrate [16,320]")]
public int LameBitrate
{
get => persistentDictionary.GetNonString<int>(nameof(LameBitrate));
set => persistentDictionary.SetNonString(nameof(LameBitrate), value);
}
[Description("Restrict encoder to constant bitrate?")]
public bool LameConstantBitrate
{
get => persistentDictionary.GetNonString<bool>(nameof(LameConstantBitrate));
set => persistentDictionary.SetNonString(nameof(LameConstantBitrate), value);
}
[Description("Match the source bitrate?")]
public bool LameMatchSourceBR
{
get => persistentDictionary.GetNonString<bool>(nameof(LameMatchSourceBR));
set => persistentDictionary.SetNonString(nameof(LameMatchSourceBR), value);
}
[Description("Lame target VBR quality [10,100]")]
public int LameVBRQuality
{
get => persistentDictionary.GetNonString<int>(nameof(LameVBRQuality));
set => persistentDictionary.SetNonString(nameof(LameVBRQuality), value);
}
public enum BadBookAction

View File

@@ -42,7 +42,7 @@ namespace LibationFileManager
}
}
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) != null;
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
public static List<(FileType fileType, string path)> GetFiles(string id)
=> getEntries(entry => entry.Id == id)

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -70,7 +70,7 @@ namespace LibationFileManager
{
lock (cacheLocker)
{
if (!cache.ContainsKey(def) || cache[def] == null)
if (!cache.ContainsKey(def) || cache[def] is null)
{
var path = getPath(def);
var bytes

View File

@@ -9,6 +9,8 @@ namespace LibationFileManager
{
public static class QuickFilters
{
public static event EventHandler Updated;
internal class FilterState
{
public bool UseDefault { get; set; }
@@ -34,7 +36,7 @@ namespace LibationFileManager
lock (locker)
{
inMemoryState.UseDefault = value;
save();
save(false);
}
}
}
@@ -97,7 +99,7 @@ namespace LibationFileManager
private static object locker { get; } = new object();
// ONLY call this within lock()
private static void save()
private static void save(bool invokeUpdatedEvent = true)
{
// create json if not exists
void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(inMemoryState, Formatting.Indented));
@@ -111,6 +113,9 @@ namespace LibationFileManager
throw;
}
}
if (invokeUpdatedEvent)
Updated?.Invoke(null, null);
}
}
}

View File

@@ -54,11 +54,17 @@ namespace LibationFileManager
private static void ensureCache()
{
if (cache is null)
lock (locker)
cache = !File.Exists(TagsFile)
? new Dictionary<string, string>()
: JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(TagsFile));
if (cache is not null)
return;
lock (locker)
{
if (File.Exists(TagsFile))
cache = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(TagsFile));
// if file doesn't exist. or if file is corrupt and deserialize returns null
cache ??= new Dictionary<string, string>();
}
}
}
}

View File

@@ -230,9 +230,9 @@ namespace LibationFileManager
#region to file name
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension)
public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false)
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension)
.GetFilePath();
.GetFilePath(returnFirstExisting);
#endregion
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -16,7 +16,7 @@ namespace LibationSearchEngine
internal static void AddAnalyzed(this Document document, string name, string value)
{
if (value != null)
if (value is not null)
document.Add(new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.ANALYZED));
}

View File

@@ -70,11 +70,11 @@ namespace LibationSearchEngine
.Select(s => s.Series.AudibleSeriesId)),
["SeriesId"] = lb => string.Join(", ", lb.Book.SeriesLink.Select(s => s.Series.AudibleSeriesId)),
[nameof(Book.CategoriesNames)] = lb => lb.Book.CategoriesIds == null ? null : string.Join(", ", lb.Book.CategoriesIds),
[nameof(Book.Category)] = lb => lb.Book.CategoriesIds == null ? null : string.Join(", ", lb.Book.CategoriesIds),
["Categories"] = lb => lb.Book.CategoriesIds == null ? null : string.Join(", ", lb.Book.CategoriesIds),
["CategoriesId"] = lb => lb.Book.CategoriesIds == null ? null : string.Join(", ", lb.Book.CategoriesIds),
["CategoryId"] = lb => lb.Book.CategoriesIds == null ? null : string.Join(", ", lb.Book.CategoriesIds),
[nameof(Book.CategoriesNames)] = lb => lb.Book.CategoriesIds is null ? null : string.Join(", ", lb.Book.CategoriesIds),
[nameof(Book.Category)] = lb => lb.Book.CategoriesIds is null ? null : string.Join(", ", lb.Book.CategoriesIds),
["Categories"] = lb => lb.Book.CategoriesIds is null ? null : string.Join(", ", lb.Book.CategoriesIds),
["CategoriesId"] = lb => lb.Book.CategoriesIds is null ? null : string.Join(", ", lb.Book.CategoriesIds),
["CategoryId"] = lb => lb.Book.CategoriesIds is null ? null : string.Join(", ", lb.Book.CategoriesIds),
[TAGS.FirstCharToUpper()] = lb => lb.Book.UserDefinedItem.Tags,
@@ -314,7 +314,7 @@ namespace LibationSearchEngine
var query = new TermQuery(productTerm);
var docs = searcher.Search(query, 1);
var scoreDoc = docs.ScoreDocs.SingleOrDefault();
if (scoreDoc == null)
if (scoreDoc is null)
return;
var document = searcher.Doc(scoreDoc.Doc);

View File

@@ -14,11 +14,6 @@ namespace LibationWinForms.BookLiberation
private Func<byte[]> GetCoverArtDelegate;
// book info
private string title;
private string authorNames;
private string narratorNames;
#region Processable event handler overrides
public override void Processable_Begin(object sender, LibraryBook libraryBook)
{
@@ -31,8 +26,8 @@ namespace LibationWinForms.BookLiberation
//Set default values from library
AudioDecodable_TitleDiscovered(sender, libraryBook.Book.Title);
AudioDecodable_AuthorsDiscovered(sender, string.Join(", ", libraryBook.Book.Authors));
AudioDecodable_NarratorsDiscovered(sender, string.Join(", ", libraryBook.Book.NarratorNames));
AudioDecodable_AuthorsDiscovered(sender, libraryBook.Book.AuthorNames);
AudioDecodable_NarratorsDiscovered(sender, libraryBook.Book.NarratorNames);
AudioDecodable_CoverImageDiscovered(sender,
PictureStorage.GetPicture(
new PictureDefinition(
@@ -60,14 +55,23 @@ namespace LibationWinForms.BookLiberation
updateRemainingTime((int)timeRemaining.TotalSeconds);
}
private void updateRemainingTime(int remaining)
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{formatTime(remaining)}");
private string formatTime(int seconds)
{
var timeSpan = new TimeSpan(0, 0, seconds);
return
timeSpan.TotalHours >= 1 ? $"{timeSpan:%h}h {timeSpan:mm}m {timeSpan:ss}s"
: timeSpan.TotalMinutes >= 1 ? $"{timeSpan:%m}m {timeSpan:ss}s"
: $"{seconds} sec";
}
#endregion
#region AudioDecodable event handlers
public override void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
{
base.AudioDecodable_RequestCoverArt(sender, setCoverArtDelegate);
setCoverArtDelegate(GetCoverArtDelegate?.Invoke());
}
private string title;
private string authorNames;
private string narratorNames;
public override void AudioDecodable_TitleDiscovered(object sender, string title)
{
@@ -91,27 +95,20 @@ namespace LibationWinForms.BookLiberation
updateBookInfo();
}
private void updateBookInfo()
=> bookInfoLbl.UIThreadAsync(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
public override void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
{
base.AudioDecodable_RequestCoverArt(sender, setCoverArtDelegate);
setCoverArtDelegate(GetCoverArtDelegate?.Invoke());
}
public override void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
{
base.AudioDecodable_CoverImageDiscovered(sender, coverArt);
pictureBox1.UIThreadAsync(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt));
}
#endregion
// thread-safe UI updates
private void updateBookInfo()
=> bookInfoLbl.UIThreadAsync(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
private void updateRemainingTime(int remaining)
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{formatTime(remaining)}");
private string formatTime(int seconds)
{
var timeSpan = new TimeSpan(0, 0, seconds);
return
timeSpan.TotalHours >= 1 ? $"{timeSpan:%h}h {timeSpan:mm}m {timeSpan:ss}s"
: timeSpan.TotalMinutes >= 1 ? $"{timeSpan:%m}m {timeSpan:ss}s"
: $"{seconds} sec";
}
}
}

View File

@@ -152,11 +152,12 @@ namespace LibationWinForms.BookLiberation.BaseForms
#endregion
#region AudioDecodable event handlers
public virtual void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate) { }
public virtual void AudioDecodable_TitleDiscovered(object sender, string title) { }
public virtual void AudioDecodable_AuthorsDiscovered(object sender, string authors) { }
public virtual void AudioDecodable_NarratorsDiscovered(object sender, string narrators) { }
public virtual void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt) { }
public virtual void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate) { }
#endregion
}
}

View File

@@ -53,7 +53,6 @@ namespace LibationWinForms.Dialogs
.ToList();
QuickFilters.ReplaceAll(list);
_parent.UpdateFilterDropDown();
this.DialogResult = DialogResult.OK;
this.Close();
}

View File

@@ -1,65 +0,0 @@
namespace LibationWinForms.Dialogs
{
partial class IndexLibraryDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(28, 24);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(263, 13);
this.label1.TabIndex = 0;
this.label1.Text = "Scanning Audible library. This may take a few minutes";
//
// IndexLibraryDialog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(440, 63);
this.Controls.Add(this.label1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "IndexLibraryDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Scan Library";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label label1;
}
}

View File

@@ -1,48 +0,0 @@
using System;
using System.Windows.Forms;
using ApplicationServices;
using AudibleUtilities;
using LibationWinForms.Login;
namespace LibationWinForms.Dialogs
{
public partial class IndexLibraryDialog : Form
{
private Account[] _accounts { get; }
public int NewBooksAdded { get; private set; }
public int TotalBooksProcessed { get; private set; }
public IndexLibraryDialog(params Account[] accounts)
{
_accounts = accounts;
InitializeComponent();
this.Shown += IndexLibraryDialog_Shown;
}
private async void IndexLibraryDialog_Shown(object sender, EventArgs e)
{
if (_accounts != null && _accounts.Length > 0)
{
this.label1.Text
= (_accounts.Length == 1)
? "Scanning Audible library. This may take a few minutes."
: $"Scanning Audible library: {_accounts.Length} accounts. This may take a few minutes per account.";
try
{
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync((account) => ApiExtended.CreateAsync(account, new WinformLoginChoiceEager(account)), _accounts);
}
catch (Exception ex)
{
MessageBoxAlertAdmin.Show(
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
"Error importing library",
ex);
}
}
this.Close();
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -17,9 +17,9 @@ namespace LibationWinForms.Dialogs
public partial class RemoveBooksDialog : Form
{
private Account[] _accounts { get; }
private readonly List<LibraryBook> _libraryBooks;
private readonly SortableBindingList<RemovableGridEntry> _removableGridEntries;
private readonly string _labelFormat;
private List<LibraryBook> _libraryBooks { get; }
private SortableBindingList<RemovableGridEntry> _removableGridEntries { get; }
private string _labelFormat { get; }
private int SelectedCount => SelectedEntries?.Count() ?? 0;
private IEnumerable<RemovableGridEntry> SelectedEntries => _removableGridEntries?.Where(b => b.Remove);
@@ -58,7 +58,7 @@ namespace LibationWinForms.Dialogs
private async void RemoveBooksDialog_Shown(object sender, EventArgs e)
{
if (_accounts == null || _accounts.Length == 0)
if (_accounts is null || _accounts.Length == 0)
return;
try
{

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.Dialogs
{
partial class SettingsDialog
{
private void lameTargetRb_CheckedChanged(object sender, EventArgs e)
{
lameBitrateGb.Enabled = lameTargetBitrateRb.Checked;
lameQualityGb.Enabled = !lameTargetBitrateRb.Checked;
}
private void LameMatchSourceBRCbox_CheckedChanged(object sender, EventArgs e)
{
lameBitrateTb.Enabled = !LameMatchSourceBRCbox.Checked;
lameConstantBitrateCbox.Enabled = !LameMatchSourceBRCbox.Checked;
}
private void convertFormatRb_CheckedChanged(object sender, EventArgs e)
{
lameOptionsGb.Enabled = convertLossyRb.Checked;
lameTargetRb_CheckedChanged(sender, e);
LameMatchSourceBRCbox_CheckedChanged(sender, e);
}
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
{
convertLosslessRb.Enabled = allowLibationFixupCbox.Checked;
convertLossyRb.Enabled = allowLibationFixupCbox.Checked;
splitFilesByChapterCbox.Enabled = allowLibationFixupCbox.Checked;
stripUnabridgedCbox.Enabled = allowLibationFixupCbox.Checked;
stripAudibleBrandingCbox.Enabled = allowLibationFixupCbox.Checked;
if (!allowLibationFixupCbox.Checked)
{
convertLosslessRb.Checked = true;
splitFilesByChapterCbox.Checked = false;
stripUnabridgedCbox.Checked = false;
stripAudibleBrandingCbox.Checked = false;
}
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -34,6 +34,10 @@ namespace LibationWinForms.Dialogs
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
this.allowLibationFixupCbox.Text = desc(nameof(config.AllowLibationFixup));
this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter));
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
this.retainAaxFileCbox.Text = desc(nameof(config.RetainAaxFile));
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
this.createCueSheetCbox.Text = desc(nameof(config.CreateCueSheet));
booksSelectControl.SetSearchTitle("books location");
booksSelectControl.SetDirectoryItems(
@@ -47,14 +51,30 @@ namespace LibationWinForms.Dialogs
"Books");
booksSelectControl.SelectDirectory(config.Books);
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
createCueSheetCbox.Checked = config.CreateCueSheet;
retainAaxFileCbox.Checked = config.RetainAaxFile;
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
stripUnabridgedCbox.Checked = config.StripUnabridged;
stripAudibleBrandingCbox.Checked = config.StripAudibleBrandAudio;
convertLosslessRb.Checked = !config.DecryptToLossy;
convertLossyRb.Checked = config.DecryptToLossy;
lameTargetBitrateRb.Checked = config.LameTargetBitrate;
lameTargetQualityRb.Checked = !config.LameTargetBitrate;
lameDownsampleMonoCbox.Checked = config.LameDownsampleMono;
lameBitrateTb.Value = config.LameBitrate;
lameConstantBitrateCbox.Checked = config.LameConstantBitrate;
LameMatchSourceBRCbox.Checked = config.LameMatchSourceBR;
lameVBRQualityTb.Value = config.LameVBRQuality;
showImportedStatsCb.Checked = config.ShowImportedStats;
importEpisodesCb.Checked = config.ImportEpisodes;
downloadEpisodesCb.Checked = config.DownloadEpisodes;
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
convertLosslessRb.Checked = !config.DecryptToLossy;
convertLossyRb.Checked = config.DecryptToLossy;
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
lameTargetRb_CheckedChanged(this, e);
LameMatchSourceBRCbox_CheckedChanged(this, e);
convertFormatRb_CheckedChanged(this, e);
allowLibationFixupCbox_CheckedChanged(this, e);
inProgressSelectControl.SetDirectoryItems(new()
@@ -90,19 +110,6 @@ namespace LibationWinForms.Dialogs
chapterFileTemplateTb.Text = config.ChapterFileTemplate;
}
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
{
convertLosslessRb.Enabled = allowLibationFixupCbox.Checked;
convertLossyRb.Enabled = allowLibationFixupCbox.Checked;
splitFilesByChapterCbox.Enabled = allowLibationFixupCbox.Checked;
if (!allowLibationFixupCbox.Checked)
{
convertLosslessRb.Checked = true;
splitFilesByChapterCbox.Checked = false;
}
}
private void logsBtn_Click(object sender, EventArgs e) => Go.To.Folder(Configuration.Instance.LibationFiles);
private void folderTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.Folder, folderTemplateTb);
@@ -168,12 +175,24 @@ namespace LibationWinForms.Dialogs
MessageBoxVerboseLoggingWarning.ShowIfTrue();
}
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
config.CreateCueSheet = createCueSheetCbox.Checked;
config.RetainAaxFile = retainAaxFileCbox.Checked;
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
config.StripUnabridged = stripUnabridgedCbox.Checked;
config.StripAudibleBrandAudio = stripAudibleBrandingCbox.Checked;
config.DecryptToLossy = convertLossyRb.Checked;
config.LameTargetBitrate = lameTargetBitrateRb.Checked;
config.LameDownsampleMono = lameDownsampleMonoCbox.Checked;
config.LameBitrate = lameBitrateTb.Value;
config.LameConstantBitrate = lameConstantBitrateCbox.Checked;
config.LameMatchSourceBR = LameMatchSourceBRCbox.Checked;
config.LameVBRQuality = lameVBRQualityTb.Value;
config.ShowImportedStats = showImportedStatsCb.Checked;
config.ImportEpisodes = importEpisodesCb.Checked;
config.DownloadEpisodes = downloadEpisodesCb.Checked;
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
config.DecryptToLossy = convertLossyRb.Checked;
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
config.InProgress = inProgressSelectControl.SelectedDirectory;
@@ -197,5 +216,6 @@ namespace LibationWinForms.Dialogs
this.DialogResult = DialogResult.Cancel;
this.Close();
}
}
}

View File

@@ -28,338 +28,366 @@
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.gridPanel = new System.Windows.Forms.Panel();
this.filterHelpBtn = new System.Windows.Forms.Button();
this.filterBtn = new System.Windows.Forms.Button();
this.filterSearchTb = new System.Windows.Forms.TextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeLibraryBooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.convertAllM4bToMp3ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.visibleCountLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.addFilterBtn = new System.Windows.Forms.Button();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
this.SuspendLayout();
//
// gridPanel
//
this.gridPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.gridPanel = new System.Windows.Forms.Panel();
this.filterHelpBtn = new System.Windows.Forms.Button();
this.filterBtn = new System.Windows.Forms.Button();
this.filterSearchTb = new System.Windows.Forms.TextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeLibraryBooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.convertAllM4bToMp3ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.visibleCountLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.addFilterBtn = new System.Windows.Forms.Button();
this.scanningToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
this.SuspendLayout();
//
// gridPanel
//
this.gridPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.gridPanel.Location = new System.Drawing.Point(14, 65);
this.gridPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.gridPanel.Name = "gridPanel";
this.gridPanel.Size = new System.Drawing.Size(979, 445);
this.gridPanel.TabIndex = 5;
//
// filterHelpBtn
//
this.filterHelpBtn.Location = new System.Drawing.Point(14, 31);
this.filterHelpBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterHelpBtn.Name = "filterHelpBtn";
this.filterHelpBtn.Size = new System.Drawing.Size(26, 27);
this.filterHelpBtn.TabIndex = 3;
this.filterHelpBtn.Text = "?";
this.filterHelpBtn.UseVisualStyleBackColor = true;
this.filterHelpBtn.Click += new System.EventHandler(this.filterHelpBtn_Click);
//
// filterBtn
//
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.filterBtn.Location = new System.Drawing.Point(905, 31);
this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterBtn.Name = "filterBtn";
this.filterBtn.Size = new System.Drawing.Size(88, 27);
this.filterBtn.TabIndex = 2;
this.filterBtn.Text = "Filter";
this.filterBtn.UseVisualStyleBackColor = true;
this.filterBtn.Click += new System.EventHandler(this.filterBtn_Click);
//
// filterSearchTb
//
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
this.gridPanel.Location = new System.Drawing.Point(14, 65);
this.gridPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.gridPanel.Name = "gridPanel";
this.gridPanel.Size = new System.Drawing.Size(979, 445);
this.gridPanel.TabIndex = 5;
//
// filterHelpBtn
//
this.filterHelpBtn.Location = new System.Drawing.Point(14, 31);
this.filterHelpBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterHelpBtn.Name = "filterHelpBtn";
this.filterHelpBtn.Size = new System.Drawing.Size(26, 27);
this.filterHelpBtn.TabIndex = 3;
this.filterHelpBtn.Text = "?";
this.filterHelpBtn.UseVisualStyleBackColor = true;
this.filterHelpBtn.Click += new System.EventHandler(this.filterHelpBtn_Click);
//
// filterBtn
//
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.filterBtn.Location = new System.Drawing.Point(905, 31);
this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterBtn.Name = "filterBtn";
this.filterBtn.Size = new System.Drawing.Size(88, 27);
this.filterBtn.TabIndex = 2;
this.filterBtn.Text = "Filter";
this.filterBtn.UseVisualStyleBackColor = true;
this.filterBtn.Click += new System.EventHandler(this.filterBtn_Click);
//
// filterSearchTb
//
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.filterSearchTb.Location = new System.Drawing.Point(217, 33);
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterSearchTb.Name = "filterSearchTb";
this.filterSearchTb.Size = new System.Drawing.Size(681, 23);
this.filterSearchTb.TabIndex = 1;
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
//
// menuStrip1
//
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.filterSearchTb.Location = new System.Drawing.Point(217, 33);
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterSearchTb.Name = "filterSearchTb";
this.filterSearchTb.Size = new System.Drawing.Size(681, 23);
this.filterSearchTb.TabIndex = 1;
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
//
// menuStrip1
//
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.importToolStripMenuItem,
this.liberateToolStripMenuItem,
this.exportToolStripMenuItem,
this.quickFiltersToolStripMenuItem,
this.settingsToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
this.menuStrip1.Size = new System.Drawing.Size(1007, 24);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// importToolStripMenuItem
//
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.settingsToolStripMenuItem,
this.scanningToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
this.menuStrip1.Size = new System.Drawing.Size(1007, 24);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// importToolStripMenuItem
//
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.noAccountsYetAddAccountToolStripMenuItem,
this.scanLibraryToolStripMenuItem,
this.scanLibraryOfAllAccountsToolStripMenuItem,
this.scanLibraryOfSomeAccountsToolStripMenuItem,
this.removeLibraryBooksToolStripMenuItem});
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
this.importToolStripMenuItem.Text = "&Import";
//
// noAccountsYetAddAccountToolStripMenuItem
//
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
this.noAccountsYetAddAccountToolStripMenuItem.Click += new System.EventHandler(this.noAccountsYetAddAccountToolStripMenuItem_Click);
//
// scanLibraryToolStripMenuItem
//
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
this.scanLibraryToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryToolStripMenuItem_Click);
//
// scanLibraryOfAllAccountsToolStripMenuItem
//
this.scanLibraryOfAllAccountsToolStripMenuItem.Name = "scanLibraryOfAllAccountsToolStripMenuItem";
this.scanLibraryOfAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfAllAccountsToolStripMenuItem.Text = "Scan Library of &All Accounts";
this.scanLibraryOfAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfAllAccountsToolStripMenuItem_Click);
//
// scanLibraryOfSomeAccountsToolStripMenuItem
//
this.scanLibraryOfSomeAccountsToolStripMenuItem.Name = "scanLibraryOfSomeAccountsToolStripMenuItem";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts...";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfSomeAccountsToolStripMenuItem_Click);
//
// removeLibraryBooksToolStripMenuItem
//
this.removeLibraryBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
this.importToolStripMenuItem.Text = "&Import";
//
// noAccountsYetAddAccountToolStripMenuItem
//
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
this.noAccountsYetAddAccountToolStripMenuItem.Click += new System.EventHandler(this.noAccountsYetAddAccountToolStripMenuItem_Click);
//
// scanLibraryToolStripMenuItem
//
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
this.scanLibraryToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryToolStripMenuItem_Click);
//
// scanLibraryOfAllAccountsToolStripMenuItem
//
this.scanLibraryOfAllAccountsToolStripMenuItem.Name = "scanLibraryOfAllAccountsToolStripMenuItem";
this.scanLibraryOfAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfAllAccountsToolStripMenuItem.Text = "Scan Library of &All Accounts";
this.scanLibraryOfAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfAllAccountsToolStripMenuItem_Click);
//
// scanLibraryOfSomeAccountsToolStripMenuItem
//
this.scanLibraryOfSomeAccountsToolStripMenuItem.Name = "scanLibraryOfSomeAccountsToolStripMenuItem";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts...";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfSomeAccountsToolStripMenuItem_Click);
//
// removeLibraryBooksToolStripMenuItem
//
this.removeLibraryBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.removeAllAccountsToolStripMenuItem,
this.removeSomeAccountsToolStripMenuItem});
this.removeLibraryBooksToolStripMenuItem.Name = "removeLibraryBooksToolStripMenuItem";
this.removeLibraryBooksToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.removeLibraryBooksToolStripMenuItem.Text = "Remove Library Books";
this.removeLibraryBooksToolStripMenuItem.Click += new System.EventHandler(this.removeLibraryBooksToolStripMenuItem_Click);
//
// removeAllAccountsToolStripMenuItem
//
this.removeAllAccountsToolStripMenuItem.Name = "removeAllAccountsToolStripMenuItem";
this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.removeAllAccountsToolStripMenuItem.Text = "All Accounts";
this.removeAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeAllAccountsToolStripMenuItem_Click);
//
// removeSomeAccountsToolStripMenuItem
//
this.removeSomeAccountsToolStripMenuItem.Name = "removeSomeAccountsToolStripMenuItem";
this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.removeSomeAccountsToolStripMenuItem.Text = "Some Accounts";
this.removeSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeSomeAccountsToolStripMenuItem_Click);
//
// liberateToolStripMenuItem
//
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.removeLibraryBooksToolStripMenuItem.Name = "removeLibraryBooksToolStripMenuItem";
this.removeLibraryBooksToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.removeLibraryBooksToolStripMenuItem.Text = "Remove Library Books";
this.removeLibraryBooksToolStripMenuItem.Click += new System.EventHandler(this.removeLibraryBooksToolStripMenuItem_Click);
//
// removeAllAccountsToolStripMenuItem
//
this.removeAllAccountsToolStripMenuItem.Name = "removeAllAccountsToolStripMenuItem";
this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.removeAllAccountsToolStripMenuItem.Text = "All Accounts";
this.removeAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeAllAccountsToolStripMenuItem_Click);
//
// removeSomeAccountsToolStripMenuItem
//
this.removeSomeAccountsToolStripMenuItem.Name = "removeSomeAccountsToolStripMenuItem";
this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.removeSomeAccountsToolStripMenuItem.Text = "Some Accounts";
this.removeSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeSomeAccountsToolStripMenuItem_Click);
//
// liberateToolStripMenuItem
//
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.beginBookBackupsToolStripMenuItem,
this.beginPdfBackupsToolStripMenuItem,
this.convertAllM4bToMp3ToolStripMenuItem});
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.liberateToolStripMenuItem.Text = "&Liberate";
//
// beginBookBackupsToolStripMenuItem
//
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
//
// beginPdfBackupsToolStripMenuItem
//
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
//
// convertAllM4bToMp3ToolStripMenuItem
//
this.convertAllM4bToMp3ToolStripMenuItem.Name = "convertAllM4bToMp3ToolStripMenuItem";
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all M4b to Mp3 [Long-running]...";
this.convertAllM4bToMp3ToolStripMenuItem.Click += new System.EventHandler(this.convertAllM4bToMp3ToolStripMenuItem_Click);
//
// exportToolStripMenuItem
//
this.exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.liberateToolStripMenuItem.Text = "&Liberate";
//
// beginBookBackupsToolStripMenuItem
//
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
//
// beginPdfBackupsToolStripMenuItem
//
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
//
// convertAllM4bToMp3ToolStripMenuItem
//
this.convertAllM4bToMp3ToolStripMenuItem.Name = "convertAllM4bToMp3ToolStripMenuItem";
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all M4b to Mp3 [Long-running]...";
this.convertAllM4bToMp3ToolStripMenuItem.Click += new System.EventHandler(this.convertAllM4bToMp3ToolStripMenuItem_Click);
//
// exportToolStripMenuItem
//
this.exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.exportLibraryToolStripMenuItem});
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
this.exportToolStripMenuItem.Size = new System.Drawing.Size(53, 20);
this.exportToolStripMenuItem.Text = "E&xport";
//
// exportLibraryToolStripMenuItem
//
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(156, 22);
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
this.exportLibraryToolStripMenuItem.Click += new System.EventHandler(this.exportLibraryToolStripMenuItem_Click);
//
// quickFiltersToolStripMenuItem
//
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
this.exportToolStripMenuItem.Size = new System.Drawing.Size(53, 20);
this.exportToolStripMenuItem.Text = "E&xport";
//
// exportLibraryToolStripMenuItem
//
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(156, 22);
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
this.exportLibraryToolStripMenuItem.Click += new System.EventHandler(this.exportLibraryToolStripMenuItem_Click);
//
// quickFiltersToolStripMenuItem
//
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.firstFilterIsDefaultToolStripMenuItem,
this.editQuickFiltersToolStripMenuItem,
this.toolStripSeparator1});
this.quickFiltersToolStripMenuItem.Name = "quickFiltersToolStripMenuItem";
this.quickFiltersToolStripMenuItem.Size = new System.Drawing.Size(84, 20);
this.quickFiltersToolStripMenuItem.Text = "Quick &Filters";
//
// firstFilterIsDefaultToolStripMenuItem
//
this.firstFilterIsDefaultToolStripMenuItem.Name = "firstFilterIsDefaultToolStripMenuItem";
this.firstFilterIsDefaultToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.firstFilterIsDefaultToolStripMenuItem.Text = "Start Libation with 1st filter &Default";
this.firstFilterIsDefaultToolStripMenuItem.Click += new System.EventHandler(this.FirstFilterIsDefaultToolStripMenuItem_Click);
//
// editQuickFiltersToolStripMenuItem
//
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters...";
this.editQuickFiltersToolStripMenuItem.Click += new System.EventHandler(this.EditQuickFiltersToolStripMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(253, 6);
//
// settingsToolStripMenuItem
//
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.quickFiltersToolStripMenuItem.Name = "quickFiltersToolStripMenuItem";
this.quickFiltersToolStripMenuItem.Size = new System.Drawing.Size(84, 20);
this.quickFiltersToolStripMenuItem.Text = "Quick &Filters";
//
// firstFilterIsDefaultToolStripMenuItem
//
this.firstFilterIsDefaultToolStripMenuItem.Name = "firstFilterIsDefaultToolStripMenuItem";
this.firstFilterIsDefaultToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.firstFilterIsDefaultToolStripMenuItem.Text = "Start Libation with 1st filter &Default";
this.firstFilterIsDefaultToolStripMenuItem.Click += new System.EventHandler(this.FirstFilterIsDefaultToolStripMenuItem_Click);
//
// editQuickFiltersToolStripMenuItem
//
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters...";
this.editQuickFiltersToolStripMenuItem.Click += new System.EventHandler(this.EditQuickFiltersToolStripMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(253, 6);
//
// settingsToolStripMenuItem
//
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.accountsToolStripMenuItem,
this.basicSettingsToolStripMenuItem});
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.settingsToolStripMenuItem.Text = "&Settings";
//
// accountsToolStripMenuItem
//
this.accountsToolStripMenuItem.Name = "accountsToolStripMenuItem";
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.accountsToolStripMenuItem.Text = "&Accounts...";
this.accountsToolStripMenuItem.Click += new System.EventHandler(this.accountsToolStripMenuItem_Click);
//
// basicSettingsToolStripMenuItem
//
this.basicSettingsToolStripMenuItem.Name = "basicSettingsToolStripMenuItem";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.basicSettingsToolStripMenuItem.Text = "&Settings...";
this.basicSettingsToolStripMenuItem.Click += new System.EventHandler(this.basicSettingsToolStripMenuItem_Click);
//
// statusStrip1
//
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.basicSettingsToolStripMenuItem,
this.toolStripSeparator2,
this.aboutToolStripMenuItem});
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.settingsToolStripMenuItem.Text = "&Settings";
//
// accountsToolStripMenuItem
//
this.accountsToolStripMenuItem.Name = "accountsToolStripMenuItem";
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.accountsToolStripMenuItem.Text = "&Accounts...";
this.accountsToolStripMenuItem.Click += new System.EventHandler(this.accountsToolStripMenuItem_Click);
//
// basicSettingsToolStripMenuItem
//
this.basicSettingsToolStripMenuItem.Name = "basicSettingsToolStripMenuItem";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.basicSettingsToolStripMenuItem.Text = "&Settings...";
this.basicSettingsToolStripMenuItem.Click += new System.EventHandler(this.basicSettingsToolStripMenuItem_Click);
//
// toolStripSeparator2
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
this.toolStripSeparator2.Size = new System.Drawing.Size(130, 6);
//
// aboutToolStripMenuItem
//
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.aboutToolStripMenuItem.Text = "A&bout...";
this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click);
//
// statusStrip1
//
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.visibleCountLbl,
this.springLbl,
this.backupsCountsLbl,
this.pdfsCountsLbl});
this.statusStrip1.Location = new System.Drawing.Point(0, 517);
this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0);
this.statusStrip1.Size = new System.Drawing.Size(1007, 22);
this.statusStrip1.TabIndex = 6;
this.statusStrip1.Text = "statusStrip1";
//
// visibleCountLbl
//
this.visibleCountLbl.Name = "visibleCountLbl";
this.visibleCountLbl.Size = new System.Drawing.Size(53, 17);
this.visibleCountLbl.Text = "Visible: 0";
//
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(548, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl
//
this.backupsCountsLbl.Name = "backupsCountsLbl";
this.backupsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.backupsCountsLbl.Text = "[Calculating backed up book quantities]";
//
// pdfsCountsLbl
//
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
this.pdfsCountsLbl.Size = new System.Drawing.Size(171, 17);
this.pdfsCountsLbl.Text = "| [Calculating backed up PDFs]";
//
// addFilterBtn
//
this.addFilterBtn.Location = new System.Drawing.Point(47, 31);
this.addFilterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.addFilterBtn.Name = "addFilterBtn";
this.addFilterBtn.Size = new System.Drawing.Size(163, 27);
this.addFilterBtn.TabIndex = 4;
this.addFilterBtn.Text = "Add To Quick Filters";
this.addFilterBtn.UseVisualStyleBackColor = true;
this.addFilterBtn.Click += new System.EventHandler(this.AddFilterBtn_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1007, 539);
this.Controls.Add(this.filterBtn);
this.Controls.Add(this.addFilterBtn);
this.Controls.Add(this.filterSearchTb);
this.Controls.Add(this.filterHelpBtn);
this.Controls.Add(this.statusStrip1);
this.Controls.Add(this.gridPanel);
this.Controls.Add(this.menuStrip1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MainMenuStrip = this.menuStrip1;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "Form1";
this.Text = "Libation: Liberate your Library";
this.Load += new System.EventHandler(this.Form1_Load);
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
this.statusStrip1.Location = new System.Drawing.Point(0, 517);
this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0);
this.statusStrip1.Size = new System.Drawing.Size(1007, 22);
this.statusStrip1.TabIndex = 6;
this.statusStrip1.Text = "statusStrip1";
//
// visibleCountLbl
//
this.visibleCountLbl.Name = "visibleCountLbl";
this.visibleCountLbl.Size = new System.Drawing.Size(53, 17);
this.visibleCountLbl.Text = "Visible: 0";
//
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(548, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl
//
this.backupsCountsLbl.Name = "backupsCountsLbl";
this.backupsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.backupsCountsLbl.Text = "[Calculating backed up book quantities]";
//
// pdfsCountsLbl
//
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
this.pdfsCountsLbl.Size = new System.Drawing.Size(171, 17);
this.pdfsCountsLbl.Text = "| [Calculating backed up PDFs]";
//
// addFilterBtn
//
this.addFilterBtn.Location = new System.Drawing.Point(47, 31);
this.addFilterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.addFilterBtn.Name = "addFilterBtn";
this.addFilterBtn.Size = new System.Drawing.Size(163, 27);
this.addFilterBtn.TabIndex = 4;
this.addFilterBtn.Text = "Add To Quick Filters";
this.addFilterBtn.UseVisualStyleBackColor = true;
this.addFilterBtn.Click += new System.EventHandler(this.AddFilterBtn_Click);
//
// scanningToolStripMenuItem
//
this.scanningToolStripMenuItem.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
this.scanningToolStripMenuItem.Enabled = false;
this.scanningToolStripMenuItem.Image = global::LibationWinForms.Properties.Resources.import_16x16;
this.scanningToolStripMenuItem.Name = "scanningToolStripMenuItem";
this.scanningToolStripMenuItem.Size = new System.Drawing.Size(93, 20);
this.scanningToolStripMenuItem.Text = "Scanning...";
this.scanningToolStripMenuItem.Visible = false;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1007, 539);
this.Controls.Add(this.filterBtn);
this.Controls.Add(this.addFilterBtn);
this.Controls.Add(this.filterSearchTb);
this.Controls.Add(this.filterHelpBtn);
this.Controls.Add(this.statusStrip1);
this.Controls.Add(this.gridPanel);
this.Controls.Add(this.menuStrip1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MainMenuStrip = this.menuStrip1;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "Form1";
this.Text = "Libation: Liberate your Library";
this.Load += new System.EventHandler(this.Form1_Load);
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
@@ -397,5 +425,8 @@
private System.Windows.Forms.ToolStripMenuItem removeLibraryBooksToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeAllAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeSomeAccountsToolStripMenuItem;
}
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanningToolStripMenuItem;
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using AudibleUtilities;
@@ -34,8 +35,9 @@ namespace LibationWinForms
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
LibraryCommands.LibrarySizeChanged += reloadGridAndUpdateBottomNumbers;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
// used by async migrations to update ui when complete
DataLayer.UserDefinedItem.Batch_ItemChanged += reloadGridAndUpdateBottomNumbers;
QuickFilters.Updated += updateFiltersMenu;
LibraryCommands.ScanBegin += LibraryCommands_ScanBegin;
LibraryCommands.ScanEnd += LibraryCommands_ScanEnd;
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
@@ -43,7 +45,7 @@ namespace LibationWinForms
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
}
private void Form1_Load(object sender, EventArgs e)
private void Form1_Load(object sender, EventArgs e)
{
if (this.DesignMode)
return;
@@ -61,7 +63,7 @@ namespace LibationWinForms
// suppressed filter while init'ing UI
var prev_isProcessingGridSelect = isProcessingGridSelect;
isProcessingGridSelect = true;
this.UIThreadSync(() => setGrid());
this.UIThreadSync(setGrid);
isProcessingGridSelect = prev_isProcessingGridSelect;
// UI init complete. now we can apply filter
@@ -71,22 +73,28 @@ namespace LibationWinForms
}
#region reload grid
private ProductsGrid currProductsGrid;
private ProductsGrid productsGrid;
private void setGrid()
{
SuspendLayout();
{
if (currProductsGrid != null)
// previous non-null grid with zero-count removes columns. remove/re-add grid to get columns back
if (productsGrid?.Count == 0)
{
gridPanel.Controls.Remove(currProductsGrid);
currProductsGrid.VisibleCountChanged -= setVisibleCount;
currProductsGrid.Dispose();
gridPanel.Controls.Remove(productsGrid);
productsGrid.VisibleCountChanged -= setVisibleCount;
productsGrid.Dispose();
productsGrid = null;
}
currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill };
currProductsGrid.VisibleCountChanged += setVisibleCount;
gridPanel.UIThreadSync(() => gridPanel.Controls.Add(currProductsGrid));
currProductsGrid.Display();
if (productsGrid is null)
{
productsGrid = new ProductsGrid { Dock = DockStyle.Fill };
productsGrid.VisibleCountChanged += setVisibleCount;
gridPanel.UIThreadSync(() => gridPanel.Controls.Add(productsGrid));
}
productsGrid.Display();
}
ResumeLayout();
}
@@ -187,11 +195,7 @@ namespace LibationWinForms
#region filter
private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog();
private void AddFilterBtn_Click(object sender, EventArgs e)
{
QuickFilters.Add(this.filterSearchTb.Text);
UpdateFilterDropDown();
}
private void AddFilterBtn_Click(object sender, EventArgs e) => QuickFilters.Add(this.filterSearchTb.Text);
private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e)
{
@@ -214,12 +218,12 @@ namespace LibationWinForms
}
private void doFilter()
{
if (isProcessingGridSelect || currProductsGrid == null)
if (isProcessingGridSelect || productsGrid is null)
return;
try
{
currProductsGrid.Filter(filterSearchTb.Text);
productsGrid.Filter(filterSearchTb.Text);
lastGoodFilter = filterSearchTb.Text;
}
catch (Exception ex)
@@ -255,21 +259,21 @@ namespace LibationWinForms
new AccountsDialog(this).ShowDialog();
}
private void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
scanLibraries(firstAccount);
}
private async void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
await scanLibrariesAsync(firstAccount);
}
private void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
private async void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
scanLibraries(allAccounts);
await scanLibrariesAsync(allAccounts);
}
private void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
private async void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog(this);
@@ -279,7 +283,7 @@ namespace LibationWinForms
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
scanLibraries(scanAccountsDialog.CheckedAccounts);
await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts);
}
private void removeLibraryBooksToolStripMenuItem_Click(object sender, EventArgs e)
@@ -325,21 +329,28 @@ namespace LibationWinForms
dialog.ShowDialog();
}
private void scanLibraries(IEnumerable<Account> accounts) => scanLibraries(accounts.ToArray());
private void scanLibraries(params Account[] accounts)
private async Task scanLibrariesAsync(IEnumerable<Account> accounts) => await scanLibrariesAsync(accounts.ToArray());
private async Task scanLibrariesAsync(params Account[] accounts)
{
using var dialog = new IndexLibraryDialog(accounts);
dialog.ShowDialog();
try
{
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(account => ApiExtended.CreateAsync(account, new Login.WinformLoginChoiceEager(account)), accounts);
var totalProcessed = dialog.TotalBooksProcessed;
var newAdded = dialog.NewBooksAdded;
if (Configuration.Instance.ShowImportedStats)
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
}
catch (Exception ex)
{
MessageBoxAlertAdmin.Show(
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
"Error importing library",
ex);
}
}
#endregion
#region liberate menu
#region Liberate menu
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync();
@@ -400,7 +411,13 @@ namespace LibationWinForms
}
#endregion
#region quick filters menu
#region Quick Filters menu
private void FirstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e)
{
firstFilterIsDefaultToolStripMenuItem.Checked = !firstFilterIsDefaultToolStripMenuItem.Checked;
QuickFilters.UseDefault = firstFilterIsDefaultToolStripMenuItem.Checked;
}
private void loadInitialQuickFilterState()
{
// set inital state. do once only
@@ -410,18 +427,11 @@ namespace LibationWinForms
if (QuickFilters.UseDefault)
doFilter(QuickFilters.Filters.FirstOrDefault());
// do after every save
UpdateFilterDropDown();
}
private void FirstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e)
{
firstFilterIsDefaultToolStripMenuItem.Checked = !firstFilterIsDefaultToolStripMenuItem.Checked;
QuickFilters.UseDefault = firstFilterIsDefaultToolStripMenuItem.Checked;
updateFiltersMenu();
}
private object quickFilterTag { get; } = new object();
public void UpdateFilterDropDown()
private void updateFiltersMenu(object _ = null, object __ = null)
{
// remove old
for (var i = quickFiltersToolStripMenuItem.DropDownItems.Count - 1; i >= 0; i--)
@@ -448,10 +458,37 @@ namespace LibationWinForms
private void EditQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters(this).ShowDialog();
#endregion
#region settings menu
#region Settings menu
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog(this).ShowDialog();
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
#endregion
}
private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
=> MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
#endregion
#region Scanning label
private void LibraryCommands_ScanBegin(object sender, int accountsLength)
{
scanLibraryToolStripMenuItem.Enabled = false;
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = false;
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = false;
this.scanningToolStripMenuItem.Visible = true;
this.scanningToolStripMenuItem.Text
= (accountsLength == 1)
? "Scanning..."
: $"Scanning {accountsLength} accounts...";
}
private void LibraryCommands_ScanEnd(object sender, EventArgs e)
{
scanLibraryToolStripMenuItem.Enabled = true;
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = true;
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = true;
this.scanningToolStripMenuItem.Visible = false;
}
#endregion
}
}

View File

@@ -28,7 +28,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.0.4.1" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.1.6.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -158,136 +158,11 @@ namespace LibationWinForms
/// <summary>migrations which require Forms or are long-running</summary>
private static void RunWindowsOnlyMigrations(Configuration config)
{
// only supported in winforms. don't move to app scaffolding
migrate_to_v5_0_0(config);
// long running. won't get a chance to finish in cli. don't move to app scaffolding
migrate_to_v5_5_0(config);
// examples:
// - only supported in winforms. don't move to app scaffolding
// - long running. won't get a chance to finish in cli. don't move to app scaffolding
}
#region migrate to v5.0.0 re-register device if device info not in settings
private static void migrate_to_v5_0_0(Configuration config)
{
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))
return;
var accountsPersister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = accountsPersister?.AccountsSettings?.Accounts;
if (accounts is null)
return;
foreach (var account in accounts)
{
var identity = account?.IdentityTokens;
if (identity is null)
continue;
if (!string.IsNullOrWhiteSpace(identity.DeviceType) &&
!string.IsNullOrWhiteSpace(identity.DeviceSerialNumber) &&
!string.IsNullOrWhiteSpace(identity.AmazonAccountId))
continue;
var authorize = new Authorize(identity.Locale);
try
{
authorize.DeregisterAsync(identity.ExistingAccessToken, identity.Cookies.ToKeyValuePair()).GetAwaiter().GetResult();
identity.Invalidate();
// re-registers device
ApiExtended.CreateAsync(account, new Login.WinformLoginChoiceEager(account)).GetAwaiter().GetResult();
}
catch
{
// Don't care if it fails
}
}
}
#endregion
#region migrate to v5.5.0. FilePaths.json => db. long running. fire and forget
private static void migrate_to_v5_5_0(Configuration config)
=> new System.Threading.Thread(() => migrate_to_v5_5_0_thread(config)) { IsBackground = true }.Start();
private static void migrate_to_v5_5_0_thread(Configuration config)
{
try
{
var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json");
if (!File.Exists(filePaths))
return;
var fileLocations = Path.Combine(config.LibationFiles, "FileLocations.json");
if (!File.Exists(fileLocations))
File.Copy(filePaths, fileLocations);
// files to be deleted at the end
var libhackFilesToDelete = new List<string>();
// .libhack files => errors
var libhackFiles = Directory.EnumerateDirectories(config.Books, "*.libhack", SearchOption.AllDirectories);
using var context = ApplicationServices.DbContexts.GetContext();
context.Books.Load();
var jArr = JArray.Parse(File.ReadAllText(filePaths));
foreach (var jToken in jArr)
{
var asinToken = jToken["Id"];
var fileTypeToken = jToken["FileType"];
var pathToken = jToken["Path"];
if (asinToken is null || fileTypeToken is null || pathToken is null ||
asinToken.Type != JTokenType.String || fileTypeToken.Type != JTokenType.Integer || pathToken.Type != JTokenType.String)
continue;
var asin = asinToken.Value<string>();
var fileType = (FileType)fileTypeToken.Value<int>();
var path = pathToken.Value<string>();
if (fileType == FileType.Unknown || fileType == FileType.AAXC)
continue;
var book = context.Books.Local.FirstOrDefault(b => b.AudibleProductId == asin);
if (book is null)
continue;
// assign these strings and enums/ints unconditionally. EFCore will only update if changed
if (fileType == FileType.PDF)
book.UserDefinedItem.BatchMode_UpdatePdfStatus(LiberatedStatus.Liberated);
if (fileType == FileType.Audio)
{
var lhack = libhackFiles.FirstOrDefault(f => f.ContainsInsensitive(asin));
if (lhack is null)
book.UserDefinedItem.BatchMode_UpdateBookStatus(LiberatedStatus.Liberated);
else
{
book.UserDefinedItem.BatchMode_UpdateBookStatus(LiberatedStatus.Error);
libhackFilesToDelete.Add(lhack);
}
}
}
// in order: save to db, full reindex from db, refresh ui
var changed = context.SaveChanges();
if (changed > 0)
ApplicationServices.SearchEngineCommands.FullReIndex();
UserDefinedItem.BatchMode_Finalize();
// only do this after save changes
foreach (var libhackFile in libhackFilesToDelete)
File.Delete(libhackFile);
File.Delete(filePaths);
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error attempting to insert FilePaths into db");
}
}
#endregion
private static void checkForUpdate()
{
string zipUrl;

View File

@@ -19,7 +19,7 @@ namespace LibationWinForms.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
@@ -130,6 +130,16 @@ namespace LibationWinForms.Properties {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap import_16x16 {
get {
object obj = ResourceManager.GetObject("import_16x16", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>

View File

@@ -112,12 +112,12 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="default_cover_300x300" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\img-coverart-prod-unavailable_300x300.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
@@ -139,6 +139,9 @@
<data name="edit_tags_50x50" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\edit-tags-50x50.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="import_16x16" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\import_16x16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="liberate_green" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\liberate_green.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -19,7 +19,7 @@ namespace LibationWinForms
protected override void OnListChanged(ListChangedEventArgs e)
{
if (syncContext != null)
if (syncContext is not null)
syncContext.Send(_ => base.OnListChanged(e), null);
else
base.OnListChanged(e);

View File

@@ -13,8 +13,8 @@ namespace LibationWinForms
internal class EditTagsDataGridViewImageButtonCell : DataGridViewImageButtonCell
{
private static readonly Image ButtonImage = Properties.Resources.edit_25x25;
private static readonly Color HiddenForeColor = Color.LightGray;
private static Image ButtonImage { get; } = Properties.Resources.edit_25x25;
private static Color HiddenForeColor { get; } = Color.LightGray;
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{

View File

@@ -10,6 +10,7 @@ using Dinah.Core.DataBinding;
using Dinah.Core;
using Dinah.Core.Drawing;
using LibationFileManager;
using System.Threading.Tasks;
namespace LibationWinForms
{
@@ -24,8 +25,7 @@ namespace LibationWinForms
[Browsable(false)]
public string AudibleProductId => Book.AudibleProductId;
[Browsable(false)]
public LibraryBook LibraryBook { get; }
public LibraryBook LibraryBook { get; private set; }
#endregion
#region Model properties exposed to the view
@@ -40,17 +40,18 @@ namespace LibationWinForms
}
}
public string ProductRating { get; }
public string PurchaseDate { get; }
public string MyRating { get; }
public string Series { get; }
public string Title { get; }
public string Length { get; }
public string Authors { get; }
public string Narrators { get; }
public string Category { get; }
public string Misc { get; }
public string Description { get; }
public bool DownloadInProgress { get; private set; }
public string ProductRating { get; private set; }
public string PurchaseDate { get; private set; }
public string MyRating { get; private set; }
public string Series { get; private set; }
public string Title { get; private set; }
public string Length { get; private set; }
public string Authors { get; private set; }
public string Narrators { get; private set; }
public string Category { get; private set; }
public string Misc { get; private set; }
public string Description { get; private set; }
public string DisplayTags
{
get => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
@@ -68,30 +69,57 @@ namespace LibationWinForms
LibraryBook.Book.UserDefinedItem.PdfStatus = value.PdfStatus;
}
}
#endregion
public event EventHandler<string> LibraryBookUpdated;
public event EventHandler Committed;
// alias
private Book Book => LibraryBook.Book;
public GridEntry(LibraryBook libraryBook)
public GridEntry(LibraryBook libraryBook) => setLibraryBook(libraryBook);
public async Task DownloadBook()
{
if (!DownloadInProgress)
{
try
{
DownloadInProgress = true;
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(LibraryBook);
}
finally
{
DownloadInProgress = false;
}
}
}
public void UpdateLibraryBook(LibraryBook libraryBook)
{
if (AudibleProductId != libraryBook.Book.AudibleProductId)
throw new Exception("Invalid grid entry update. IDs must match");
setLibraryBook(libraryBook);
}
private void setLibraryBook(LibraryBook libraryBook)
{
LibraryBook = libraryBook;
_memberValues = CreateMemberValueDictionary();
//Get cover art. If it's default, subscribe to PictureCached
// Get cover art. If it's default, subscribe to PictureCached
{
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
if (isDefault)
PictureStorage.PictureCached += PictureStorage_PictureCached;
//Mutable property. Set the field so PropertyChanged isn't fired.
// Mutable property. Set the field so PropertyChanged isn't fired.
_cover = ImageReader.ToImage(picture);
}
//Immutable properties
// Immutable properties
{
Title = Book.Title;
Series = Book.SeriesNames;
@@ -107,6 +135,9 @@ namespace LibationWinForms
}
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
// this will never have a value when triggered by ctor b/c nothing can subscribe to the event until after ctor is complete
LibraryBookUpdated?.Invoke(this, AudibleProductId);
}
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
@@ -134,22 +165,16 @@ namespace LibationWinForms
switch (itemName)
{
case nameof(udi.Tags):
{
Book.UserDefinedItem.Tags = udi.Tags;
NotifyPropertyChanged(nameof(DisplayTags));
}
Book.UserDefinedItem.Tags = udi.Tags;
NotifyPropertyChanged(nameof(DisplayTags));
break;
case nameof(udi.BookStatus):
{
Book.UserDefinedItem.BookStatus = udi.BookStatus;
NotifyPropertyChanged(nameof(Liberate));
}
Book.UserDefinedItem.BookStatus = udi.BookStatus;
NotifyPropertyChanged(nameof(Liberate));
break;
case nameof(udi.PdfStatus):
{
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
NotifyPropertyChanged(nameof(Liberate));
}
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
NotifyPropertyChanged(nameof(Liberate));
break;
}
@@ -195,15 +220,15 @@ namespace LibationWinForms
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
public virtual IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
private Dictionary<string, Func<object>> _memberValues { get; }
private Dictionary<string, Func<object>> _memberValues { get; set; }
/// <summary>
/// Create getters for all member object values by name
/// </summary>
private Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
{
{ nameof(Title), () => GetSortName(Book.Title) },
{ nameof(Series), () => GetSortName(Book.SeriesNames) },
{ nameof(Title), () => Book.TitleSortable },
{ nameof(Series), () => Book.SeriesSortable },
{ nameof(Length), () => Book.LengthInMinutes },
{ nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore },
{ nameof(PurchaseDate), () => LibraryBook.DateAdded },
@@ -227,21 +252,6 @@ namespace LibationWinForms
{ typeof(LiberatedStatus), new ObjectComparer<LiberatedStatus>() },
};
private static readonly string[] _sortPrefixIgnores = { "the", "a", "an" };
private static string GetSortName(string unformattedName)
{
var sortName = unformattedName
.Replace("|", "")
.Replace(":", "")
.ToLowerInvariant()
.Trim();
if (_sortPrefixIgnores.Any(prefix => sortName.StartsWith(prefix + " ")))
sortName = sortName.Substring(sortName.IndexOf(" ") + 1).TrimStart();
return sortName;
}
#endregion
#region Static library display functions

View File

@@ -1,5 +1,4 @@
using ApplicationServices;
using System;
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Linq;

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -38,7 +39,7 @@ namespace LibationWinForms
InitializeComponent();
// sorting breaks filters. must reapply filters after sorting
_dataGridView.Sorted += (_, __) => Filter();
_dataGridView.Sorted += Filter;
_dataGridView.CellContentClick += DataGridView_CellContentClick;
EnableDoubleBuffering();
@@ -88,7 +89,7 @@ namespace LibationWinForms
}
// else: liberate
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook);
await liveGridEntry.DownloadBook();
}
private static void Details_Click(GridEntry liveGridEntry)
@@ -109,18 +110,16 @@ namespace LibationWinForms
#region UI display functions
private bool hasBeenDisplayed = false;
public int Count { get; private set; }
private SortableBindingList<GridEntry> bindingList;
public void Display()
{
if (hasBeenDisplayed)
return;
hasBeenDisplayed = true;
//
// transform into sorted GridEntry.s BEFORE binding
//
var lib = DbContexts.GetLibrary_Flat_NoTracking();
Count = lib.Count;
// if no data. hide all columns. return
if (!lib.Any())
{
@@ -129,34 +128,72 @@ namespace LibationWinForms
return;
}
var orderedGridEntries = lib
.Select(lb =>
{
var entry = new GridEntry(lb);
entry.Committed += (_, __) => Filter();
return entry;
}).ToList()
var orderedBooks = lib
// default load order
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
.OrderByDescending(lb => lb.DateAdded)
//// more advanced example: sort by author, then series, then title
//.OrderBy(ge => ge.Authors)
// .ThenBy(ge => ge.Series)
// .ThenBy(ge => ge.Title)
//.OrderBy(lb => lb.Book.AuthorNames)
// .ThenBy(lb => lb.Book.SeriesSortable)
// .ThenBy(lb => lb.Book.TitleSortable)
.ToList();
// BIND
gridEntryBindingSource.DataSource = new SortableBindingList<GridEntry>(orderedGridEntries);
if (bindingList is null)
bindToGrid(orderedBooks);
else
updateGrid(orderedBooks);
// FILTER
Filter();
}
#endregion
private void bindToGrid(List<DataLayer.LibraryBook> orderedBooks)
{
bindingList = new SortableBindingList<GridEntry>(orderedBooks.Select(lb => toGridEntry(lb)));
gridEntryBindingSource.DataSource = bindingList;
}
#region Filter
private void updateGrid(List<DataLayer.LibraryBook> orderedBooks)
{
for (var i = orderedBooks.Count - 1; i >= 0; i--)
{
var libraryBook = orderedBooks[i];
var existingItem = bindingList.FirstOrDefault(i => i.AudibleProductId == libraryBook.Book.AudibleProductId);
private string _filterSearchString;
private void Filter() => Filter(_filterSearchString);
// add new to top
if (existingItem is null)
bindingList.Insert(0, toGridEntry(libraryBook));
// update existing
else
existingItem.UpdateLibraryBook(libraryBook);
}
// remove deleted from grid. note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
var oldIds = bindingList.Select(ge => ge.AudibleProductId).ToList();
var newIds = orderedBooks.Select(lb => lb.Book.AudibleProductId).ToList();
var remove = oldIds.Except(newIds).ToList();
foreach (var id in remove)
{
var oldItem = bindingList.FirstOrDefault(ge => ge.AudibleProductId == id);
if (oldItem is not null)
bindingList.Remove(oldItem);
}
}
private GridEntry toGridEntry(DataLayer.LibraryBook libraryBook)
{
var entry = new GridEntry(libraryBook);
entry.Committed += Filter;
entry.LibraryBookUpdated += (sender, productId) => _dataGridView.InvalidateRow(_dataGridView.GetRowIdOfBoundItem((GridEntry)sender));
return entry;
}
#endregion
#region Filter
private string _filterSearchString;
private void Filter(object _ = null, EventArgs __ = null) => Filter(_filterSearchString);
public void Filter(string searchString)
{
_filterSearchString = searchString;
@@ -178,7 +215,7 @@ namespace LibationWinForms
});
}
//Causes repainting of the DataGridView
// Causes repainting of the DataGridView
bindingContext.ResumeBinding();
VisibleCountChanged?.Invoke(this, _dataGridView.AsEnumerable().Count(r => r.Visible));
}

View File

@@ -92,6 +92,26 @@ Or if you have multiple accounts, you'll get to choose whether to scan all accou
![Import which accounts](images/v40_import.png)
If this is a new installation, or you're scanning an account you haven't scanned before, you'll be prompted to enter your password for the Audible account.
![Login password](images/alt-login1.png)
Enter the password and click Submit. Audible will prompt you with a CAPTCHA image.
![Login captcha](images/alt-login2.png)
Enter the CAPTCHA answer characters and click Submit. If all has gone well, Libation will start scanning the account.
In rare instances, the Captcha image/response will fail in an endless loop. If this happens, delete the problem account, and then click Save. Re-add the account and click Save again. Now try to scan the account again. This time, instead of typing your password, click the link that says "Or click here". This will open the Audible External Login dialog shown below.
![Login alternative setup](images/alt-login3.png)
You can either copy the URL shown and paste it into your browser or launch the browser directly by clicking Launch in Browser. Audible will display its standard login page. Login, including answering the CAPTCHA on the next page. In some cases, you might have to approve the login from the email account associated with that login, but once the login is successful, you'll see an error message.
![Login alternative login result](images/alt-login4.png)
This actually means you've successfully logged in. Copy the entire URL shown in your browser and return to Libation. Paste that URL into the text box at the bottom of the Audible External Login window and click Submit.
You'll see this window while it's scanning:
![Import step 2](images/Import2.png)

View File

@@ -1,17 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -7,10 +7,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,17 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

BIN
images/alt-login1.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
images/alt-login2.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
images/alt-login3.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
images/alt-login4.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB