mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-24 06:28:02 -05:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ee73fa1a7 | ||
|
|
adbbff368f | ||
|
|
ee9d30bd56 | ||
|
|
5a822809a9 | ||
|
|
4e587e0429 | ||
|
|
9a619186fd | ||
|
|
eab6f71a4c | ||
|
|
f68bf2d6b3 | ||
|
|
2afcaebb78 | ||
|
|
458ea6a377 | ||
|
|
0e2997d309 | ||
|
|
420f4b9d5d | ||
|
|
bcfa97219f | ||
|
|
4c66010afe | ||
|
|
05f25a88c6 | ||
|
|
2c6c08fbb5 | ||
|
|
8af60b56b6 | ||
|
|
c0516772a7 | ||
|
|
77de70762c | ||
|
|
7164100cb1 | ||
|
|
9292a62015 | ||
|
|
5280e68da9 | ||
|
|
0f6b0bf9fe | ||
|
|
510ed95590 | ||
|
|
9862593f4a | ||
|
|
d595b62f13 | ||
|
|
12abbb79b1 | ||
|
|
ecaa3b9aab | ||
|
|
ded175f2d2 | ||
|
|
128facec21 | ||
|
|
0bde86ebfd | ||
|
|
28625029cd | ||
|
|
1816bd721c | ||
|
|
68ad627159 | ||
|
|
878a5dd36c | ||
|
|
7c144b8277 | ||
|
|
bca8c3865b | ||
|
|
58102acd35 | ||
|
|
5e577843f7 | ||
|
|
e1d549cead | ||
|
|
323b8f2fb9 | ||
|
|
3dcbcf42ed | ||
|
|
825078abc6 | ||
|
|
6be44966ad | ||
|
|
66da138556 | ||
|
|
e5dd4b856e | ||
|
|
5caa9c5687 | ||
|
|
c8c0ffeb0d | ||
|
|
bfceb58d6b | ||
|
|
2e4c4cf5f7 | ||
|
|
23966c9b00 | ||
|
|
ef73d2243d | ||
|
|
c95feebd39 | ||
|
|
d6601fed83 |
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
AaxDecrypter/DownloadOptions.cs
Normal file
30
AaxDecrypter/DownloadOptions.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
27
DataLayer/Formatters.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
DtoImporterService/_importer notes.txt
Normal file
37
DtoImporterService/_importer notes.txt
Normal 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);
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ namespace LibationWinForms.Dialogs
|
||||
.ToList();
|
||||
QuickFilters.ReplaceAll(list);
|
||||
|
||||
_parent.UpdateFilterDropDown();
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
{
|
||||
|
||||
46
LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs
Normal file
46
LibationWinForms/Dialogs/SettingsDialog.AudioSettings.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1466
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
1466
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
645
LibationWinForms/Form1.Designer.cs
generated
645
LibationWinForms/Form1.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
12
LibationWinForms/Properties/Resources.Designer.cs
generated
12
LibationWinForms/Properties/Resources.Designer.cs
generated
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
LibationWinForms/Resources/import_16x16.png
Normal file
BIN
LibationWinForms/Resources/import_16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 383 B |
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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
|
||||
@@ -1,5 +1,4 @@
|
||||
using ApplicationServices;
|
||||
using System;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using System.Linq;
|
||||
@@ -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));
|
||||
}
|
||||
20
README.md
20
README.md
@@ -92,6 +92,26 @@ Or if you have multiple accounts, you'll get to choose whether to scan all accou
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
Enter the password and click Submit. Audible will prompt you with a CAPTCHA image.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
BIN
images/alt-login1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
BIN
images/alt-login2.png
Normal file
BIN
images/alt-login2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
images/alt-login3.png
Normal file
BIN
images/alt-login3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
images/alt-login4.png
Normal file
BIN
images/alt-login4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Reference in New Issue
Block a user