mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-04 20:08:04 -05:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2eaf26117 | ||
|
|
7c38e18435 | ||
|
|
bfb1dbc69a | ||
|
|
d2ff19e309 | ||
|
|
aa3a7dce06 | ||
|
|
71075838eb | ||
|
|
803a0b7ccf | ||
|
|
d9f3fa825c | ||
|
|
df42ba584e | ||
|
|
9f09a62a1e | ||
|
|
e714179c30 | ||
|
|
db84c9a7d9 | ||
|
|
937bd56fcc | ||
|
|
f29968f379 | ||
|
|
14e14ba9bd | ||
|
|
613c97524a |
@@ -1,5 +1,4 @@
|
||||
using AAXClean;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.StepRunner;
|
||||
@@ -8,157 +7,83 @@ using System.IO;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public enum OutputFormat { Mp4a, Mp3 }
|
||||
public class AaxcDownloadConverter
|
||||
public class AaxcDownloadConverter : AudiobookDownloadBase
|
||||
{
|
||||
public event EventHandler<AppleTags> RetrievedTags;
|
||||
public event EventHandler<byte[]> RetrievedCoverArt;
|
||||
public event EventHandler<DownloadProgress> DecryptProgressUpdate;
|
||||
public event EventHandler<TimeSpan> DecryptTimeRemaining;
|
||||
protected override StepSequence steps { get; }
|
||||
|
||||
public string AppName { get; set; } = nameof(AaxcDownloadConverter);
|
||||
|
||||
private string outputFileName { get; }
|
||||
private string cacheDir { get; }
|
||||
private DownloadLicense downloadLicense { get; }
|
||||
private AaxFile aaxFile;
|
||||
private OutputFormat OutputFormat;
|
||||
|
||||
private StepSequence steps { get; }
|
||||
private NetworkFileStreamPersister nfsPersister;
|
||||
private bool isCanceled { get; set; }
|
||||
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
|
||||
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".aaxc");
|
||||
private OutputFormat OutputFormat { get; }
|
||||
|
||||
public AaxcDownloadConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
|
||||
:base(outFileName, cacheDirectory, dlLic)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
|
||||
outputFileName = outFileName;
|
||||
|
||||
var outDir = Path.GetDirectoryName(outputFileName);
|
||||
if (!Directory.Exists(outDir))
|
||||
throw new ArgumentNullException(nameof(outDir), "Directory does not exist");
|
||||
if (File.Exists(outputFileName))
|
||||
File.Delete(outputFileName);
|
||||
|
||||
if (!Directory.Exists(cacheDirectory))
|
||||
throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist");
|
||||
cacheDir = cacheDirectory;
|
||||
|
||||
downloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
|
||||
OutputFormat = outputFormat;
|
||||
|
||||
steps = new StepSequence
|
||||
{
|
||||
Name = "Download and Convert Aaxc To " + (outputFormat == OutputFormat.Mp4a ? "M4b" : "Mp3"),
|
||||
Name = "Download and Convert Aaxc To " + OutputFormat,
|
||||
|
||||
["Step 1: Get Aaxc Metadata"] = Step1_GetMetadata,
|
||||
["Step 2: Download Decrypted Audiobook"] = Step2_DownloadAndCombine,
|
||||
["Step 2: Download Decrypted Audiobook"] = Step2_DownloadAudiobook,
|
||||
["Step 3: Create Cue"] = Step3_CreateCue,
|
||||
["Step 4: Create Nfo"] = Step4_CreateNfo,
|
||||
["Step 5: Cleanup"] = Step5_Cleanup,
|
||||
["Step 4: Cleanup"] = Step4_Cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setting cover art by this method will insert the art into the audiobook metadata
|
||||
/// </summary>
|
||||
public void SetCoverArt(byte[] coverArt)
|
||||
public override void SetCoverArt(byte[] coverArt)
|
||||
{
|
||||
if (coverArt is null) return;
|
||||
base.SetCoverArt(coverArt);
|
||||
|
||||
aaxFile?.AppleTags.SetCoverArt(coverArt);
|
||||
|
||||
RetrievedCoverArt?.Invoke(this, coverArt);
|
||||
}
|
||||
|
||||
public bool Run()
|
||||
{
|
||||
var (IsSuccess, Elapsed) = steps.Run();
|
||||
protected override bool Step1_GetMetadata()
|
||||
{
|
||||
aaxFile = new AaxFile(InputFileStream);
|
||||
|
||||
if (!IsSuccess)
|
||||
{
|
||||
Console.WriteLine("WARNING-Conversion failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
var speedup = (int)(aaxFile.Duration.TotalSeconds / (long)Elapsed.TotalSeconds);
|
||||
Serilog.Log.Logger.Information($"Speedup is {speedup}x realtime.");
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Step1_GetMetadata()
|
||||
{
|
||||
//Get metadata from the file over http
|
||||
|
||||
if (File.Exists(jsonDownloadState))
|
||||
{
|
||||
try
|
||||
{
|
||||
nfsPersister = 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.
|
||||
nfsPersister.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl));
|
||||
}
|
||||
catch
|
||||
{
|
||||
FileExt.SafeDelete(jsonDownloadState);
|
||||
FileExt.SafeDelete(tempFile);
|
||||
nfsPersister = NewNetworkFilePersister();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
nfsPersister = NewNetworkFilePersister();
|
||||
}
|
||||
|
||||
aaxFile = new AaxFile(nfsPersister.NetworkFileStream);
|
||||
|
||||
RetrievedTags?.Invoke(this, aaxFile.AppleTags);
|
||||
RetrievedCoverArt?.Invoke(this, aaxFile.AppleTags.Cover);
|
||||
OnRetrievedTitle(aaxFile.AppleTags.TitleSansUnabridged);
|
||||
OnRetrievedAuthors(aaxFile.AppleTags.FirstAuthor ?? "[unknown]");
|
||||
OnRetrievedNarrators(aaxFile.AppleTags.Narrator ?? "[unknown]");
|
||||
OnRetrievedCoverArt(aaxFile.AppleTags.Cover);
|
||||
|
||||
return !isCanceled;
|
||||
}
|
||||
private NetworkFileStreamPersister NewNetworkFilePersister()
|
||||
{
|
||||
var headers = new System.Net.WebHeaderCollection
|
||||
{
|
||||
{ "User-Agent", downloadLicense.UserAgent }
|
||||
};
|
||||
|
||||
var networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
|
||||
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
|
||||
}
|
||||
|
||||
public bool Step2_DownloadAndCombine()
|
||||
protected override bool Step2_DownloadAudiobook()
|
||||
{
|
||||
var zeroProgress = new DownloadProgress
|
||||
{
|
||||
BytesReceived = 0,
|
||||
ProgressPercentage = 0,
|
||||
TotalBytesToReceive = nfsPersister.NetworkFileStream.Length
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
};
|
||||
|
||||
DecryptProgressUpdate?.Invoke(this, zeroProgress);
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
|
||||
|
||||
aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV);
|
||||
|
||||
|
||||
if (File.Exists(outputFileName))
|
||||
FileExt.SafeDelete(outputFileName);
|
||||
|
||||
FileStream outFile = File.Open(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
|
||||
aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV);
|
||||
var outputFile = File.Open(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
|
||||
aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
var decryptionResult = OutputFormat == OutputFormat.Mp4a ? aaxFile.ConvertToMp4a(outFile, downloadLicense.ChapterInfo) : aaxFile.ConvertToMp3(outFile);
|
||||
var decryptionResult = OutputFormat == OutputFormat.M4b ? aaxFile.ConvertToMp4a(outputFile, downloadLicense.ChapterInfo) : aaxFile.ConvertToMp3(outputFile);
|
||||
aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
aaxFile.Close();
|
||||
|
||||
downloadLicense.ChapterInfo = aaxFile.Chapters;
|
||||
|
||||
nfsPersister.Dispose();
|
||||
CloseInputFileStream();
|
||||
|
||||
DecryptProgressUpdate?.Invoke(this, zeroProgress);
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
|
||||
return decryptionResult == ConversionResult.NoErrorsDetected && !isCanceled;
|
||||
}
|
||||
@@ -170,61 +95,28 @@ namespace AaxDecrypter
|
||||
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
DecryptTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
|
||||
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
|
||||
double progressPercent = e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
|
||||
|
||||
DecryptProgressUpdate?.Invoke(this,
|
||||
OnDecryptProgressUpdate(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = (long)(nfsPersister.NetworkFileStream.Length * progressPercent),
|
||||
TotalBytesToReceive = nfsPersister.NetworkFileStream.Length
|
||||
ProgressPercentage = 100 * progressPercent,
|
||||
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
});
|
||||
}
|
||||
|
||||
public bool Step3_CreateCue()
|
||||
{
|
||||
// not a critical step. its failure should not prevent future steps from running
|
||||
try
|
||||
{
|
||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), downloadLicense.ChapterInfo));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"{nameof(Step3_CreateCue)}. FAILED");
|
||||
}
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public bool Step4_CreateNfo()
|
||||
{
|
||||
// not a critical step. its failure should not prevent future steps from running
|
||||
try
|
||||
{
|
||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxFile, downloadLicense.ChapterInfo));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"{nameof(Step4_CreateNfo)}. FAILED");
|
||||
}
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public bool Step5_Cleanup()
|
||||
{
|
||||
FileExt.SafeDelete(jsonDownloadState);
|
||||
FileExt.SafeDelete(tempFile);
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
public override void Cancel()
|
||||
{
|
||||
isCanceled = true;
|
||||
aaxFile?.Cancel();
|
||||
aaxFile?.Dispose();
|
||||
nfsPersister?.NetworkFileStream?.Close();
|
||||
nfsPersister?.Dispose();
|
||||
CloseInputFileStream();
|
||||
}
|
||||
}
|
||||
|
||||
protected override int GetSpeedup(TimeSpan elapsed)
|
||||
=> (int)(aaxFile.Duration.TotalSeconds / (long)elapsed.TotalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
165
AaxDecrypter/AudiobookDownloadBase.cs
Normal file
165
AaxDecrypter/AudiobookDownloadBase.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.StepRunner;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public enum OutputFormat { M4b, Mp3 }
|
||||
|
||||
public abstract class AudiobookDownloadBase
|
||||
{
|
||||
public event EventHandler<string> RetrievedTitle;
|
||||
public event EventHandler<string> RetrievedAuthors;
|
||||
public event EventHandler<string> RetrievedNarrators;
|
||||
public event EventHandler<byte[]> RetrievedCoverArt;
|
||||
public event EventHandler<DownloadProgress> DecryptProgressUpdate;
|
||||
public event EventHandler<TimeSpan> DecryptTimeRemaining;
|
||||
|
||||
public string AppName { get; set; }
|
||||
|
||||
protected bool isCanceled { get; set; }
|
||||
protected string outputFileName { get; }
|
||||
protected string cacheDir { get; }
|
||||
protected DownloadLicense downloadLicense { get; }
|
||||
protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream;
|
||||
|
||||
|
||||
protected abstract StepSequence steps { get; }
|
||||
private NetworkFileStreamPersister nfsPersister;
|
||||
|
||||
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
|
||||
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".tmp");
|
||||
|
||||
public AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
|
||||
{
|
||||
AppName = GetType().Name;
|
||||
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
|
||||
outputFileName = outFileName;
|
||||
|
||||
var outDir = Path.GetDirectoryName(outputFileName);
|
||||
if (!Directory.Exists(outDir))
|
||||
throw new ArgumentNullException(nameof(outDir), "Directory does not exist");
|
||||
if (File.Exists(outputFileName))
|
||||
File.Delete(outputFileName);
|
||||
|
||||
if (!Directory.Exists(cacheDirectory))
|
||||
throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist");
|
||||
cacheDir = cacheDirectory;
|
||||
|
||||
downloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
|
||||
}
|
||||
|
||||
public abstract void Cancel();
|
||||
protected abstract int GetSpeedup(TimeSpan elapsed);
|
||||
protected abstract bool Step2_DownloadAudiobook();
|
||||
protected abstract bool Step1_GetMetadata();
|
||||
|
||||
public virtual void SetCoverArt(byte[] coverArt)
|
||||
{
|
||||
if (coverArt is null) return;
|
||||
|
||||
OnRetrievedCoverArt(coverArt);
|
||||
}
|
||||
|
||||
|
||||
public bool Run()
|
||||
{
|
||||
var (IsSuccess, Elapsed) = steps.Run();
|
||||
|
||||
if (!IsSuccess)
|
||||
{
|
||||
Console.WriteLine("WARNING-Conversion failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information($"Speedup is {GetSpeedup(Elapsed)}x realtime.");
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void OnRetrievedTitle(string title)
|
||||
=> RetrievedTitle?.Invoke(this, title);
|
||||
protected void OnRetrievedAuthors(string authors)
|
||||
=> RetrievedAuthors?.Invoke(this, authors);
|
||||
protected void OnRetrievedNarrators(string narrators)
|
||||
=> RetrievedNarrators?.Invoke(this, narrators);
|
||||
protected void OnRetrievedCoverArt(byte[] coverArt)
|
||||
=> RetrievedCoverArt?.Invoke(this, coverArt);
|
||||
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
|
||||
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
|
||||
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
|
||||
=> DecryptTimeRemaining?.Invoke(this, timeRemaining);
|
||||
|
||||
protected void CloseInputFileStream()
|
||||
{
|
||||
nfsPersister?.NetworkFileStream?.Close();
|
||||
nfsPersister?.Dispose();
|
||||
}
|
||||
|
||||
protected bool Step3_CreateCue()
|
||||
{
|
||||
// not a critical step. its failure should not prevent future steps from running
|
||||
try
|
||||
{
|
||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), downloadLicense.ChapterInfo));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"{nameof(Step3_CreateCue)}. FAILED");
|
||||
}
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
protected bool Step4_Cleanup()
|
||||
{
|
||||
FileExt.SafeDelete(jsonDownloadState);
|
||||
FileExt.SafeDelete(tempFile);
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
private NetworkFileStreamPersister OpenNetworkFileStream()
|
||||
{
|
||||
NetworkFileStreamPersister nfsp;
|
||||
|
||||
if (File.Exists(jsonDownloadState))
|
||||
{
|
||||
try
|
||||
{
|
||||
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));
|
||||
}
|
||||
catch
|
||||
{
|
||||
FileExt.SafeDelete(jsonDownloadState);
|
||||
FileExt.SafeDelete(tempFile);
|
||||
nfsp = NewNetworkFilePersister();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
nfsp = NewNetworkFilePersister();
|
||||
}
|
||||
return nfsp;
|
||||
}
|
||||
|
||||
private NetworkFileStreamPersister NewNetworkFilePersister()
|
||||
{
|
||||
var headers = new System.Net.WebHeaderCollection
|
||||
{
|
||||
{ "User-Agent", downloadLicense.UserAgent }
|
||||
};
|
||||
|
||||
var networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
|
||||
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,15 +13,12 @@ namespace AaxDecrypter
|
||||
|
||||
public DownloadLicense(string downloadUrl, string audibleKey, string audibleIV, string userAgent)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
||||
ArgumentValidator.EnsureNotNullOrEmpty(audibleKey, nameof(audibleKey));
|
||||
ArgumentValidator.EnsureNotNullOrEmpty(audibleIV, nameof(audibleIV));
|
||||
ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
|
||||
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
||||
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
|
||||
|
||||
DownloadUrl = downloadUrl;
|
||||
// no null/empty check. unencrypted files do not have these
|
||||
AudibleKey = audibleKey;
|
||||
AudibleIV = audibleIV;
|
||||
UserAgent = userAgent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
using AAXClean;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public static class NFO
|
||||
{
|
||||
public static string CreateContents(string ripper, Mp4File aaxcTagLib, ChapterInfo chapters)
|
||||
{
|
||||
var _hours = (int)aaxcTagLib.Duration.TotalHours;
|
||||
var myDuration
|
||||
= (_hours > 0 ? _hours + " hours, " : string.Empty)
|
||||
+ aaxcTagLib.Duration.Minutes + " minutes, "
|
||||
+ aaxcTagLib.Duration.Seconds + " seconds";
|
||||
|
||||
var nfoString
|
||||
= "General Information\r\n"
|
||||
+ "======================\r\n"
|
||||
+ $" Title: {aaxcTagLib.AppleTags.TitleSansUnabridged?.UnicodeToAscii() ?? "[unknown]"}\r\n"
|
||||
+ $" Author: {aaxcTagLib.AppleTags.FirstAuthor?.UnicodeToAscii() ?? "[unknown]"}\r\n"
|
||||
+ $" Read By: {aaxcTagLib.AppleTags.Narrator?.UnicodeToAscii() ?? "[unknown]"}\r\n"
|
||||
+ $" Release Date: {aaxcTagLib.AppleTags.ReleaseDate ?? "[unknown]"}\r\n"
|
||||
+ $" Book Copyright: {aaxcTagLib.AppleTags.BookCopyright ?? "[unknown]"}\r\n"
|
||||
+ $" Recording Copyright: {aaxcTagLib.AppleTags.RecordingCopyright ?? "[unknown]"}\r\n"
|
||||
+ $" Genre: {aaxcTagLib.AppleTags.Generes ?? "[unknown]"}\r\n"
|
||||
+ $" Publisher: {aaxcTagLib.AppleTags.Publisher ?? "[unknown]"}\r\n"
|
||||
+ $" Duration: {myDuration}\r\n"
|
||||
+ $" Chapters: {chapters.Count}\r\n"
|
||||
+ "\r\n"
|
||||
+ "\r\n"
|
||||
+ "Media Information\r\n"
|
||||
+ "======================\r\n"
|
||||
+ " Source Format: Audible AAXC\r\n"
|
||||
+ $" Source Sample Rate: {aaxcTagLib.TimeScale} Hz\r\n"
|
||||
+ $" Source Channels: {aaxcTagLib.AudioChannels}\r\n"
|
||||
+ $" Source Bitrate: {aaxcTagLib.AverageBitrate} Kbps\r\n"
|
||||
+ "\r\n"
|
||||
+ " Lossless Encode: Yes\r\n"
|
||||
+ " Encoded Codec: AAC / M4B\r\n"
|
||||
+ $" Encoded Sample Rate: {aaxcTagLib.TimeScale} Hz\r\n"
|
||||
+ $" Encoded Channels: {aaxcTagLib.AudioChannels}\r\n"
|
||||
+ $" Encoded Bitrate: {aaxcTagLib.AverageBitrate} Kbps\r\n"
|
||||
+ "\r\n"
|
||||
+ $" Ripper: {ripper}\r\n"
|
||||
+ "\r\n"
|
||||
+ "\r\n"
|
||||
+ "Book Description\r\n"
|
||||
+ "================\r\n"
|
||||
+ (!string.IsNullOrWhiteSpace(aaxcTagLib.AppleTags.LongDescription) ? aaxcTagLib.AppleTags.LongDescription.UnicodeToAscii() : aaxcTagLib.AppleTags.Comment?.UnicodeToAscii());
|
||||
|
||||
return nfoString;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ namespace AaxDecrypter
|
||||
private FileStream _readFile { get; }
|
||||
private Stream _networkStream { get; set; }
|
||||
private bool hasBegunDownloading { get; set; }
|
||||
private bool isCancelled { get; set; }
|
||||
public bool IsCancelled { get; private set; }
|
||||
private EventWaitHandle downloadEnded { get; set; }
|
||||
private EventWaitHandle downloadedPiece { get; set; }
|
||||
|
||||
@@ -238,7 +238,7 @@ namespace AaxDecrypter
|
||||
downloadedPiece.Set();
|
||||
}
|
||||
|
||||
} while (downloadPosition < ContentLength && !isCancelled);
|
||||
} while (downloadPosition < ContentLength && !IsCancelled);
|
||||
|
||||
_writeFile.Close();
|
||||
_networkStream.Close();
|
||||
@@ -248,7 +248,7 @@ namespace AaxDecrypter
|
||||
downloadedPiece.Set();
|
||||
downloadEnded.Set();
|
||||
|
||||
if (!isCancelled && WritePosition < ContentLength)
|
||||
if (!IsCancelled && WritePosition < ContentLength)
|
||||
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
|
||||
if (WritePosition > ContentLength)
|
||||
@@ -421,12 +421,12 @@ namespace AaxDecrypter
|
||||
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
|
||||
private void WaitToPosition(long requiredPosition)
|
||||
{
|
||||
while (requiredPosition > WritePosition && !isCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
|
||||
while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
isCancelled = true;
|
||||
IsCancelled = true;
|
||||
|
||||
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
|
||||
|
||||
|
||||
86
AaxDecrypter/UnencryptedAudiobookDownloader.cs
Normal file
86
AaxDecrypter/UnencryptedAudiobookDownloader.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.StepRunner;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
|
||||
{
|
||||
protected override StepSequence steps { get; }
|
||||
|
||||
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadLicense dlLic)
|
||||
: base(outFileName, cacheDirectory, dlLic)
|
||||
{
|
||||
|
||||
steps = new StepSequence
|
||||
{
|
||||
Name = "Download Mp3 Audiobook",
|
||||
|
||||
["Step 1: Get Mp3 Metadata"] = Step1_GetMetadata,
|
||||
["Step 2: Download Audiobook"] = Step2_DownloadAudiobook,
|
||||
["Step 3: Create Cue"] = Step3_CreateCue,
|
||||
["Step 4: Cleanup"] = Step4_Cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
public override void Cancel()
|
||||
{
|
||||
isCanceled = true;
|
||||
CloseInputFileStream();
|
||||
}
|
||||
|
||||
protected override int GetSpeedup(TimeSpan elapsed)
|
||||
{
|
||||
//Not implemented
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected override bool Step1_GetMetadata()
|
||||
{
|
||||
OnRetrievedCoverArt(null);
|
||||
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
protected override bool Step2_DownloadAudiobook()
|
||||
{
|
||||
DateTime startTime = DateTime.Now;
|
||||
|
||||
//MUST put InputFileStream.Length first, because it starts background downloader.
|
||||
|
||||
while (InputFileStream.Length > InputFileStream.WritePosition && !InputFileStream.IsCancelled)
|
||||
{
|
||||
var rate = InputFileStream.WritePosition / (DateTime.Now - startTime).TotalSeconds;
|
||||
|
||||
var estTimeRemaining = (InputFileStream.Length - InputFileStream.WritePosition) / rate;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
var progressPercent = (double)InputFileStream.WritePosition / InputFileStream.Length;
|
||||
|
||||
OnDecryptProgressUpdate(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = 100 * progressPercent,
|
||||
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
});
|
||||
Thread.Sleep(200);
|
||||
}
|
||||
|
||||
CloseInputFileStream();
|
||||
|
||||
if (File.Exists(outputFileName))
|
||||
FileExt.SafeDelete(outputFileName);
|
||||
|
||||
FileExt.SafeMove(InputFileStream.SaveFilePath, outputFileName);
|
||||
|
||||
return !isCanceled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Version>6.0.4.1</Version>
|
||||
<Version>6.1.1.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -192,6 +192,7 @@ namespace ApplicationServices
|
||||
|
||||
var removeLibraryBooks = libBooks.Where(lb => idsToRemove.Contains(lb.Book.AudibleProductId)).ToList();
|
||||
context.LibraryBooks.RemoveRange(removeLibraryBooks);
|
||||
context.Books.RemoveRange(removeLibraryBooks.Select(lb => lb.Book));
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
|
||||
@@ -99,8 +99,8 @@ namespace DataLayer
|
||||
Category = category;
|
||||
|
||||
// simple assigns
|
||||
Title = title.Trim();
|
||||
Description = description?.Trim();
|
||||
Title = title.Trim() ?? "";
|
||||
Description = description?.Trim() ?? "";
|
||||
LengthInMinutes = lengthInMinutes;
|
||||
ContentType = contentType;
|
||||
|
||||
|
||||
@@ -106,6 +106,11 @@ namespace DataLayer
|
||||
#endregion
|
||||
|
||||
#region LiberatedStatuses
|
||||
/// <summary>
|
||||
/// Occurs when <see cref="Tags"/>, <see cref="BookStatus"/>, or <see cref="PdfStatus"/> values change.
|
||||
/// This signals the change of the in-memory value; it does not ensure that the new value has been persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<string> ItemChanged;
|
||||
|
||||
private LiberatedStatus _bookStatus;
|
||||
private LiberatedStatus? _pdfStatus;
|
||||
@@ -132,13 +137,41 @@ namespace DataLayer
|
||||
ItemChanged?.Invoke(this, nameof(PdfStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
#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
|
||||
/// <summary>
|
||||
/// Occurs when <see cref="Tags"/>, <see cref="BookStatus"/>, or <see cref="PdfStatus"/> values change.
|
||||
/// This signals the change of the in-memory value; it does not ensure that the new value has been persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<string> ItemChanged;
|
||||
|
||||
public override string ToString() => $"{Book} {Rating} {Tags}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,21 +29,6 @@ namespace FileLiberator
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public ConvertToMp3()
|
||||
{
|
||||
RequestCoverArt += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
|
||||
TitleDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(TitleDiscovered), Title = e });
|
||||
AuthorsDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(AuthorsDiscovered), Authors = e });
|
||||
NarratorsDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(NarratorsDiscovered), Narrators = e });
|
||||
CoverImageDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(CoverImageDiscovered), CoverImageBytes = e?.Length });
|
||||
|
||||
StreamingBegin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingBegin), Message = e });
|
||||
StreamingCompleted += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = e });
|
||||
|
||||
Begin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Begin), Book = e.LogFriendly() });
|
||||
Completed += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = e.LogFriendly() });
|
||||
}
|
||||
|
||||
private long fileSize;
|
||||
private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3");
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace FileLiberator
|
||||
{
|
||||
public class DownloadDecryptBook : IAudioDecodable
|
||||
{
|
||||
private AaxcDownloadConverter aaxcDownloader;
|
||||
private AudiobookDownloadBase aaxcDownloader;
|
||||
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
public event EventHandler<Action<byte[]>> RequestCoverArt;
|
||||
@@ -30,21 +30,6 @@ namespace FileLiberator
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public DownloadDecryptBook()
|
||||
{
|
||||
RequestCoverArt += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
|
||||
TitleDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(TitleDiscovered), Title = e });
|
||||
AuthorsDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(AuthorsDiscovered), Authors = e });
|
||||
NarratorsDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(NarratorsDiscovered), Narrators = e });
|
||||
CoverImageDiscovered += (o, e) => Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(CoverImageDiscovered), CoverImageBytes = e?.Length });
|
||||
|
||||
StreamingBegin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingBegin), Message = e });
|
||||
StreamingCompleted += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = e });
|
||||
|
||||
Begin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Begin), Book = e.LogFriendly() });
|
||||
Completed += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = e.LogFriendly() });
|
||||
}
|
||||
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
@@ -54,7 +39,7 @@ namespace FileLiberator
|
||||
if (libraryBook.Book.Audio_Exists)
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
|
||||
var outputAudioFilename = await downloadAudiobookAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
|
||||
|
||||
// decrypt failed
|
||||
if (outputAudioFilename is null)
|
||||
@@ -76,7 +61,7 @@ namespace FileLiberator
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> aaxToM4bConverterDecryptAsync(string cacheDir, string destinationDir, LibraryBook libraryBook)
|
||||
private async Task<string> downloadAudiobookAsync(string cacheDir, string destinationDir, LibraryBook libraryBook)
|
||||
{
|
||||
StreamingBegin?.Invoke(this, $"Begin decrypting {libraryBook}");
|
||||
|
||||
@@ -87,7 +72,7 @@ namespace FileLiberator
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
var aaxcDecryptDlLic = new DownloadLicense
|
||||
var audiobookDlLic = new DownloadLicense
|
||||
(
|
||||
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
|
||||
contentLic?.Voucher?.Key,
|
||||
@@ -95,32 +80,31 @@ namespace FileLiberator
|
||||
Resources.USER_AGENT
|
||||
);
|
||||
|
||||
if (Configuration.Instance.AllowLibationFixup)
|
||||
//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)
|
||||
{
|
||||
aaxcDecryptDlLic.ChapterInfo = new AAXClean.ChapterInfo();
|
||||
audiobookDlLic.ChapterInfo = new AAXClean.ChapterInfo();
|
||||
|
||||
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
|
||||
aaxcDecryptDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
|
||||
audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
|
||||
}
|
||||
|
||||
var outFileName = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{outputFormat.ToString().ToLower()}");
|
||||
|
||||
var format = Configuration.Instance.DecryptToLossy ? OutputFormat.Mp3 : OutputFormat.Mp4a;
|
||||
|
||||
var extension = format switch
|
||||
{
|
||||
OutputFormat.Mp4a => "m4b",
|
||||
OutputFormat.Mp3 => "mp3",
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
var outFileName = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{extension}");
|
||||
|
||||
|
||||
aaxcDownloader = new AaxcDownloadConverter(outFileName, cacheDir, aaxcDecryptDlLic, format) { AppName = "Libation" };
|
||||
aaxcDownloader = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm ? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat) { AppName = "Libation" } : new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic);
|
||||
aaxcDownloader.DecryptProgressUpdate += (s, progress) => StreamingProgressChanged?.Invoke(this, progress);
|
||||
aaxcDownloader.DecryptTimeRemaining += (s, remaining) => StreamingTimeRemaining?.Invoke(this, remaining);
|
||||
aaxcDownloader.RetrievedTitle += (s, title) => TitleDiscovered?.Invoke(this, title);
|
||||
aaxcDownloader.RetrievedAuthors += (s, authors) => AuthorsDiscovered?.Invoke(this, authors);
|
||||
aaxcDownloader.RetrievedNarrators += (s, narrators) => NarratorsDiscovered?.Invoke(this, narrators);
|
||||
aaxcDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||
aaxcDownloader.RetrievedTags += aaxcDownloader_RetrievedTags;
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await Task.Run(() => aaxcDownloader.Run());
|
||||
@@ -137,7 +121,6 @@ namespace FileLiberator
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
|
||||
{
|
||||
if (e is null && Configuration.Instance.AllowLibationFixup)
|
||||
@@ -151,13 +134,6 @@ namespace FileLiberator
|
||||
}
|
||||
}
|
||||
|
||||
private void aaxcDownloader_RetrievedTags(object sender, AAXClean.AppleTags e)
|
||||
{
|
||||
TitleDiscovered?.Invoke(this, e.TitleSansUnabridged);
|
||||
AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]");
|
||||
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
|
||||
}
|
||||
|
||||
private static (string destinationDir, bool movedAudioFile) MoveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||
{
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
|
||||
@@ -13,12 +13,6 @@ namespace FileLiberator
|
||||
public event EventHandler<string> StreamingCompleted;
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
|
||||
public DownloadFile()
|
||||
{
|
||||
StreamingBegin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingBegin), Message = e });
|
||||
StreamingCompleted += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = e });
|
||||
}
|
||||
|
||||
public async Task<string> PerformDownloadFileAsync(string downloadUrl, string proposedDownloadFilePath)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
|
||||
@@ -17,15 +17,6 @@ namespace FileLiberator
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !libraryBook.Book.PDF_Exists;
|
||||
|
||||
public DownloadPdf()
|
||||
{
|
||||
StreamingBegin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingBegin), Message = e });
|
||||
StreamingCompleted += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = e });
|
||||
|
||||
Begin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Begin), Book = e.LogFriendly() });
|
||||
Completed += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = e.LogFriendly() });
|
||||
}
|
||||
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||
|
||||
@@ -121,6 +121,7 @@ namespace InternalUtilities
|
||||
#if DEBUG
|
||||
//// this will not work for multi accounts
|
||||
//var library_json = "library.json";
|
||||
//library_json = System.IO.Path.GetFullPath(library_json);
|
||||
//if (System.IO.File.Exists(library_json))
|
||||
//{
|
||||
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
|
||||
@@ -128,12 +129,13 @@ namespace InternalUtilities
|
||||
#endif
|
||||
if (!items.Any())
|
||||
items = await Api.GetAllLibraryItemsAsync(responseGroups);
|
||||
#if DEBUG
|
||||
//System.IO.File.WriteAllText("library.json", AudibleApi.Common.Converter.ToJson(items));
|
||||
#endif
|
||||
|
||||
await manageEpisodesAsync(items);
|
||||
|
||||
#if DEBUG
|
||||
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
|
||||
#endif
|
||||
|
||||
var validators = new List<IValidator>();
|
||||
validators.AddRange(getValidators());
|
||||
foreach (var v in validators)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="2.1.3.1" />
|
||||
<PackageReference Include="AudibleApi" Version="2.2.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -315,7 +315,7 @@ namespace LibationSearchEngine
|
||||
var docs = searcher.Search(query, 1);
|
||||
var scoreDoc = docs.ScoreDocs.SingleOrDefault();
|
||||
if (scoreDoc == null)
|
||||
throw new Exception("document not found");
|
||||
return;
|
||||
var document = searcher.Doc(scoreDoc.Doc);
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,10 @@ namespace LibationWinForms.BookLiberation
|
||||
base.OnBegin(sender, libraryBook);
|
||||
}
|
||||
public override void OnCompleted(object sender, LibraryBook libraryBook)
|
||||
=> LogMe.Info($"Convert Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
{
|
||||
base.OnCompleted(sender, libraryBook);
|
||||
LogMe.Info($"Convert Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ namespace LibationWinForms.BookLiberation
|
||||
#region IProcessable event handler overrides
|
||||
public override void OnBegin(object sender, LibraryBook libraryBook)
|
||||
{
|
||||
base.OnBegin(sender, libraryBook);
|
||||
|
||||
GetCoverArtDelegate = () => FileManager.PictureStorage.GetPictureSynchronously(
|
||||
new FileManager.PictureDefinition(
|
||||
libraryBook.Book.PictureId,
|
||||
@@ -41,6 +43,7 @@ namespace LibationWinForms.BookLiberation
|
||||
#region IStreamable event handler overrides
|
||||
public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress)
|
||||
{
|
||||
base.OnStreamingProgressChanged(sender, downloadProgress);
|
||||
if (!downloadProgress.ProgressPercentage.HasValue)
|
||||
return;
|
||||
|
||||
@@ -51,16 +54,23 @@ namespace LibationWinForms.BookLiberation
|
||||
}
|
||||
|
||||
public override void OnStreamingTimeRemaining(object sender, TimeSpan timeRemaining)
|
||||
=> updateRemainingTime((int)timeRemaining.TotalSeconds);
|
||||
{
|
||||
base.OnStreamingTimeRemaining(sender, timeRemaining);
|
||||
updateRemainingTime((int)timeRemaining.TotalSeconds);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IAudioDecodable event handlers
|
||||
public override void OnRequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
|
||||
=> setCoverArtDelegate(GetCoverArtDelegate?.Invoke());
|
||||
{
|
||||
base.OnRequestCoverArt(sender, setCoverArtDelegate);
|
||||
setCoverArtDelegate(GetCoverArtDelegate?.Invoke());
|
||||
}
|
||||
|
||||
public override void OnTitleDiscovered(object sender, string title)
|
||||
{
|
||||
base.OnTitleDiscovered(sender, title);
|
||||
this.UIThreadAsync(() => this.Text = DecodeActionName + " " + title);
|
||||
this.title = title;
|
||||
updateBookInfo();
|
||||
@@ -68,18 +78,23 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
public override void OnAuthorsDiscovered(object sender, string authors)
|
||||
{
|
||||
base.OnAuthorsDiscovered(sender, authors);
|
||||
authorNames = authors;
|
||||
updateBookInfo();
|
||||
}
|
||||
|
||||
public override void OnNarratorsDiscovered(object sender, string narrators)
|
||||
{
|
||||
base.OnNarratorsDiscovered(sender, narrators);
|
||||
narratorNames = narrators;
|
||||
updateBookInfo();
|
||||
}
|
||||
|
||||
public override void OnCoverImageDiscovered(object sender, byte[] coverArt)
|
||||
=> pictureBox1.UIThreadAsync(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt));
|
||||
public override void OnCoverImageDiscovered(object sender, byte[] coverArt)
|
||||
{
|
||||
base.OnCoverImageDiscovered(sender, coverArt);
|
||||
pictureBox1.UIThreadAsync(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt));
|
||||
}
|
||||
#endregion
|
||||
|
||||
// thread-safe UI updates
|
||||
|
||||
@@ -24,7 +24,10 @@ namespace LibationWinForms.BookLiberation
|
||||
base.OnBegin(sender, libraryBook);
|
||||
}
|
||||
public override void OnCompleted(object sender, LibraryBook libraryBook)
|
||||
=> LogMe.Info($"Download & Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
{
|
||||
base.OnCompleted(sender, libraryBook);
|
||||
LogMe.Info($"Download & Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -137,24 +137,36 @@ namespace LibationWinForms.BookLiberation.BaseForms
|
||||
#endregion
|
||||
|
||||
#region IStreamable event handlers
|
||||
public virtual void OnStreamingBegin(object sender, string beginString) { }
|
||||
public virtual void OnStreamingBegin(object sender, string beginString)
|
||||
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IStreamable.StreamingBegin), Message = beginString });
|
||||
public virtual void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress) { }
|
||||
public virtual void OnStreamingTimeRemaining(object sender, TimeSpan timeRemaining) { }
|
||||
public virtual void OnStreamingCompleted(object sender, string completedString) { }
|
||||
public virtual void OnStreamingCompleted(object sender, string completedString)
|
||||
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IStreamable.StreamingCompleted), Message = completedString });
|
||||
|
||||
#endregion
|
||||
|
||||
#region IProcessable event handlers
|
||||
public virtual void OnBegin(object sender, LibraryBook libraryBook) { }
|
||||
public virtual void OnStatusUpdate(object sender, string statusUpdate) { }
|
||||
public virtual void OnCompleted(object sender, LibraryBook libraryBook) { }
|
||||
public virtual void OnBegin(object sender, LibraryBook libraryBook)
|
||||
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IProcessable.Begin), Book = libraryBook.LogFriendly() });
|
||||
public virtual void OnStatusUpdate(object sender, string statusUpdate)
|
||||
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IProcessable.StatusUpdate), Status = statusUpdate });
|
||||
public virtual void OnCompleted(object sender, LibraryBook libraryBook)
|
||||
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IProcessable.Completed), Book = libraryBook.LogFriendly() });
|
||||
|
||||
#endregion
|
||||
|
||||
#region IAudioDecodable event handlers
|
||||
public virtual void OnRequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate) { }
|
||||
public virtual void OnTitleDiscovered(object sender, string title) { }
|
||||
public virtual void OnAuthorsDiscovered(object sender, string authors) { }
|
||||
public virtual void OnNarratorsDiscovered(object sender, string narrators) { }
|
||||
public virtual void OnCoverImageDiscovered(object sender, byte[] coverArt) { }
|
||||
public virtual void OnRequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
|
||||
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IAudioDecodable.RequestCoverArt) });
|
||||
public virtual void OnTitleDiscovered(object sender, string title)
|
||||
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IAudioDecodable.TitleDiscovered), Title = title });
|
||||
public virtual void OnAuthorsDiscovered(object sender, string authors)
|
||||
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IAudioDecodable.AuthorsDiscovered), Authors = authors });
|
||||
public virtual void OnNarratorsDiscovered(object sender, string narrators)
|
||||
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IAudioDecodable.NarratorsDiscovered), Narrators = narrators });
|
||||
public virtual void OnCoverImageDiscovered(object sender, byte[] coverArt)
|
||||
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IAudioDecodable.CoverImageDiscovered), CoverImageBytes = coverArt?.Length });
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,12 @@ namespace LibationWinForms.BookLiberation
|
||||
#region IStreamable event handler overrides
|
||||
public override void OnStreamingBegin(object sender, string beginString)
|
||||
{
|
||||
base.OnStreamingBegin(sender, beginString);
|
||||
filenameLbl.UIThreadAsync(() => filenameLbl.Text = beginString);
|
||||
}
|
||||
public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress)
|
||||
{
|
||||
base.OnStreamingProgressChanged(sender, downloadProgress);
|
||||
// this won't happen with download file. it will happen with download string
|
||||
if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0)
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,15 @@ namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
internal class PdfDownloadForm : DownloadForm
|
||||
{
|
||||
public override void OnBegin(object sender, LibraryBook libraryBook) => LogMe.Info($"PDF Step, Begin: {libraryBook.Book}");
|
||||
public override void OnCompleted(object sender, LibraryBook libraryBook) => LogMe.Info($"PDF Step, Completed: {libraryBook.Book}");
|
||||
public override void OnBegin(object sender, LibraryBook libraryBook)
|
||||
{
|
||||
base.OnBegin(sender, libraryBook);
|
||||
LogMe.Info($"PDF Step, Begin: {libraryBook.Book}");
|
||||
}
|
||||
public override void OnCompleted(object sender, LibraryBook libraryBook)
|
||||
{
|
||||
base.OnCompleted(sender, libraryBook);
|
||||
LogMe.Info($"PDF Step, Completed: {libraryBook.Book}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace LibationWinForms.Dialogs
|
||||
this.authorsDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.miscDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.purchaseDateGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
|
||||
this.gridEntryBindingSource = new LibationWinForms.SyncBindingSource(this.components);
|
||||
this.btnRemoveBooks = new System.Windows.Forms.Button();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).BeginInit();
|
||||
@@ -176,7 +176,7 @@ namespace LibationWinForms.Dialogs
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.DataGridView _dataGridView;
|
||||
private System.Windows.Forms.BindingSource gridEntryBindingSource;
|
||||
private LibationWinForms.SyncBindingSource gridEntryBindingSource;
|
||||
private System.Windows.Forms.Button btnRemoveBooks;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.DataGridViewCheckBoxColumn removeDataGridViewCheckBoxColumn;
|
||||
|
||||
@@ -91,16 +91,22 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
var selectedBooks = SelectedEntries.ToList();
|
||||
|
||||
if (selectedBooks.Count == 0) return;
|
||||
if (selectedBooks.Count == 0)
|
||||
return;
|
||||
|
||||
string titles = string.Join("\r\n", selectedBooks.Select(rge => "-" + rge.Title));
|
||||
var titles = selectedBooks.Select(rge => "- " + rge.Title).ToList();
|
||||
var titlesAgg = titles.Take(5).Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
if (titles.Count == 6)
|
||||
titlesAgg += $"\r\n\r\nand 1 other";
|
||||
else if (titles.Count > 6)
|
||||
titlesAgg += $"\r\n\r\nand {titles.Count - 5} others";
|
||||
|
||||
string thisThese = selectedBooks.Count > 1 ? "these" : "this";
|
||||
string bookBooks = selectedBooks.Count > 1 ? "books" : "book";
|
||||
|
||||
var result = MessageBox.Show(
|
||||
this,
|
||||
$"Are you sure you want to remove {thisThese} {selectedBooks.Count} {bookBooks} from Libation's library?\r\n\r\n{titles}",
|
||||
$"Are you sure you want to remove {thisThese} {selectedBooks.Count} {bookBooks} from Libation's library?\r\n\r\n{titlesAgg}",
|
||||
"Remove books from Libation?",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question,
|
||||
|
||||
@@ -204,9 +204,9 @@
|
||||
this.convertLossyRb.AutoSize = true;
|
||||
this.convertLossyRb.Location = new System.Drawing.Point(6, 81);
|
||||
this.convertLossyRb.Name = "convertLossyRb";
|
||||
this.convertLossyRb.Size = new System.Drawing.Size(242, 19);
|
||||
this.convertLossyRb.Size = new System.Drawing.Size(329, 19);
|
||||
this.convertLossyRb.TabIndex = 10;
|
||||
this.convertLossyRb.Text = "Download my books as .MP3 files (Lossy)";
|
||||
this.convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)";
|
||||
this.convertLossyRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// convertLosslessRb
|
||||
@@ -215,10 +215,10 @@
|
||||
this.convertLosslessRb.Checked = true;
|
||||
this.convertLosslessRb.Location = new System.Drawing.Point(6, 56);
|
||||
this.convertLosslessRb.Name = "convertLosslessRb";
|
||||
this.convertLosslessRb.Size = new System.Drawing.Size(327, 19);
|
||||
this.convertLosslessRb.Size = new System.Drawing.Size(335, 19);
|
||||
this.convertLosslessRb.TabIndex = 9;
|
||||
this.convertLosslessRb.TabStop = true;
|
||||
this.convertLosslessRb.Text = "Download my books as .M4B files (Lossless Mp4a format)";
|
||||
this.convertLosslessRb.Text = "Download my books in the original audio format (Lossless)";
|
||||
this.convertLosslessRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// inProgressSelectControl
|
||||
|
||||
@@ -34,6 +34,8 @@ 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;
|
||||
|
||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||
|
||||
@@ -27,17 +27,16 @@ namespace LibationWinForms
|
||||
|
||||
#endregion
|
||||
|
||||
public event EventHandler Committed;
|
||||
|
||||
private Book Book => LibraryBook.Book;
|
||||
private Image _cover;
|
||||
private Action Refilter { get; }
|
||||
|
||||
public GridEntry(LibraryBook libraryBook, Action refilterOnChanged = null)
|
||||
public GridEntry(LibraryBook libraryBook)
|
||||
{
|
||||
LibraryBook = libraryBook;
|
||||
Refilter = refilterOnChanged;
|
||||
_memberValues = CreateMemberValueDictionary();
|
||||
|
||||
|
||||
//Get cover art. If it's default, subscribe to PictureCached
|
||||
{
|
||||
(bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
|
||||
@@ -142,7 +141,7 @@ namespace LibationWinForms
|
||||
|
||||
Book.UserDefinedItem.BookStatus = displayStatus;
|
||||
|
||||
Refilter?.Invoke();
|
||||
Committed?.Invoke(this, null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -253,7 +252,7 @@ namespace LibationWinForms
|
||||
private static string GetDescriptionDisplay(Book book)
|
||||
{
|
||||
var doc = new HtmlAgilityPack.HtmlDocument();
|
||||
doc.LoadHtml(book.Description);
|
||||
doc.LoadHtml(book?.Description ?? "");
|
||||
var noHtml = doc.DocumentNode.InnerText;
|
||||
return
|
||||
noHtml.Length < 63 ?
|
||||
|
||||
4
LibationWinForms/ProductsGrid.Designer.cs
generated
4
LibationWinForms/ProductsGrid.Designer.cs
generated
@@ -30,7 +30,7 @@
|
||||
{
|
||||
this.components = new System.ComponentModel.Container();
|
||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
|
||||
this.gridEntryBindingSource = new LibationWinForms.SyncBindingSource(this.components);
|
||||
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
|
||||
this.dataGridViewImageButtonBoxColumn1 = new LibationWinForms.LiberateDataGridViewImageButtonColumn();
|
||||
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
|
||||
@@ -222,7 +222,7 @@
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.BindingSource gridEntryBindingSource;
|
||||
private LibationWinForms.SyncBindingSource gridEntryBindingSource;
|
||||
private System.Windows.Forms.DataGridView gridEntryDataGridView;
|
||||
private LiberateDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn1;
|
||||
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
|
||||
|
||||
@@ -3,9 +3,9 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.DataBinding;
|
||||
using Dinah.Core.Threading;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using LibationWinForms.Dialogs;
|
||||
|
||||
@@ -130,7 +130,12 @@ namespace LibationWinForms
|
||||
}
|
||||
|
||||
var orderedGridEntries = lib
|
||||
.Select(lb => new GridEntry(lb, Filter)).ToList()
|
||||
.Select(lb =>
|
||||
{
|
||||
var entry = new GridEntry(lb);
|
||||
entry.Committed += (_, __) => Filter();
|
||||
return entry;
|
||||
}).ToList()
|
||||
// default load order
|
||||
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
|
||||
//// more advanced example: sort by author, then series, then title
|
||||
@@ -166,8 +171,11 @@ namespace LibationWinForms
|
||||
var bindingContext = BindingContext[_dataGridView.DataSource];
|
||||
bindingContext.SuspendBinding();
|
||||
{
|
||||
for (var r = _dataGridView.RowCount - 1; r >= 0; r--)
|
||||
_dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId);
|
||||
this.UIThreadSync(() =>
|
||||
{
|
||||
for (var r = _dataGridView.RowCount - 1; r >= 0; r--)
|
||||
_dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId);
|
||||
});
|
||||
}
|
||||
|
||||
//Causes repainting of the DataGridView
|
||||
|
||||
@@ -259,22 +259,26 @@ namespace LibationWinForms
|
||||
|
||||
// assign these strings and enums/ints unconditionally. EFCore will only update if changed
|
||||
if (fileType == FileType.PDF)
|
||||
book.UserDefinedItem.PdfStatus = LiberatedStatus.Liberated;
|
||||
book.UserDefinedItem.BatchMode_UpdatePdfStatus(LiberatedStatus.Liberated);
|
||||
|
||||
if (fileType == FileType.Audio)
|
||||
{
|
||||
var lhack = libhackFiles.FirstOrDefault(f => f.ContainsInsensitive(asin));
|
||||
if (lhack is null)
|
||||
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
|
||||
book.UserDefinedItem.BatchMode_UpdateBookStatus(LiberatedStatus.Liberated);
|
||||
else
|
||||
{
|
||||
book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
|
||||
book.UserDefinedItem.BatchMode_UpdateBookStatus(LiberatedStatus.Error);
|
||||
libhackFilesToDelete.Add(lhack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
// 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)
|
||||
|
||||
28
LibationWinForms/SyncBindingSource.cs
Normal file
28
LibationWinForms/SyncBindingSource.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
// https://stackoverflow.com/a/32886415
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public class SyncBindingSource : BindingSource
|
||||
{
|
||||
private SynchronizationContext syncContext { get; }
|
||||
|
||||
public SyncBindingSource() : base()
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
public SyncBindingSource(IContainer container) : base(container)
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
public SyncBindingSource(object dataSource, string dataMember) : base(dataSource, dataMember)
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
|
||||
protected override void OnListChanged(ListChangedEventArgs e)
|
||||
{
|
||||
if (syncContext != null)
|
||||
syncContext.Send(_ => base.OnListChanged(e), null);
|
||||
else
|
||||
base.OnListChanged(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user