mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-08-07 04:42:17 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19369a21ef | ||
|
|
611fb4d6d8 | ||
|
|
c77ec54035 | ||
|
|
c9c28c7826 | ||
|
|
30e2caaff5 | ||
|
|
fd56017af5 | ||
|
|
d2eaf26117 | ||
|
|
7c38e18435 | ||
|
|
bfb1dbc69a | ||
|
|
d2ff19e309 | ||
|
|
aa3a7dce06 | ||
|
|
71075838eb | ||
|
|
803a0b7ccf | ||
|
|
d9f3fa825c | ||
|
|
df42ba584e | ||
|
|
9f09a62a1e | ||
|
|
e714179c30 | ||
|
|
db84c9a7d9 | ||
|
|
937bd56fcc | ||
|
|
f29968f379 | ||
|
|
14e14ba9bd | ||
|
|
613c97524a | ||
|
|
4fd16f04e0 | ||
|
|
61385f0f0b | ||
|
|
7647882344 | ||
|
|
96ffa619ec | ||
|
|
de1147ac1b | ||
|
|
926a7a1148 | ||
|
|
51020ef99e | ||
|
|
5a1303c33a | ||
|
|
a0e2d78b9b | ||
|
|
6b711190c3 | ||
|
|
b4a6342513 | ||
|
|
988b137d67 | ||
|
|
dae9c9c9b6 | ||
|
|
420b7529c6 | ||
|
|
4cf999c84d | ||
|
|
8fe3896d76 | ||
|
|
adcba34560 | ||
|
|
8e09d7e617 | ||
|
|
197b50e3ac | ||
|
|
ac2114e270 |
No files matched your search
@@ -5,8 +5,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AAXClean" Version="0.1.8" />
|
<PackageReference Include="AAXClean" Version="0.1.9" />
|
||||||
<PackageReference Include="Dinah.Core" Version="1.1.0.1" />
|
<PackageReference Include="Dinah.Core" Version="1.1.1.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using AAXClean;
|
using AAXClean;
|
||||||
using Dinah.Core;
|
|
||||||
using Dinah.Core.IO;
|
using Dinah.Core.IO;
|
||||||
using Dinah.Core.Net.Http;
|
using Dinah.Core.Net.Http;
|
||||||
using Dinah.Core.StepRunner;
|
using Dinah.Core.StepRunner;
|
||||||
@@ -8,157 +7,83 @@ using System.IO;
|
|||||||
|
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
{
|
{
|
||||||
public enum OutputFormat { Mp4a, Mp3 }
|
public class AaxcDownloadConverter : AudiobookDownloadBase
|
||||||
public class AaxcDownloadConverter
|
|
||||||
{
|
{
|
||||||
public event EventHandler<AppleTags> RetrievedTags;
|
protected override StepSequence steps { get; }
|
||||||
public event EventHandler<byte[]> RetrievedCoverArt;
|
|
||||||
public event EventHandler<DownloadProgress> DecryptProgressUpdate;
|
|
||||||
public event EventHandler<TimeSpan> DecryptTimeRemaining;
|
|
||||||
|
|
||||||
public string AppName { get; set; } = nameof(AaxcDownloadConverter);
|
|
||||||
|
|
||||||
private string outputFileName { get; }
|
|
||||||
private string cacheDir { get; }
|
|
||||||
private DownloadLicense downloadLicense { get; }
|
|
||||||
private AaxFile aaxFile;
|
private AaxFile aaxFile;
|
||||||
private OutputFormat OutputFormat;
|
|
||||||
|
|
||||||
private StepSequence steps { get; }
|
private OutputFormat OutputFormat { 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");
|
|
||||||
|
|
||||||
public AaxcDownloadConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat)
|
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;
|
OutputFormat = outputFormat;
|
||||||
|
|
||||||
steps = new StepSequence
|
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 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 3: Create Cue"] = Step3_CreateCue,
|
||||||
["Step 4: Create Nfo"] = Step4_CreateNfo,
|
["Step 4: Cleanup"] = Step4_Cleanup,
|
||||||
["Step 5: Cleanup"] = Step5_Cleanup,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Setting cover art by this method will insert the art into the audiobook metadata
|
/// Setting cover art by this method will insert the art into the audiobook metadata
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetCoverArt(byte[] coverArt)
|
public override void SetCoverArt(byte[] coverArt)
|
||||||
{
|
{
|
||||||
if (coverArt is null) return;
|
base.SetCoverArt(coverArt);
|
||||||
|
|
||||||
aaxFile?.AppleTags.SetCoverArt(coverArt);
|
aaxFile?.AppleTags.SetCoverArt(coverArt);
|
||||||
|
|
||||||
RetrievedCoverArt?.Invoke(this, coverArt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Run()
|
protected override bool Step1_GetMetadata()
|
||||||
{
|
{
|
||||||
var (IsSuccess, Elapsed) = steps.Run();
|
aaxFile = new AaxFile(InputFileStream);
|
||||||
|
|
||||||
if (!IsSuccess)
|
OnRetrievedTitle(aaxFile.AppleTags.TitleSansUnabridged);
|
||||||
{
|
OnRetrievedAuthors(aaxFile.AppleTags.FirstAuthor ?? "[unknown]");
|
||||||
Console.WriteLine("WARNING-Conversion failed");
|
OnRetrievedNarrators(aaxFile.AppleTags.Narrator ?? "[unknown]");
|
||||||
return false;
|
OnRetrievedCoverArt(aaxFile.AppleTags.Cover);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
return !isCanceled;
|
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);
|
protected override bool Step2_DownloadAudiobook()
|
||||||
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Step2_DownloadAndCombine()
|
|
||||||
{
|
{
|
||||||
var zeroProgress = new DownloadProgress
|
var zeroProgress = new DownloadProgress
|
||||||
{
|
{
|
||||||
BytesReceived = 0,
|
BytesReceived = 0,
|
||||||
ProgressPercentage = 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))
|
if (File.Exists(outputFileName))
|
||||||
FileExt.SafeDelete(outputFileName);
|
FileExt.SafeDelete(outputFileName);
|
||||||
|
|
||||||
FileStream outFile = File.OpenWrite(outputFileName);
|
var outputFile = File.Open(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||||
|
|
||||||
aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV);
|
|
||||||
|
|
||||||
aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
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.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||||
|
|
||||||
aaxFile.Close();
|
aaxFile.Close();
|
||||||
|
|
||||||
downloadLicense.ChapterInfo = aaxFile.Chapters;
|
downloadLicense.ChapterInfo = aaxFile.Chapters;
|
||||||
|
|
||||||
nfsPersister.Dispose();
|
CloseInputFileStream();
|
||||||
|
|
||||||
DecryptProgressUpdate?.Invoke(this, zeroProgress);
|
OnDecryptProgressUpdate(zeroProgress);
|
||||||
|
|
||||||
return decryptionResult == ConversionResult.NoErrorsDetected && !isCanceled;
|
return decryptionResult == ConversionResult.NoErrorsDetected && !isCanceled;
|
||||||
}
|
}
|
||||||
@@ -170,61 +95,28 @@ namespace AaxDecrypter
|
|||||||
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||||
|
|
||||||
if (double.IsNormal(estTimeRemaining))
|
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
|
new DownloadProgress
|
||||||
{
|
{
|
||||||
ProgressPercentage = progressPercent,
|
ProgressPercentage = 100 * progressPercent,
|
||||||
BytesReceived = (long)(nfsPersister.NetworkFileStream.Length * progressPercent),
|
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
||||||
TotalBytesToReceive = nfsPersister.NetworkFileStream.Length
|
TotalBytesToReceive = InputFileStream.Length
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Step3_CreateCue()
|
public override void Cancel()
|
||||||
{
|
|
||||||
// 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()
|
|
||||||
{
|
{
|
||||||
isCanceled = true;
|
isCanceled = true;
|
||||||
aaxFile?.Cancel();
|
aaxFile?.Cancel();
|
||||||
aaxFile?.Dispose();
|
aaxFile?.Dispose();
|
||||||
nfsPersister?.NetworkFileStream?.Close();
|
CloseInputFileStream();
|
||||||
nfsPersister?.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
protected override int GetSpeedup(TimeSpan elapsed)
|
||||||
|
=> (int)(aaxFile.Duration.TotalSeconds / (long)elapsed.TotalSeconds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
public DownloadLicense(string downloadUrl, string audibleKey, string audibleIV, string userAgent)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
||||||
ArgumentValidator.EnsureNotNullOrEmpty(audibleKey, nameof(audibleKey));
|
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
|
||||||
ArgumentValidator.EnsureNotNullOrEmpty(audibleIV, nameof(audibleIV));
|
|
||||||
ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
|
|
||||||
|
|
||||||
DownloadUrl = downloadUrl;
|
// no null/empty check. unencrypted files do not have these
|
||||||
AudibleKey = audibleKey;
|
AudibleKey = audibleKey;
|
||||||
AudibleIV = audibleIV;
|
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 FileStream _readFile { get; }
|
||||||
private Stream _networkStream { get; set; }
|
private Stream _networkStream { get; set; }
|
||||||
private bool hasBegunDownloading { 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 downloadEnded { get; set; }
|
||||||
private EventWaitHandle downloadedPiece { get; set; }
|
private EventWaitHandle downloadedPiece { get; set; }
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ namespace AaxDecrypter
|
|||||||
downloadedPiece.Set();
|
downloadedPiece.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
} while (downloadPosition < ContentLength && !isCancelled);
|
} while (downloadPosition < ContentLength && !IsCancelled);
|
||||||
|
|
||||||
_writeFile.Close();
|
_writeFile.Close();
|
||||||
_networkStream.Close();
|
_networkStream.Close();
|
||||||
@@ -248,7 +248,7 @@ namespace AaxDecrypter
|
|||||||
downloadedPiece.Set();
|
downloadedPiece.Set();
|
||||||
downloadEnded.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}).");
|
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||||
|
|
||||||
if (WritePosition > ContentLength)
|
if (WritePosition > ContentLength)
|
||||||
@@ -421,12 +421,12 @@ namespace AaxDecrypter
|
|||||||
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
|
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
|
||||||
private void WaitToPosition(long requiredPosition)
|
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()
|
public override void Close()
|
||||||
{
|
{
|
||||||
isCancelled = true;
|
IsCancelled = true;
|
||||||
|
|
||||||
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
|
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<Version>5.7.0.1</Version>
|
<Version>6.1.4.1</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -46,24 +46,23 @@ namespace AppScaffolding
|
|||||||
return Configuration.Instance;
|
return Configuration.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RunPostConfigMigrations()
|
/// <summary>most migrations go in here</summary>
|
||||||
|
public static void RunPostConfigMigrations(Configuration config)
|
||||||
{
|
{
|
||||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||||
|
|
||||||
var config = Configuration.Instance;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// migrations go below here
|
// migrations go below here
|
||||||
//
|
//
|
||||||
|
|
||||||
Migrations.migrate_to_v5_2_0__post_config(config);
|
Migrations.migrate_to_v5_2_0__post_config(config);
|
||||||
|
Migrations.migrate_to_v5_7_1(config);
|
||||||
|
Migrations.migrate_to_v6_1_2(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Initialize logging. Run after migration</summary>
|
/// <summary>Initialize logging. Run after migration</summary>
|
||||||
public static void RunPostMigrationScaffolding()
|
public static void RunPostMigrationScaffolding(Configuration config)
|
||||||
{
|
{
|
||||||
var config = Configuration.Instance;
|
|
||||||
|
|
||||||
ensureSerilogConfig(config);
|
ensureSerilogConfig(config);
|
||||||
configureLogging(config);
|
configureLogging(config);
|
||||||
logStartupState(config);
|
logStartupState(config);
|
||||||
@@ -321,5 +320,22 @@ namespace AppScaffolding
|
|||||||
config.DecryptToLossy = false;
|
config.DecryptToLossy = false;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
// add config.BadBook
|
||||||
|
public static void migrate_to_v5_7_1(Configuration config)
|
||||||
|
{
|
||||||
|
if (!config.Exists(nameof(config.BadBook)))
|
||||||
|
config.BadBook = Configuration.BadBookAction.Ask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add config.DownloadEpisodes , config.ImportEpisodes
|
||||||
|
public static void migrate_to_v6_1_2(Configuration config)
|
||||||
|
{
|
||||||
|
if (!config.Exists(nameof(config.DownloadEpisodes)))
|
||||||
|
config.DownloadEpisodes = true;
|
||||||
|
|
||||||
|
if (!config.Exists(nameof(config.ImportEpisodes)))
|
||||||
|
config.ImportEpisodes = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,16 @@ namespace AppScaffolding
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
||||||
var jObj = JObject.Parse(startingContents);
|
|
||||||
|
JObject jObj;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jObj = JObject.Parse(startingContents);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
action(jObj);
|
action(jObj);
|
||||||
|
|
||||||
@@ -130,7 +139,16 @@ namespace AppScaffolding
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var startingContents = File.ReadAllText(SettingsJsonPath);
|
var startingContents = File.ReadAllText(SettingsJsonPath);
|
||||||
var jObj = JObject.Parse(startingContents);
|
|
||||||
|
JObject jObj;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jObj = JObject.Parse(startingContents);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
action(jObj);
|
action(jObj);
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,13 @@ namespace ApplicationServices
|
|||||||
{
|
{
|
||||||
public static class LibraryCommands
|
public static class LibraryCommands
|
||||||
{
|
{
|
||||||
private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
|
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, List<LibraryBook> existingLibrary, params Account[] accounts)
|
||||||
|
|
||||||
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, ILoginCallback> loginCallbackFactoryFunc, List<LibraryBook> existingLibrary, params Account[] accounts)
|
|
||||||
{
|
{
|
||||||
logRestart();
|
logRestart();
|
||||||
|
|
||||||
//These are the minimum response groups required for the
|
//These are the minimum response groups required for the
|
||||||
//library scanner to pass all validation and filtering.
|
//library scanner to pass all validation and filtering.
|
||||||
LibraryResponseGroups =
|
var libraryResponseGroups =
|
||||||
LibraryOptions.ResponseGroupOptions.ProductAttrs |
|
LibraryOptions.ResponseGroupOptions.ProductAttrs |
|
||||||
LibraryOptions.ResponseGroupOptions.ProductDesc |
|
LibraryOptions.ResponseGroupOptions.ProductDesc |
|
||||||
LibraryOptions.ResponseGroupOptions.Relationships;
|
LibraryOptions.ResponseGroupOptions.Relationships;
|
||||||
@@ -33,7 +31,7 @@ namespace ApplicationServices
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||||
var libraryItems = await scanAccountsAsync(loginCallbackFactoryFunc, accounts);
|
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryResponseGroups);
|
||||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||||
|
|
||||||
var totalCount = libraryItems.Count;
|
var totalCount = libraryItems.Count;
|
||||||
@@ -68,14 +66,13 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
|
|
||||||
stop();
|
stop();
|
||||||
var putBreakPointHere = logOutput;
|
var putBreakPointHere = logOutput;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region FULL LIBRARY scan and import
|
#region FULL LIBRARY scan and import
|
||||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, params Account[] accounts)
|
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[] accounts)
|
||||||
{
|
{
|
||||||
logRestart();
|
logRestart();
|
||||||
|
|
||||||
@@ -85,7 +82,7 @@ namespace ApplicationServices
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||||
var importItems = await scanAccountsAsync(loginCallbackFactoryFunc, accounts);
|
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||||
|
|
||||||
var totalCount = importItems.Count;
|
var totalCount = importItems.Count;
|
||||||
@@ -129,18 +126,16 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, Account[] accounts)
|
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions.ResponseGroupOptions libraryResponseGroups)
|
||||||
{
|
{
|
||||||
var tasks = new List<Task<List<ImportItem>>>();
|
var tasks = new List<Task<List<ImportItem>>>();
|
||||||
foreach (var account in accounts)
|
foreach (var account in accounts)
|
||||||
{
|
{
|
||||||
var callback = loginCallbackFactoryFunc(account);
|
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
|
||||||
|
var apiExtended = await apiExtendedfunc(account);
|
||||||
// get APIs in serial, esp b/c of logins
|
|
||||||
var api = await AudibleApiActions.GetApiAsync(callback, account);
|
|
||||||
|
|
||||||
// add scanAccountAsync as a TASK: do not await
|
// add scanAccountAsync as a TASK: do not await
|
||||||
tasks.Add(scanAccountAsync(api, account));
|
tasks.Add(scanAccountAsync(apiExtended, account, libraryResponseGroups));
|
||||||
}
|
}
|
||||||
|
|
||||||
// import library in parallel
|
// import library in parallel
|
||||||
@@ -149,7 +144,7 @@ namespace ApplicationServices
|
|||||||
return importItems;
|
return importItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<List<ImportItem>> scanAccountAsync(Api api, Account account)
|
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions.ResponseGroupOptions libraryResponseGroups)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||||
|
|
||||||
@@ -160,7 +155,7 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||||
|
|
||||||
var dtoItems = await AudibleApiActions.GetLibraryValidatedAsync(api, LibraryResponseGroups);
|
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryResponseGroups, FileManager.Configuration.Instance.ImportEpisodes);
|
||||||
|
|
||||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||||
|
|
||||||
@@ -194,6 +189,7 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
var removeLibraryBooks = libBooks.Where(lb => idsToRemove.Contains(lb.Book.AudibleProductId)).ToList();
|
var removeLibraryBooks = libBooks.Where(lb => idsToRemove.Contains(lb.Book.AudibleProductId)).ToList();
|
||||||
context.LibraryBooks.RemoveRange(removeLibraryBooks);
|
context.LibraryBooks.RemoveRange(removeLibraryBooks);
|
||||||
|
context.Books.RemoveRange(removeLibraryBooks.Select(lb => lb.Book));
|
||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ namespace ApplicationServices
|
|||||||
Publisher = a.Book.Publisher,
|
Publisher = a.Book.Publisher,
|
||||||
HasPdf = a.Book.HasPdf,
|
HasPdf = a.Book.HasPdf,
|
||||||
SeriesNames = a.Book.SeriesNames,
|
SeriesNames = a.Book.SeriesNames,
|
||||||
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Index} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
|
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Order} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
|
||||||
CommunityRatingOverall = a.Book.Rating?.OverallRating,
|
CommunityRatingOverall = a.Book.Rating?.OverallRating,
|
||||||
CommunityRatingPerformance = a.Book.Rating?.PerformanceRating,
|
CommunityRatingPerformance = a.Book.Rating?.PerformanceRating,
|
||||||
CommunityRatingStory = a.Book.Rating?.StoryRating,
|
CommunityRatingStory = a.Book.Rating?.StoryRating,
|
||||||
|
|||||||
@@ -12,13 +12,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="1.0.5.1" />
|
<PackageReference Include="Dinah.EntityFrameworkCore" Version="1.0.5.2" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.9">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.9" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.9">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.10">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ namespace DataLayer
|
|||||||
Category = category;
|
Category = category;
|
||||||
|
|
||||||
// simple assigns
|
// simple assigns
|
||||||
Title = title.Trim();
|
Title = title.Trim() ?? "";
|
||||||
Description = description.Trim();
|
Description = description?.Trim() ?? "";
|
||||||
LengthInMinutes = lengthInMinutes;
|
LengthInMinutes = lengthInMinutes;
|
||||||
ContentType = contentType;
|
ContentType = contentType;
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ namespace DataLayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpsertSeries(Series series, float? index = null, DbContext context = null)
|
public void UpsertSeries(Series series, string order, DbContext context = null)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
||||||
|
|
||||||
@@ -214,9 +214,9 @@ namespace DataLayer
|
|||||||
|
|
||||||
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
|
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
|
||||||
if (singleSeriesBook == null)
|
if (singleSeriesBook == null)
|
||||||
_seriesLink.Add(new SeriesBook(series, this, index));
|
_seriesLink.Add(new SeriesBook(series, this, order));
|
||||||
else
|
else
|
||||||
singleSeriesBook.UpdateIndex(index);
|
singleSeriesBook.UpdateOrder(order);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|||||||
@@ -48,25 +48,6 @@ namespace DataLayer
|
|||||||
Name = name;
|
Name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddBook(Book book, float? index = null, DbContext context = null)
|
|
||||||
{
|
|
||||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
|
||||||
|
|
||||||
// our add() is conditional upon what's already included in the collection.
|
|
||||||
// therefore if not loaded, a trip is required. might as well just load it
|
|
||||||
if (_booksLink == null)
|
|
||||||
{
|
|
||||||
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
|
||||||
if (!context.Entry(this).IsKeySet)
|
|
||||||
throw new InvalidOperationException("Could not add series");
|
|
||||||
|
|
||||||
context.Entry(this).Collection(s => s.BooksLink).Load();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_booksLink.SingleOrDefault(sb => sb.Book == book) == null)
|
|
||||||
_booksLink.Add(new SeriesBook(this, book, index));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,32 +7,27 @@ namespace DataLayer
|
|||||||
internal int SeriesId { get; private set; }
|
internal int SeriesId { get; private set; }
|
||||||
internal int BookId { get; private set; }
|
internal int BookId { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
public string Order { get; private set; }
|
||||||
/// <para>"index" not "order". This is both for sequence and display</para>
|
public float Index => StringLib.ExtractFirstNumber(Order);
|
||||||
/// <para>Float allows for in-between books. eg: 2.5</para>
|
|
||||||
/// <para>To show 2 editions as the same book in a series, give them the same index</para>
|
|
||||||
/// <para>null IS NOT the same as 0. Some series call a book "book 0"</para>
|
|
||||||
/// </summary>
|
|
||||||
public float? Index { get; private set; }
|
|
||||||
|
|
||||||
public Series Series { get; private set; }
|
public Series Series { get; private set; }
|
||||||
public Book Book { get; private set; }
|
public Book Book { get; private set; }
|
||||||
|
|
||||||
private SeriesBook() { }
|
private SeriesBook() { }
|
||||||
internal SeriesBook(Series series, Book book, float? index = null)
|
internal SeriesBook(Series series, Book book, string order)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
||||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||||
|
|
||||||
Series = series;
|
Series = series;
|
||||||
Book = book;
|
Book = book;
|
||||||
Index = index;
|
Order = order;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateIndex(float? index)
|
public void UpdateOrder(string order)
|
||||||
{
|
{
|
||||||
if (index.HasValue)
|
if (!string.IsNullOrWhiteSpace(order))
|
||||||
Index = index.Value;
|
Order = order;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => $"Series={Series} Book={Book}";
|
public override string ToString() => $"Series={Series} Book={Book}";
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ namespace DataLayer
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region LiberatedStatuses
|
#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 _bookStatus;
|
||||||
private LiberatedStatus? _pdfStatus;
|
private LiberatedStatus? _pdfStatus;
|
||||||
@@ -132,13 +137,41 @@ namespace DataLayer
|
|||||||
ItemChanged?.Invoke(this, nameof(PdfStatus));
|
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
|
#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}";
|
public override string ToString() => $"{Book} {Rating} {Tags}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using DataLayer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace DataLayer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(LibationContext))]
|
||||||
|
[Migration("20210922154900_AddSeriesOrderString")]
|
||||||
|
partial class AddSeriesOrderString
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.10");
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleProductId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("CategoryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ContentType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DatePublished")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAbridged")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("LengthInMinutes")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Locale")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PictureId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("AudibleProductId");
|
||||||
|
|
||||||
|
b.HasIndex("CategoryId");
|
||||||
|
|
||||||
|
b.ToTable("Books");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ContributorId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte>("Order")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("BookId", "ContributorId", "Role");
|
||||||
|
|
||||||
|
b.HasIndex("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("ContributorId");
|
||||||
|
|
||||||
|
b.ToTable("BookContributor");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Category", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CategoryId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleCategoryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("ParentCategoryCategoryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("CategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("AudibleCategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("ParentCategoryCategoryId");
|
||||||
|
|
||||||
|
b.ToTable("Categories");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
CategoryId = -1,
|
||||||
|
AudibleCategoryId = "",
|
||||||
|
Name = ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ContributorId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleContributorId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ContributorId");
|
||||||
|
|
||||||
|
b.HasIndex("Name");
|
||||||
|
|
||||||
|
b.ToTable("Contributors");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
ContributorId = -1,
|
||||||
|
Name = ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Account")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateAdded")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("BookId");
|
||||||
|
|
||||||
|
b.ToTable("LibraryBooks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Series", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleSeriesId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("SeriesId");
|
||||||
|
|
||||||
|
b.HasIndex("AudibleSeriesId");
|
||||||
|
|
||||||
|
b.ToTable("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Order")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("SeriesId", "BookId");
|
||||||
|
|
||||||
|
b.HasIndex("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("SeriesBook");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Category", "Category")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CategoryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<float>("OverallRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b1.Property<float>("PerformanceRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b1.Property<float>("StoryRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b1.HasKey("BookId");
|
||||||
|
|
||||||
|
b1.ToTable("Books");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("BookId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("SupplementId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<string>("Url")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.HasKey("SupplementId");
|
||||||
|
|
||||||
|
b1.HasIndex("BookId");
|
||||||
|
|
||||||
|
b1.ToTable("Supplement");
|
||||||
|
|
||||||
|
b1.WithOwner("Book")
|
||||||
|
.HasForeignKey("BookId");
|
||||||
|
|
||||||
|
b1.Navigation("Book");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<int>("BookStatus")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<int?>("PdfStatus")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<string>("Tags")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.HasKey("BookId");
|
||||||
|
|
||||||
|
b1.ToTable("UserDefinedItem");
|
||||||
|
|
||||||
|
b1.WithOwner("Book")
|
||||||
|
.HasForeignKey("BookId");
|
||||||
|
|
||||||
|
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||||
|
{
|
||||||
|
b2.Property<int>("UserDefinedItemBookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b2.Property<float>("OverallRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b2.Property<float>("PerformanceRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b2.Property<float>("StoryRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b2.HasKey("UserDefinedItemBookId");
|
||||||
|
|
||||||
|
b2.ToTable("UserDefinedItem");
|
||||||
|
|
||||||
|
b2.WithOwner()
|
||||||
|
.HasForeignKey("UserDefinedItemBookId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b1.Navigation("Book");
|
||||||
|
|
||||||
|
b1.Navigation("Rating");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Category");
|
||||||
|
|
||||||
|
b.Navigation("Rating");
|
||||||
|
|
||||||
|
b.Navigation("Supplements");
|
||||||
|
|
||||||
|
b.Navigation("UserDefinedItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithMany("ContributorsLink")
|
||||||
|
.HasForeignKey("BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||||
|
.WithMany("BooksLink")
|
||||||
|
.HasForeignKey("ContributorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
|
||||||
|
b.Navigation("Contributor");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Category", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ParentCategoryCategoryId");
|
||||||
|
|
||||||
|
b.Navigation("ParentCategory");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithMany("SeriesLink")
|
||||||
|
.HasForeignKey("BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("DataLayer.Series", "Series")
|
||||||
|
.WithMany("BooksLink")
|
||||||
|
.HasForeignKey("SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ContributorsLink");
|
||||||
|
|
||||||
|
b.Navigation("SeriesLink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("BooksLink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("BooksLink");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace DataLayer.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddSeriesOrderString : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Index",
|
||||||
|
table: "SeriesBook");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Order",
|
||||||
|
table: "SeriesBook",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Order",
|
||||||
|
table: "SeriesBook");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<float>(
|
||||||
|
name: "Index",
|
||||||
|
table: "SeriesBook",
|
||||||
|
type: "REAL",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ namespace DataLayer.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "5.0.9");
|
.HasAnnotation("ProductVersion", "5.0.10");
|
||||||
|
|
||||||
modelBuilder.Entity("DataLayer.Book", b =>
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
{
|
{
|
||||||
@@ -185,8 +185,8 @@ namespace DataLayer.Migrations
|
|||||||
b.Property<int>("BookId")
|
b.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<float?>("Index")
|
b.Property<string>("Order")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("SeriesId", "BookId");
|
b.HasKey("SeriesId", "BookId");
|
||||||
|
|
||||||
|
|||||||
@@ -165,18 +165,7 @@ namespace DtoImporterService
|
|||||||
foreach (var seriesEntry in item.Series)
|
foreach (var seriesEntry in item.Series)
|
||||||
{
|
{
|
||||||
var series = DbContext.Series.Local.Single(s => seriesEntry.SeriesId == s.AudibleSeriesId);
|
var series = DbContext.Series.Local.Single(s => seriesEntry.SeriesId == s.AudibleSeriesId);
|
||||||
|
book.UpsertSeries(series, seriesEntry.Sequence);
|
||||||
var index = 0f;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
index = seriesEntry.Index;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, $"Error parsing series index. Title: {item.Title}. ASIN: {item.Asin}. Series index: {seriesEntry.Sequence}");
|
|
||||||
}
|
|
||||||
|
|
||||||
book.UpsertSeries(series, index);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using AudibleApi.Common;
|
using AudibleApi.Common;
|
||||||
|
|
||||||
namespace InternalUtilities
|
namespace DtoImporterService
|
||||||
{
|
{
|
||||||
public class ImportItem
|
public class ImportItem
|
||||||
{
|
{
|
||||||
@@ -29,21 +29,6 @@ namespace FileLiberator
|
|||||||
public event EventHandler<string> StatusUpdate;
|
public event EventHandler<string> StatusUpdate;
|
||||||
public event EventHandler<LibraryBook> Completed;
|
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 long fileSize;
|
||||||
private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3");
|
private string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3");
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace FileLiberator
|
|||||||
{
|
{
|
||||||
public class DownloadDecryptBook : IAudioDecodable
|
public class DownloadDecryptBook : IAudioDecodable
|
||||||
{
|
{
|
||||||
private AaxcDownloadConverter aaxcDownloader;
|
private AudiobookDownloadBase aaxcDownloader;
|
||||||
|
|
||||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||||
public event EventHandler<Action<byte[]>> RequestCoverArt;
|
public event EventHandler<Action<byte[]>> RequestCoverArt;
|
||||||
@@ -30,21 +30,6 @@ namespace FileLiberator
|
|||||||
public event EventHandler<string> StatusUpdate;
|
public event EventHandler<string> StatusUpdate;
|
||||||
public event EventHandler<LibraryBook> Completed;
|
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)
|
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
Begin?.Invoke(this, libraryBook);
|
Begin?.Invoke(this, libraryBook);
|
||||||
@@ -54,7 +39,7 @@ namespace FileLiberator
|
|||||||
if (libraryBook.Book.Audio_Exists)
|
if (libraryBook.Book.Audio_Exists)
|
||||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||||
|
|
||||||
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
|
var outputAudioFilename = await downloadAudiobookAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
|
||||||
|
|
||||||
// decrypt failed
|
// decrypt failed
|
||||||
if (outputAudioFilename is null)
|
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}");
|
StreamingBegin?.Invoke(this, $"Begin decrypting {libraryBook}");
|
||||||
|
|
||||||
@@ -84,44 +69,42 @@ namespace FileLiberator
|
|||||||
{
|
{
|
||||||
validate(libraryBook);
|
validate(libraryBook);
|
||||||
|
|
||||||
var api = await InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
|
var api = await libraryBook.GetApiAsync();
|
||||||
|
|
||||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
||||||
|
|
||||||
var aaxcDecryptDlLic = new DownloadLicense
|
var audiobookDlLic = new DownloadLicense
|
||||||
(
|
(
|
||||||
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
|
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
|
||||||
contentLic?.Voucher?.Key,
|
contentLic?.Voucher?.Key,
|
||||||
contentLic?.Voucher?.Iv,
|
contentLic?.Voucher?.Iv,
|
||||||
Resources.UserAgent
|
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)
|
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;
|
aaxcDownloader = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm ? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat) { AppName = "Libation" } : new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic);
|
||||||
|
|
||||||
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.DecryptProgressUpdate += (s, progress) => StreamingProgressChanged?.Invoke(this, progress);
|
aaxcDownloader.DecryptProgressUpdate += (s, progress) => StreamingProgressChanged?.Invoke(this, progress);
|
||||||
aaxcDownloader.DecryptTimeRemaining += (s, remaining) => StreamingTimeRemaining?.Invoke(this, remaining);
|
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.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||||
aaxcDownloader.RetrievedTags += aaxcDownloader_RetrievedTags;
|
|
||||||
|
|
||||||
// REAL WORK DONE HERE
|
// REAL WORK DONE HERE
|
||||||
var success = await Task.Run(() => aaxcDownloader.Run());
|
var success = await Task.Run(() => aaxcDownloader.Run());
|
||||||
@@ -138,7 +121,6 @@ namespace FileLiberator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
|
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
|
||||||
{
|
{
|
||||||
if (e is null && Configuration.Instance.AllowLibationFixup)
|
if (e is null && Configuration.Instance.AllowLibationFixup)
|
||||||
@@ -152,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)
|
private static (string destinationDir, bool movedAudioFile) MoveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||||
{
|
{
|
||||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||||
@@ -180,7 +155,7 @@ namespace FileLiberator
|
|||||||
var dest
|
var dest
|
||||||
= AudibleFileStorage.Audio.IsFileTypeMatch(f)
|
= AudibleFileStorage.Audio.IsFileTypeMatch(f)
|
||||||
? audioFileName
|
? audioFileName
|
||||||
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
|
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext
|
||||||
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
|
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
|
||||||
|
|
||||||
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
|
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
|
||||||
|
|||||||
@@ -13,12 +13,6 @@ namespace FileLiberator
|
|||||||
public event EventHandler<string> StreamingCompleted;
|
public event EventHandler<string> StreamingCompleted;
|
||||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
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)
|
public async Task<string> PerformDownloadFileAsync(string downloadUrl, string proposedDownloadFilePath)
|
||||||
{
|
{
|
||||||
var client = new HttpClient();
|
var client = new HttpClient();
|
||||||
|
|||||||
@@ -11,30 +11,40 @@ using FileManager;
|
|||||||
|
|
||||||
namespace FileLiberator
|
namespace FileLiberator
|
||||||
{
|
{
|
||||||
public class DownloadPdf : DownloadableBase
|
public class DownloadPdf : IProcessable
|
||||||
{
|
{
|
||||||
public override bool Validate(LibraryBook libraryBook)
|
public event EventHandler<LibraryBook> Begin;
|
||||||
|
public event EventHandler<LibraryBook> Completed;
|
||||||
|
|
||||||
|
public event EventHandler<string> StreamingBegin;
|
||||||
|
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
||||||
|
public event EventHandler<string> StreamingCompleted;
|
||||||
|
|
||||||
|
public event EventHandler<string> StatusUpdate;
|
||||||
|
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||||
|
|
||||||
|
public bool Validate(LibraryBook libraryBook)
|
||||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||||
&& !libraryBook.Book.PDF_Exists;
|
&& !libraryBook.Book.PDF_Exists;
|
||||||
|
|
||||||
public DownloadPdf()
|
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
StreamingBegin += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(StreamingBegin), Message = e });
|
Begin?.Invoke(this, libraryBook);
|
||||||
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() });
|
try
|
||||||
Completed += (o, e) => Serilog.Log.Logger.Information("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = e.LogFriendly() });
|
{
|
||||||
}
|
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||||
|
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||||
|
var result = verifyDownload(actualDownloadedFilePath);
|
||||||
|
|
||||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
|
||||||
{
|
|
||||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
|
||||||
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
|
||||||
var result = verifyDownload(actualDownloadedFilePath);
|
|
||||||
|
|
||||||
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
|
return result;
|
||||||
|
}
|
||||||
return result;
|
finally
|
||||||
|
{
|
||||||
|
Completed?.Invoke(this, libraryBook);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||||
@@ -59,15 +69,26 @@ namespace FileLiberator
|
|||||||
|
|
||||||
private async Task<string> downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
private async Task<string> downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||||
{
|
{
|
||||||
var api = await GetApiAsync(libraryBook);
|
StreamingBegin?.Invoke(this, proposedDownloadFilePath);
|
||||||
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
|
||||||
|
|
||||||
var client = new HttpClient();
|
try
|
||||||
var actualDownloadedFilePath = await PerformDownloadAsync(
|
{
|
||||||
proposedDownloadFilePath,
|
var api = await libraryBook.GetApiAsync();
|
||||||
(p) => client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, p));
|
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
||||||
|
|
||||||
return actualDownloadedFilePath;
|
var progress = new Progress<DownloadProgress>();
|
||||||
|
progress.ProgressChanged += (_, e) => StreamingProgressChanged?.Invoke(this, e);
|
||||||
|
|
||||||
|
var client = new HttpClient();
|
||||||
|
|
||||||
|
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
||||||
|
StatusUpdate?.Invoke(this, actualDownloadedFilePath);
|
||||||
|
return actualDownloadedFilePath;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
StreamingCompleted?.Invoke(this, proposedDownloadFilePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StatusHandler verifyDownload(string actualDownloadedFilePath)
|
private static StatusHandler verifyDownload(string actualDownloadedFilePath)
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using DataLayer;
|
|
||||||
using Dinah.Core.ErrorHandling;
|
|
||||||
using Dinah.Core.Net.Http;
|
|
||||||
|
|
||||||
namespace FileLiberator
|
|
||||||
{
|
|
||||||
public abstract class DownloadableBase : IProcessable
|
|
||||||
{
|
|
||||||
public event EventHandler<LibraryBook> Begin;
|
|
||||||
public event EventHandler<LibraryBook> Completed;
|
|
||||||
|
|
||||||
public event EventHandler<string> StreamingBegin;
|
|
||||||
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
|
||||||
public event EventHandler<string> StreamingCompleted;
|
|
||||||
|
|
||||||
public event EventHandler<string> StatusUpdate;
|
|
||||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
|
||||||
|
|
||||||
protected void Invoke_StatusUpdate(string message) => StatusUpdate?.Invoke(this, message);
|
|
||||||
|
|
||||||
public abstract bool Validate(LibraryBook libraryBook);
|
|
||||||
|
|
||||||
public abstract Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook);
|
|
||||||
|
|
||||||
// do NOT use ConfigureAwait(false) on ProcessAsync()
|
|
||||||
// often calls events which prints to forms in the UI context
|
|
||||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
|
||||||
{
|
|
||||||
Begin?.Invoke(this, libraryBook);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await ProcessItemAsync(libraryBook);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Completed?.Invoke(this, libraryBook);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static Task<AudibleApi.Api> GetApiAsync(LibraryBook libraryBook)
|
|
||||||
=> InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
|
|
||||||
|
|
||||||
protected async Task<string> PerformDownloadAsync(string proposedDownloadFilePath, Func<Progress<DownloadProgress>, Task<string>> func)
|
|
||||||
{
|
|
||||||
var progress = new Progress<DownloadProgress>();
|
|
||||||
progress.ProgressChanged += (_, e) => StreamingProgressChanged?.Invoke(this, e);
|
|
||||||
|
|
||||||
StreamingBegin?.Invoke(this, proposedDownloadFilePath);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await func(progress);
|
|
||||||
StatusUpdate?.Invoke(this, result);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
StreamingCompleted?.Invoke(this, proposedDownloadFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,15 +10,12 @@ namespace FileLiberator
|
|||||||
{
|
{
|
||||||
public static class IProcessableExt
|
public static class IProcessableExt
|
||||||
{
|
{
|
||||||
//
|
|
||||||
// DO NOT USE ConfigureAwait(false) WITH ProcessAsync() unless ensuring ProcessAsync() implementation is cross-thread compatible
|
|
||||||
// ProcessAsync() often does a lot with forms in the UI context
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
// when used in foreach: stateful. deferred execution
|
// when used in foreach: stateful. deferred execution
|
||||||
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable, IEnumerable<LibraryBook> library)
|
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable, IEnumerable<LibraryBook> library)
|
||||||
=> library.Where(libraryBook => processable.Validate(libraryBook));
|
=> library.Where(libraryBook =>
|
||||||
|
processable.Validate(libraryBook)
|
||||||
|
&& (libraryBook.Book.ContentType != ContentType.Episode || FileManager.Configuration.Instance.DownloadEpisodes)
|
||||||
|
);
|
||||||
|
|
||||||
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate)
|
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Dinah.Core.ErrorHandling;
|
|
||||||
|
|
||||||
namespace FileLiberator
|
namespace FileLiberator
|
||||||
{
|
{
|
||||||
public static class LoggerUtilities
|
public static class UtilityExtensions
|
||||||
{
|
{
|
||||||
public static (string id, string title, string locale, string account) LogFriendly(this LibraryBook libraryBook)
|
public static (string id, string title, string locale, string account) LogFriendly(this LibraryBook libraryBook)
|
||||||
=> (
|
=> (
|
||||||
@@ -19,5 +16,11 @@ namespace FileLiberator
|
|||||||
locale: libraryBook.Book.Locale,
|
locale: libraryBook.Book.Locale,
|
||||||
account: libraryBook.Account.ToMask()
|
account: libraryBook.Account.ToMask()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
|
||||||
|
{
|
||||||
|
var apiExtended = await InternalUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||||
|
return apiExtended.Api;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,7 @@ namespace FileManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static object bookDirectoryFilesLocker { get; } = new();
|
||||||
internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ namespace FileManager
|
|||||||
|
|
||||||
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
|
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
|
||||||
{
|
{
|
||||||
extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList();
|
extensions_noDots = Extensions.Select(ext => ext.ToLower().Trim('.')).ToList();
|
||||||
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
|
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
|
||||||
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,8 @@ namespace FileManager
|
|||||||
if (cachedFile != null)
|
if (cachedFile != null)
|
||||||
return cachedFile;
|
return cachedFile;
|
||||||
|
|
||||||
string regexPattern = $@"{productId}.*?\.({extAggr})$";
|
var regex = new Regex($@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
string firstOrNull;
|
string firstOrNull;
|
||||||
|
|
||||||
if (StorageDirectory == BooksDirectory)
|
if (StorageDirectory == BooksDirectory)
|
||||||
@@ -66,7 +68,7 @@ namespace FileManager
|
|||||||
//If user changed the BooksDirectory, reinitialize.
|
//If user changed the BooksDirectory, reinitialize.
|
||||||
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
|
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
|
||||||
{
|
{
|
||||||
lock (BookDirectoryFiles)
|
lock (bookDirectoryFilesLocker)
|
||||||
{
|
{
|
||||||
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
|
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
|
||||||
{
|
{
|
||||||
@@ -75,14 +77,14 @@ namespace FileManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
firstOrNull = BookDirectoryFiles.FindFile(regexPattern, RegexOptions.IgnoreCase);
|
firstOrNull = BookDirectoryFiles.FindFile(regex);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
firstOrNull =
|
firstOrNull =
|
||||||
Directory
|
Directory
|
||||||
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
|
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
|
||||||
.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, RegexOptions.IgnoreCase));
|
.FirstOrDefault(s => regex.IsMatch(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstOrNull is null)
|
if (firstOrNull is null)
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace FileManager
|
namespace FileManager
|
||||||
@@ -25,6 +23,8 @@ namespace FileManager
|
|||||||
private FileSystemWatcher fileSystemWatcher { get; set; }
|
private FileSystemWatcher fileSystemWatcher { get; set; }
|
||||||
private BlockingCollection<FileSystemEventArgs> directoryChangesEvents { get; set; }
|
private BlockingCollection<FileSystemEventArgs> directoryChangesEvents { get; set; }
|
||||||
private Task backgroundScanner { get; set; }
|
private Task backgroundScanner { get; set; }
|
||||||
|
|
||||||
|
private object fsCacheLocker { get; } = new();
|
||||||
private List<string> fsCache { get; } = new();
|
private List<string> fsCache { get; } = new();
|
||||||
|
|
||||||
public BackgroundFileSystem(string rootDirectory, string searchPattern, SearchOption searchOptions)
|
public BackgroundFileSystem(string rootDirectory, string searchPattern, SearchOption searchOptions)
|
||||||
@@ -36,17 +36,15 @@ namespace FileManager
|
|||||||
Init();
|
Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FindFile(string regexPattern, RegexOptions options)
|
public string FindFile(System.Text.RegularExpressions.Regex regex)
|
||||||
{
|
{
|
||||||
lock (fsCache)
|
lock (fsCacheLocker)
|
||||||
{
|
return fsCache.FirstOrDefault(s => regex.IsMatch(s));
|
||||||
return fsCache.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, options));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshFiles()
|
public void RefreshFiles()
|
||||||
{
|
{
|
||||||
lock (fsCache)
|
lock (fsCacheLocker)
|
||||||
{
|
{
|
||||||
fsCache.Clear();
|
fsCache.Clear();
|
||||||
fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption));
|
fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption));
|
||||||
@@ -57,17 +55,19 @@ namespace FileManager
|
|||||||
{
|
{
|
||||||
Stop();
|
Stop();
|
||||||
|
|
||||||
lock (fsCache)
|
lock (fsCacheLocker)
|
||||||
fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption));
|
fsCache.AddRange(Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption));
|
||||||
|
|
||||||
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
|
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
|
||||||
fileSystemWatcher = new FileSystemWatcher(RootDirectory);
|
fileSystemWatcher = new FileSystemWatcher(RootDirectory)
|
||||||
fileSystemWatcher.Created += FileSystemWatcher_Changed;
|
{
|
||||||
|
IncludeSubdirectories = true,
|
||||||
|
EnableRaisingEvents = true
|
||||||
|
};
|
||||||
|
fileSystemWatcher.Created += FileSystemWatcher_Changed;
|
||||||
fileSystemWatcher.Deleted += FileSystemWatcher_Changed;
|
fileSystemWatcher.Deleted += FileSystemWatcher_Changed;
|
||||||
fileSystemWatcher.Renamed += FileSystemWatcher_Changed;
|
fileSystemWatcher.Renamed += FileSystemWatcher_Changed;
|
||||||
fileSystemWatcher.Error += FileSystemWatcher_Error;
|
fileSystemWatcher.Error += FileSystemWatcher_Error;
|
||||||
fileSystemWatcher.IncludeSubdirectories = true;
|
|
||||||
fileSystemWatcher.EnableRaisingEvents = true;
|
|
||||||
|
|
||||||
backgroundScanner = new Task(BackgroundScanner);
|
backgroundScanner = new Task(BackgroundScanner);
|
||||||
backgroundScanner.Start();
|
backgroundScanner.Start();
|
||||||
@@ -86,7 +86,7 @@ namespace FileManager
|
|||||||
//Dispose of directoryChangesEvents after backgroundScanner exists.
|
//Dispose of directoryChangesEvents after backgroundScanner exists.
|
||||||
directoryChangesEvents?.Dispose();
|
directoryChangesEvents?.Dispose();
|
||||||
|
|
||||||
lock (fsCache)
|
lock (fsCacheLocker)
|
||||||
fsCache.Clear();
|
fsCache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ namespace FileManager
|
|||||||
{
|
{
|
||||||
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1))
|
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1))
|
||||||
{
|
{
|
||||||
lock (fsCache)
|
lock (fsCacheLocker)
|
||||||
UpdateLocalCache(change);
|
UpdateLocalCache(change);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,9 +146,7 @@ namespace FileManager
|
|||||||
private void AddUniqueFiles(IEnumerable<string> newFiles)
|
private void AddUniqueFiles(IEnumerable<string> newFiles)
|
||||||
{
|
{
|
||||||
foreach (var file in newFiles)
|
foreach (var file in newFiles)
|
||||||
{
|
|
||||||
AddUniqueFile(file);
|
AddUniqueFile(file);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
private void AddUniqueFile(string newFile)
|
private void AddUniqueFile(string newFile)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -102,6 +102,43 @@ namespace FileManager
|
|||||||
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
|
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum BadBookAction
|
||||||
|
{
|
||||||
|
[Description("Ask each time what action to take.")]
|
||||||
|
Ask = 0,
|
||||||
|
[Description("Stop processing books.")]
|
||||||
|
Abort = 1,
|
||||||
|
[Description("Retry book later. Skip for now. Continue processing books.")]
|
||||||
|
Retry = 2,
|
||||||
|
[Description("Permanently ignore book. Continue processing books. Do not try book again.")]
|
||||||
|
Ignore = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
[Description("When liberating books and there is an error, Libation should:")]
|
||||||
|
public BadBookAction BadBook
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var badBookStr = persistentDictionary.GetString(nameof(BadBook));
|
||||||
|
return Enum.TryParse<BadBookAction>(badBookStr, out var badBookEnum) ? badBookEnum : BadBookAction.Ask;
|
||||||
|
}
|
||||||
|
set => persistentDictionary.SetString(nameof(BadBook), value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||||
|
public bool ImportEpisodes
|
||||||
|
{
|
||||||
|
get => persistentDictionary.GetNonString<bool>(nameof(ImportEpisodes));
|
||||||
|
set => persistentDictionary.SetNonString(nameof(ImportEpisodes), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
|
||||||
|
public bool DownloadEpisodes
|
||||||
|
{
|
||||||
|
get => persistentDictionary.GetNonString<bool>(nameof(DownloadEpisodes));
|
||||||
|
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region known directories
|
#region known directories
|
||||||
@@ -178,16 +215,8 @@ namespace FileManager
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
try
|
var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel");
|
||||||
{
|
return Enum.TryParse<LogEventLevel>(logLevelStr, out var logLevelEnum) ? logLevelEnum : LogEventLevel.Information;
|
||||||
var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel");
|
|
||||||
var logLevelEnum = Enum<LogEventLevel>.Parse(logLevelStr);
|
|
||||||
return logLevelEnum;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return LogEventLevel.Information;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
@@ -219,7 +248,6 @@ namespace FileManager
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region LibationFiles
|
#region LibationFiles
|
||||||
|
|
||||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location), "appsettings.json");
|
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location), "appsettings.json");
|
||||||
private const string LIBATION_FILES_KEY = "LibationFiles";
|
private const string LIBATION_FILES_KEY = "LibationFiles";
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="1.1.0.1" />
|
<PackageReference Include="Dinah.Core" Version="1.1.1.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
||||||
<PackageReference Include="Polly" Version="7.2.2" />
|
<PackageReference Include="Polly" Version="7.2.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace FileManager
|
|||||||
// file max length = 255. dir max len = 247
|
// file max length = 255. dir max len = 247
|
||||||
|
|
||||||
// sanitize
|
// sanitize
|
||||||
filename = GetAsciiTag(filename);
|
filename = getAsciiTag(filename);
|
||||||
// manage length
|
// manage length
|
||||||
if (filename.Length > 50)
|
if (filename.Length > 50)
|
||||||
filename = filename.Substring(0, 50) + "[...]";
|
filename = filename.Substring(0, 50) + "[...]";
|
||||||
@@ -35,7 +35,7 @@ namespace FileManager
|
|||||||
return fullfilename;
|
return fullfilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetAsciiTag(string property)
|
private static string getAsciiTag(string property)
|
||||||
{
|
{
|
||||||
if (property == null)
|
if (property == null)
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -43,11 +43,12 @@ namespace FileManager
|
|||||||
public static event EventHandler<PictureCachedEventArgs> PictureCached;
|
public static event EventHandler<PictureCachedEventArgs> PictureCached;
|
||||||
|
|
||||||
private static BlockingCollection<PictureDefinition> DownloadQueue { get; } = new BlockingCollection<PictureDefinition>();
|
private static BlockingCollection<PictureDefinition> DownloadQueue { get; } = new BlockingCollection<PictureDefinition>();
|
||||||
|
private static object cacheLocker { get; } = new object();
|
||||||
private static Dictionary<PictureDefinition, byte[]> cache { get; } = new Dictionary<PictureDefinition, byte[]>();
|
private static Dictionary<PictureDefinition, byte[]> cache { get; } = new Dictionary<PictureDefinition, byte[]>();
|
||||||
private static Dictionary<PictureSize, byte[]> defaultImages { get; } = new Dictionary<PictureSize, byte[]>();
|
private static Dictionary<PictureSize, byte[]> defaultImages { get; } = new Dictionary<PictureSize, byte[]>();
|
||||||
public static (bool isDefault, byte[] bytes) GetPicture(PictureDefinition def)
|
public static (bool isDefault, byte[] bytes) GetPicture(PictureDefinition def)
|
||||||
{
|
{
|
||||||
lock (cache)
|
lock (cacheLocker)
|
||||||
{
|
{
|
||||||
if (cache.ContainsKey(def))
|
if (cache.ContainsKey(def))
|
||||||
return (false, cache[def]);
|
return (false, cache[def]);
|
||||||
@@ -67,7 +68,7 @@ namespace FileManager
|
|||||||
|
|
||||||
public static byte[] GetPictureSynchronously(PictureDefinition def)
|
public static byte[] GetPictureSynchronously(PictureDefinition def)
|
||||||
{
|
{
|
||||||
lock (cache)
|
lock (cacheLocker)
|
||||||
{
|
{
|
||||||
if (!cache.ContainsKey(def) || cache[def] == null)
|
if (!cache.ContainsKey(def) || cache[def] == null)
|
||||||
{
|
{
|
||||||
@@ -104,7 +105,7 @@ namespace FileManager
|
|||||||
|
|
||||||
var bytes = downloadBytes(def);
|
var bytes = downloadBytes(def);
|
||||||
saveFile(def, bytes);
|
saveFile(def, bytes);
|
||||||
lock (cache)
|
lock (cacheLocker)
|
||||||
cache[def] = bytes;
|
cache[def] = bytes;
|
||||||
|
|
||||||
PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs { Definition = def, Picture = bytes });
|
PictureCached?.Invoke(nameof(PictureStorage), new PictureCachedEventArgs { Definition = def, Picture = bytes });
|
||||||
|
|||||||
@@ -10,36 +10,95 @@ using Polly.Retry;
|
|||||||
|
|
||||||
namespace InternalUtilities
|
namespace InternalUtilities
|
||||||
{
|
{
|
||||||
public static class AudibleApiActions
|
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
|
||||||
|
public class ApiExtended
|
||||||
{
|
{
|
||||||
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
|
public Api Api { get; private set; }
|
||||||
public static Task<Api> GetApiAsync(string username, string localeName, ILoginCallback loginCallback = null)
|
|
||||||
|
private ApiExtended(Api api) => Api = api;
|
||||||
|
|
||||||
|
/// <summary>Get api from existing tokens else login with 'eager' choice. External browser url is provided. Response can be external browser login or continuing with native api callbacks.</summary>
|
||||||
|
public static async Task<ApiExtended> CreateAsync(Account account, ILoginChoiceEager loginChoiceEager)
|
||||||
{
|
{
|
||||||
Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new
|
Serilog.Log.Logger.Information("{@DebugInfo}", new
|
||||||
|
{
|
||||||
|
LoginType = nameof(ILoginChoiceEager),
|
||||||
|
Account = account?.MaskedLogEntry ?? "[null]",
|
||||||
|
LocaleName = account?.Locale?.Name
|
||||||
|
});
|
||||||
|
|
||||||
|
var api = await EzApiCreator.GetApiAsync(
|
||||||
|
loginChoiceEager,
|
||||||
|
account.Locale,
|
||||||
|
AudibleApiStorage.AccountsSettingsFile,
|
||||||
|
account.GetIdentityTokensJsonPath());
|
||||||
|
return new ApiExtended(api);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get api from existing tokens else login with native api callbacks.</summary>
|
||||||
|
public static async Task<ApiExtended> CreateAsync(Account account, ILoginCallback loginCallback)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("{@DebugInfo}", new
|
||||||
|
{
|
||||||
|
LoginType = nameof(ILoginCallback),
|
||||||
|
Account = account?.MaskedLogEntry ?? "[null]",
|
||||||
|
LocaleName = account?.Locale?.Name
|
||||||
|
});
|
||||||
|
|
||||||
|
var api = await EzApiCreator.GetApiAsync(
|
||||||
|
loginCallback,
|
||||||
|
account.Locale,
|
||||||
|
AudibleApiStorage.AccountsSettingsFile,
|
||||||
|
account.GetIdentityTokensJsonPath());
|
||||||
|
return new ApiExtended(api);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get api from existing tokens else login with external browser</summary>
|
||||||
|
public static async Task<ApiExtended> CreateAsync(Account account, ILoginExternal loginExternal)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("{@DebugInfo}", new
|
||||||
|
{
|
||||||
|
LoginType = nameof(ILoginExternal),
|
||||||
|
Account = account?.MaskedLogEntry ?? "[null]",
|
||||||
|
LocaleName = account?.Locale?.Name
|
||||||
|
});
|
||||||
|
|
||||||
|
var api = await EzApiCreator.GetApiAsync(
|
||||||
|
loginExternal,
|
||||||
|
account.Locale,
|
||||||
|
AudibleApiStorage.AccountsSettingsFile,
|
||||||
|
account.GetIdentityTokensJsonPath());
|
||||||
|
return new ApiExtended(api);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary>
|
||||||
|
public static async Task<ApiExtended> CreateAsync(Account account)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||||
|
ArgumentValidator.EnsureNotNull(account.Locale, nameof(account.Locale));
|
||||||
|
|
||||||
|
Serilog.Log.Logger.Information("{@DebugInfo}", new
|
||||||
|
{
|
||||||
|
AccountMaskedLogEntry = account.MaskedLogEntry
|
||||||
|
});
|
||||||
|
|
||||||
|
return await CreateAsync(account.AccountId, account.Locale.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary>
|
||||||
|
public static async Task<ApiExtended> CreateAsync(string username, string localeName)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("{@DebugInfo}", new
|
||||||
{
|
{
|
||||||
Username = username.ToMask(),
|
Username = username.ToMask(),
|
||||||
LocaleName = localeName,
|
LocaleName = localeName,
|
||||||
});
|
});
|
||||||
return EzApiCreator.GetApiAsync(
|
|
||||||
Localization.Get(localeName),
|
|
||||||
AudibleApiStorage.AccountsSettingsFile,
|
|
||||||
AudibleApiStorage.GetIdentityTokensJsonPath(username, localeName),
|
|
||||||
loginCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>USE THIS from within Libation. It wraps the call with correct JSONPath</summary>
|
var api = await EzApiCreator.GetApiAsync(
|
||||||
public static Task<Api> GetApiAsync(ILoginCallback loginCallback, Account account)
|
Localization.Get(localeName),
|
||||||
{
|
AudibleApiStorage.AccountsSettingsFile,
|
||||||
Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new
|
AudibleApiStorage.GetIdentityTokensJsonPath(username, localeName));
|
||||||
{
|
return new ApiExtended(api);
|
||||||
Account = account?.MaskedLogEntry ?? "[null]",
|
|
||||||
LocaleName = account?.Locale?.Name
|
|
||||||
});
|
|
||||||
return EzApiCreator.GetApiAsync(
|
|
||||||
account.Locale,
|
|
||||||
AudibleApiStorage.AccountsSettingsFile,
|
|
||||||
account.GetIdentityTokensJsonPath(),
|
|
||||||
loginCallback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AsyncRetryPolicy policy { get; }
|
private static AsyncRetryPolicy policy { get; }
|
||||||
@@ -47,33 +106,36 @@ namespace InternalUtilities
|
|||||||
// 2 retries == 3 total
|
// 2 retries == 3 total
|
||||||
.RetryAsync(2);
|
.RetryAsync(2);
|
||||||
|
|
||||||
public static Task<List<Item>> GetLibraryValidatedAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS)
|
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions.ResponseGroupOptions responseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS, bool importEpisodes = true)
|
||||||
{
|
{
|
||||||
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or tokens are refreshed
|
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or tokens are refreshed
|
||||||
// worse, this 1st dummy call doesn't seem to help:
|
// worse, this 1st dummy call doesn't seem to help:
|
||||||
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
|
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
|
||||||
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
|
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
|
||||||
return policy.ExecuteAsync(() => getItemsAsync(api, responseGroups));
|
return policy.ExecuteAsync(() => getItemsAsync(responseGroups, importEpisodes));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<List<Item>> getItemsAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups)
|
private async Task<List<Item>> getItemsAsync(LibraryOptions.ResponseGroupOptions responseGroups, bool importEpisodes)
|
||||||
{
|
{
|
||||||
var items = new List<Item>();
|
var items = new List<Item>();
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
//// this will not work for multi accounts
|
//// this will not work for multi accounts
|
||||||
//var library_json = "library.json";
|
//var library_json = "library.json";
|
||||||
|
//library_json = System.IO.Path.GetFullPath(library_json);
|
||||||
//if (System.IO.File.Exists(library_json))
|
//if (System.IO.File.Exists(library_json))
|
||||||
//{
|
//{
|
||||||
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
|
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
|
||||||
//}
|
//}
|
||||||
#endif
|
#endif
|
||||||
if (!items.Any())
|
if (!items.Any())
|
||||||
items = await api.GetAllLibraryItemsAsync(responseGroups);
|
items = await Api.GetAllLibraryItemsAsync(responseGroups);
|
||||||
#if DEBUG
|
|
||||||
//System.IO.File.WriteAllText("library.json", AudibleApi.Common.Converter.ToJson(items));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
await manageEpisodesAsync(api, items);
|
if (importEpisodes)
|
||||||
|
await manageEpisodesAsync(items);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
|
||||||
|
#endif
|
||||||
|
|
||||||
var validators = new List<IValidator>();
|
var validators = new List<IValidator>();
|
||||||
validators.AddRange(getValidators());
|
validators.AddRange(getValidators());
|
||||||
@@ -88,7 +150,7 @@ namespace InternalUtilities
|
|||||||
}
|
}
|
||||||
|
|
||||||
#region episodes and podcasts
|
#region episodes and podcasts
|
||||||
private static async Task manageEpisodesAsync(Api api, List<Item> items)
|
private async Task manageEpisodesAsync(List<Item> items)
|
||||||
{
|
{
|
||||||
// add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import
|
// add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import
|
||||||
try
|
try
|
||||||
@@ -110,7 +172,7 @@ namespace InternalUtilities
|
|||||||
items.RemoveAll(i => i.IsEpisodes);
|
items.RemoveAll(i => i.IsEpisodes);
|
||||||
|
|
||||||
// add children
|
// add children
|
||||||
var children = await getEpisodesAsync(api, parents);
|
var children = await getEpisodesAsync(parents);
|
||||||
Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found");
|
Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found");
|
||||||
items.AddRange(children);
|
items.AddRange(children);
|
||||||
}
|
}
|
||||||
@@ -120,13 +182,13 @@ namespace InternalUtilities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<List<Item>> getEpisodesAsync(Api api, List<Item> parents)
|
private async Task<List<Item>> getEpisodesAsync(List<Item> parents)
|
||||||
{
|
{
|
||||||
var results = new List<Item>();
|
var results = new List<Item>();
|
||||||
|
|
||||||
foreach (var parent in parents)
|
foreach (var parent in parents)
|
||||||
{
|
{
|
||||||
var children = await getEpisodeChildrenAsync(api, parent);
|
var children = await getEpisodeChildrenAsync(parent);
|
||||||
|
|
||||||
foreach (var child in children)
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
@@ -159,7 +221,7 @@ namespace InternalUtilities
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<List<Item>> getEpisodeChildrenAsync(Api api, Item parent)
|
private async Task<List<Item>> getEpisodeChildrenAsync(Item parent)
|
||||||
{
|
{
|
||||||
var childrenIds = parent.Relationships
|
var childrenIds = parent.Relationships
|
||||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
|
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
|
||||||
@@ -180,7 +242,7 @@ namespace InternalUtilities
|
|||||||
List<Item> childrenBatch;
|
List<Item> childrenBatch;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
childrenBatch = await api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
childrenBatch = await Api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
|
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
|
||||||
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
|
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
|
||||||
@@ -222,7 +284,7 @@ namespace InternalUtilities
|
|||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private static List<IValidator> getValidators()
|
private static List<IValidator> getValidators()
|
||||||
{
|
{
|
||||||
@@ -33,8 +33,10 @@ namespace InternalUtilities
|
|||||||
|
|
||||||
if (items.Any(i => string.IsNullOrWhiteSpace(i.ProductId)))
|
if (items.Any(i => string.IsNullOrWhiteSpace(i.ProductId)))
|
||||||
exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.ProductId)}", nameof(items)));
|
exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.ProductId)}", nameof(items)));
|
||||||
if (items.Any(i => string.IsNullOrWhiteSpace(i.Title)))
|
|
||||||
exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.Title)}", nameof(items)));
|
// this can happen with podcast episodes
|
||||||
|
foreach (var i in items.Where(i => string.IsNullOrWhiteSpace(i.Title)))
|
||||||
|
i.Title = "[blank title]";
|
||||||
|
|
||||||
return exceptions;
|
return exceptions;
|
||||||
}
|
}
|
||||||
@@ -76,9 +78,9 @@ namespace InternalUtilities
|
|||||||
|
|
||||||
var distinct = items.GetSeriesDistinct();
|
var distinct = items.GetSeriesDistinct();
|
||||||
if (distinct.Any(s => s.SeriesId is null))
|
if (distinct.Any(s => s.SeriesId is null))
|
||||||
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApi.Common.Series.SeriesId)}", nameof(items)));
|
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesId)}", nameof(items)));
|
||||||
if (distinct.Any(s => s.SeriesName is null))
|
if (distinct.Any(s => s.SeriesName is null))
|
||||||
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApi.Common.Series.SeriesName)}", nameof(items)));
|
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesName)}", nameof(items)));
|
||||||
|
|
||||||
return exceptions;
|
return exceptions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AudibleApi" Version="1.2.1.2" />
|
<PackageReference Include="AudibleApi" Version="2.2.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ EndProject
|
|||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine", "LibationSearchEngine\LibationSearchEngine.csproj", "{2E1F5DB4-40CC-4804-A893-5DCE0193E598}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine", "LibationSearchEngine\LibationSearchEngine.csproj", "{2E1F5DB4-40CC-4804-A893-5DCE0193E598}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationWinForms", "LibationWinForms\LibationWinForms.csproj", "{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationWinForms", "LibationWinForms\LibationWinForms.csproj", "{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}"
|
||||||
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
|
{428163C3-D558-4914-B570-A92069521877} = {428163C3-D558-4914-B570-A92069521877}
|
||||||
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtoImporterService", "DtoImporterService\DtoImporterService.csproj", "{401865F5-1942-4713-B230-04544C0A97B0}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtoImporterService", "DtoImporterService\DtoImporterService.csproj", "{401865F5-1942-4713-B230-04544C0A97B0}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|||||||
@@ -12,6 +12,14 @@
|
|||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
When LibationWinForms and LibationCli output to the same dir, LibationCli must build before LibationWinForms
|
||||||
|
|
||||||
|
VS > rt-clik solution > Project Build Order...
|
||||||
|
Dependencies [tab]
|
||||||
|
Projects: LibationWinForms
|
||||||
|
manually check LibationCli
|
||||||
|
-->
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
<OutputPath>..\LibationWinForms\bin\Debug</OutputPath>
|
<OutputPath>..\LibationWinForms\bin\Debug</OutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ namespace LibationCli
|
|||||||
: $"Scanning Audible library: {_accounts.Length} accounts. This may take a few minutes per account.";
|
: $"Scanning Audible library: {_accounts.Length} accounts. This may take a few minutes per account.";
|
||||||
Console.WriteLine(intro);
|
Console.WriteLine(intro);
|
||||||
|
|
||||||
var (TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync(
|
var (TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync((a) => ApiExtended.CreateAsync(a), _accounts);
|
||||||
(account) => null,
|
|
||||||
_accounts);
|
|
||||||
|
|
||||||
Console.WriteLine("Scan complete.");
|
Console.WriteLine("Scan complete.");
|
||||||
Console.WriteLine($"Total processed: {TotalBooksProcessed}\r\nNew: {NewBooksAdded}");
|
Console.WriteLine($"Total processed: {TotalBooksProcessed}\r\nNew: {NewBooksAdded}");
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace LibationCli
|
|||||||
string input = null;
|
string input = null;
|
||||||
|
|
||||||
//input = " export --help";
|
//input = " export --help";
|
||||||
//input = " scan cupidneedsglasses";
|
//input = " scan rmcrackan";
|
||||||
//input = " liberate ";
|
//input = " liberate ";
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ namespace LibationCli
|
|||||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||||
|
|
||||||
|
|
||||||
LibationScaffolding.RunPostConfigMigrations();
|
LibationScaffolding.RunPostConfigMigrations(config);
|
||||||
LibationScaffolding.RunPostMigrationScaffolding();
|
LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
checkForUpdate();
|
checkForUpdate();
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ namespace LibationSearchEngine
|
|||||||
var docs = searcher.Search(query, 1);
|
var docs = searcher.Search(query, 1);
|
||||||
var scoreDoc = docs.ScoreDocs.SingleOrDefault();
|
var scoreDoc = docs.ScoreDocs.SingleOrDefault();
|
||||||
if (scoreDoc == null)
|
if (scoreDoc == null)
|
||||||
throw new Exception("document not found");
|
return;
|
||||||
var document = searcher.Doc(scoreDoc.Doc);
|
var document = searcher.Doc(scoreDoc.Doc);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ namespace LibationWinForms.BookLiberation
|
|||||||
base.OnBegin(sender, libraryBook);
|
base.OnBegin(sender, libraryBook);
|
||||||
}
|
}
|
||||||
public override void OnCompleted(object sender, LibraryBook 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
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ namespace LibationWinForms.BookLiberation
|
|||||||
#region IProcessable event handler overrides
|
#region IProcessable event handler overrides
|
||||||
public override void OnBegin(object sender, LibraryBook libraryBook)
|
public override void OnBegin(object sender, LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
|
base.OnBegin(sender, libraryBook);
|
||||||
|
|
||||||
GetCoverArtDelegate = () => FileManager.PictureStorage.GetPictureSynchronously(
|
GetCoverArtDelegate = () => FileManager.PictureStorage.GetPictureSynchronously(
|
||||||
new FileManager.PictureDefinition(
|
new FileManager.PictureDefinition(
|
||||||
libraryBook.Book.PictureId,
|
libraryBook.Book.PictureId,
|
||||||
@@ -41,6 +43,7 @@ namespace LibationWinForms.BookLiberation
|
|||||||
#region IStreamable event handler overrides
|
#region IStreamable event handler overrides
|
||||||
public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress)
|
public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress)
|
||||||
{
|
{
|
||||||
|
base.OnStreamingProgressChanged(sender, downloadProgress);
|
||||||
if (!downloadProgress.ProgressPercentage.HasValue)
|
if (!downloadProgress.ProgressPercentage.HasValue)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -51,16 +54,23 @@ namespace LibationWinForms.BookLiberation
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override void OnStreamingTimeRemaining(object sender, TimeSpan timeRemaining)
|
public override void OnStreamingTimeRemaining(object sender, TimeSpan timeRemaining)
|
||||||
=> updateRemainingTime((int)timeRemaining.TotalSeconds);
|
{
|
||||||
|
base.OnStreamingTimeRemaining(sender, timeRemaining);
|
||||||
|
updateRemainingTime((int)timeRemaining.TotalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region IAudioDecodable event handlers
|
#region IAudioDecodable event handlers
|
||||||
public override void OnRequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
|
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)
|
public override void OnTitleDiscovered(object sender, string title)
|
||||||
{
|
{
|
||||||
|
base.OnTitleDiscovered(sender, title);
|
||||||
this.UIThreadAsync(() => this.Text = DecodeActionName + " " + title);
|
this.UIThreadAsync(() => this.Text = DecodeActionName + " " + title);
|
||||||
this.title = title;
|
this.title = title;
|
||||||
updateBookInfo();
|
updateBookInfo();
|
||||||
@@ -68,18 +78,23 @@ namespace LibationWinForms.BookLiberation
|
|||||||
|
|
||||||
public override void OnAuthorsDiscovered(object sender, string authors)
|
public override void OnAuthorsDiscovered(object sender, string authors)
|
||||||
{
|
{
|
||||||
|
base.OnAuthorsDiscovered(sender, authors);
|
||||||
authorNames = authors;
|
authorNames = authors;
|
||||||
updateBookInfo();
|
updateBookInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNarratorsDiscovered(object sender, string narrators)
|
public override void OnNarratorsDiscovered(object sender, string narrators)
|
||||||
{
|
{
|
||||||
|
base.OnNarratorsDiscovered(sender, narrators);
|
||||||
narratorNames = narrators;
|
narratorNames = narrators;
|
||||||
updateBookInfo();
|
updateBookInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnCoverImageDiscovered(object sender, byte[] coverArt)
|
public override void OnCoverImageDiscovered(object sender, byte[] coverArt)
|
||||||
=> pictureBox1.UIThreadAsync(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt));
|
{
|
||||||
|
base.OnCoverImageDiscovered(sender, coverArt);
|
||||||
|
pictureBox1.UIThreadAsync(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt));
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
// thread-safe UI updates
|
// thread-safe UI updates
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ namespace LibationWinForms.BookLiberation
|
|||||||
base.OnBegin(sender, libraryBook);
|
base.OnBegin(sender, libraryBook);
|
||||||
}
|
}
|
||||||
public override void OnCompleted(object sender, LibraryBook 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
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,24 +137,36 @@ namespace LibationWinForms.BookLiberation.BaseForms
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region IStreamable event handlers
|
#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 OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress) { }
|
||||||
public virtual void OnStreamingTimeRemaining(object sender, TimeSpan timeRemaining) { }
|
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
|
#endregion
|
||||||
|
|
||||||
#region IProcessable event handlers
|
#region IProcessable event handlers
|
||||||
public virtual void OnBegin(object sender, LibraryBook libraryBook) { }
|
public virtual void OnBegin(object sender, LibraryBook libraryBook)
|
||||||
public virtual void OnStatusUpdate(object sender, string statusUpdate) { }
|
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IProcessable.Begin), Book = libraryBook.LogFriendly() });
|
||||||
public virtual void OnCompleted(object sender, LibraryBook libraryBook) { }
|
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
|
#endregion
|
||||||
|
|
||||||
#region IAudioDecodable event handlers
|
#region IAudioDecodable event handlers
|
||||||
public virtual void OnRequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate) { }
|
public virtual void OnRequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
|
||||||
public virtual void OnTitleDiscovered(object sender, string title) { }
|
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IAudioDecodable.RequestCoverArt) });
|
||||||
public virtual void OnAuthorsDiscovered(object sender, string authors) { }
|
public virtual void OnTitleDiscovered(object sender, string title)
|
||||||
public virtual void OnNarratorsDiscovered(object sender, string narrators) { }
|
=> Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(IAudioDecodable.TitleDiscovered), Title = title });
|
||||||
public virtual void OnCoverImageDiscovered(object sender, byte[] coverArt) { }
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,10 +20,12 @@ namespace LibationWinForms.BookLiberation
|
|||||||
#region IStreamable event handler overrides
|
#region IStreamable event handler overrides
|
||||||
public override void OnStreamingBegin(object sender, string beginString)
|
public override void OnStreamingBegin(object sender, string beginString)
|
||||||
{
|
{
|
||||||
|
base.OnStreamingBegin(sender, beginString);
|
||||||
filenameLbl.UIThreadAsync(() => filenameLbl.Text = beginString);
|
filenameLbl.UIThreadAsync(() => filenameLbl.Text = beginString);
|
||||||
}
|
}
|
||||||
public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress)
|
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
|
// this won't happen with download file. it will happen with download string
|
||||||
if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0)
|
if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ namespace LibationWinForms.BookLiberation
|
|||||||
{
|
{
|
||||||
internal class PdfDownloadForm : DownloadForm
|
internal class PdfDownloadForm : DownloadForm
|
||||||
{
|
{
|
||||||
public override void OnBegin(object sender, LibraryBook libraryBook) => LogMe.Info($"PDF Step, Begin: {libraryBook.Book}");
|
public override void OnBegin(object sender, LibraryBook libraryBook)
|
||||||
public override void OnCompleted(object sender, LibraryBook libraryBook) => LogMe.Info($"PDF Step, Completed: {libraryBook.Book}");
|
{
|
||||||
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,8 +196,6 @@ namespace LibationWinForms.BookLiberation
|
|||||||
|
|
||||||
protected async Task<bool> ProcessOneAsync(LibraryBook libraryBook, bool validate)
|
protected async Task<bool> ProcessOneAsync(LibraryBook libraryBook, bool validate)
|
||||||
{
|
{
|
||||||
string logMessage;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var statusHandler = await Processable.ProcessSingleAsync(libraryBook, validate);
|
var statusHandler = await Processable.ProcessSingleAsync(libraryBook, validate);
|
||||||
@@ -207,18 +205,28 @@ namespace LibationWinForms.BookLiberation
|
|||||||
|
|
||||||
foreach (var errorMessage in statusHandler.Errors)
|
foreach (var errorMessage in statusHandler.Errors)
|
||||||
LogMe.Error(errorMessage);
|
LogMe.Error(errorMessage);
|
||||||
|
|
||||||
logMessage = statusHandler.Errors.Aggregate((a, b) => $"{a}\r\n{b}");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LogMe.Error(ex);
|
LogMe.Error(ex);
|
||||||
|
|
||||||
logMessage = ex.Message + "\r\n|\r\n" + ex.StackTrace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return showRetry(libraryBook);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool showRetry(LibraryBook libraryBook)
|
||||||
|
{
|
||||||
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
|
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
|
||||||
|
|
||||||
|
DialogResult? dialogResult = FileManager.Configuration.Instance.BadBook switch
|
||||||
|
{
|
||||||
|
FileManager.Configuration.BadBookAction.Abort => DialogResult.Abort,
|
||||||
|
FileManager.Configuration.BadBookAction.Retry => DialogResult.Retry,
|
||||||
|
FileManager.Configuration.BadBookAction.Ignore => DialogResult.Ignore,
|
||||||
|
FileManager.Configuration.BadBookAction.Ask => null,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
string details;
|
string details;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -238,7 +246,8 @@ $@" Title: {libraryBook.Book.Title}
|
|||||||
details = "[Error retrieving details]";
|
details = "[Error retrieving details]";
|
||||||
}
|
}
|
||||||
|
|
||||||
var dialogResult = MessageBox.Show(string.Format(SkipDialogText, details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
|
// if null then ask user
|
||||||
|
dialogResult ??= MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
|
||||||
|
|
||||||
if (dialogResult == DialogResult.Abort)
|
if (dialogResult == DialogResult.Abort)
|
||||||
return false;
|
return false;
|
||||||
@@ -288,7 +297,7 @@ An error occurred while trying to process this book. Skip this book permanently?
|
|||||||
An error occurred while trying to process this book.
|
An error occurred while trying to process this book.
|
||||||
{0}
|
{0}
|
||||||
|
|
||||||
- ABORT: stop processing books.
|
- ABORT: Stop processing books.
|
||||||
|
|
||||||
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
|
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync((account) => new WinformResponder(account), _accounts);
|
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.ImportAccountAsync((account) => ApiExtended.CreateAsync(account, new WinformLoginChoiceEager(account)), _accounts);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
+22
-15
@@ -1,6 +1,6 @@
|
|||||||
namespace LibationWinForms.Dialogs.Login
|
namespace LibationWinForms.Dialogs.Login
|
||||||
{
|
{
|
||||||
partial class AudibleLoginDialog
|
partial class LoginCallbackDialog
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required designer variable.
|
/// Required designer variable.
|
||||||
@@ -38,25 +38,29 @@
|
|||||||
// passwordLbl
|
// passwordLbl
|
||||||
//
|
//
|
||||||
this.passwordLbl.AutoSize = true;
|
this.passwordLbl.AutoSize = true;
|
||||||
this.passwordLbl.Location = new System.Drawing.Point(12, 41);
|
this.passwordLbl.Location = new System.Drawing.Point(14, 47);
|
||||||
|
this.passwordLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
this.passwordLbl.Name = "passwordLbl";
|
this.passwordLbl.Name = "passwordLbl";
|
||||||
this.passwordLbl.Size = new System.Drawing.Size(53, 13);
|
this.passwordLbl.Size = new System.Drawing.Size(57, 15);
|
||||||
this.passwordLbl.TabIndex = 2;
|
this.passwordLbl.TabIndex = 2;
|
||||||
this.passwordLbl.Text = "Password";
|
this.passwordLbl.Text = "Password";
|
||||||
//
|
//
|
||||||
// passwordTb
|
// passwordTb
|
||||||
//
|
//
|
||||||
this.passwordTb.Location = new System.Drawing.Point(71, 38);
|
this.passwordTb.Location = new System.Drawing.Point(83, 44);
|
||||||
|
this.passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.passwordTb.Name = "passwordTb";
|
this.passwordTb.Name = "passwordTb";
|
||||||
this.passwordTb.PasswordChar = '*';
|
this.passwordTb.PasswordChar = '*';
|
||||||
this.passwordTb.Size = new System.Drawing.Size(200, 20);
|
this.passwordTb.Size = new System.Drawing.Size(233, 23);
|
||||||
this.passwordTb.TabIndex = 3;
|
this.passwordTb.TabIndex = 3;
|
||||||
//
|
//
|
||||||
// submitBtn
|
// submitBtn
|
||||||
//
|
//
|
||||||
this.submitBtn.Location = new System.Drawing.Point(196, 64);
|
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.submitBtn.Location = new System.Drawing.Point(229, 74);
|
||||||
|
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.submitBtn.Name = "submitBtn";
|
this.submitBtn.Name = "submitBtn";
|
||||||
this.submitBtn.Size = new System.Drawing.Size(75, 23);
|
this.submitBtn.Size = new System.Drawing.Size(88, 27);
|
||||||
this.submitBtn.TabIndex = 4;
|
this.submitBtn.TabIndex = 4;
|
||||||
this.submitBtn.Text = "Submit";
|
this.submitBtn.Text = "Submit";
|
||||||
this.submitBtn.UseVisualStyleBackColor = true;
|
this.submitBtn.UseVisualStyleBackColor = true;
|
||||||
@@ -65,36 +69,39 @@
|
|||||||
// localeLbl
|
// localeLbl
|
||||||
//
|
//
|
||||||
this.localeLbl.AutoSize = true;
|
this.localeLbl.AutoSize = true;
|
||||||
this.localeLbl.Location = new System.Drawing.Point(12, 9);
|
this.localeLbl.Location = new System.Drawing.Point(14, 10);
|
||||||
|
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
this.localeLbl.Name = "localeLbl";
|
this.localeLbl.Name = "localeLbl";
|
||||||
this.localeLbl.Size = new System.Drawing.Size(59, 13);
|
this.localeLbl.Size = new System.Drawing.Size(61, 15);
|
||||||
this.localeLbl.TabIndex = 0;
|
this.localeLbl.TabIndex = 0;
|
||||||
this.localeLbl.Text = "Locale: {0}";
|
this.localeLbl.Text = "Locale: {0}";
|
||||||
//
|
//
|
||||||
// usernameLbl
|
// usernameLbl
|
||||||
//
|
//
|
||||||
this.usernameLbl.AutoSize = true;
|
this.usernameLbl.AutoSize = true;
|
||||||
this.usernameLbl.Location = new System.Drawing.Point(12, 22);
|
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
|
||||||
|
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
this.usernameLbl.Name = "usernameLbl";
|
this.usernameLbl.Name = "usernameLbl";
|
||||||
this.usernameLbl.Size = new System.Drawing.Size(75, 13);
|
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
|
||||||
this.usernameLbl.TabIndex = 1;
|
this.usernameLbl.TabIndex = 1;
|
||||||
this.usernameLbl.Text = "Username: {0}";
|
this.usernameLbl.Text = "Username: {0}";
|
||||||
//
|
//
|
||||||
// AudibleLoginDialog
|
// LoginCallbackDialog
|
||||||
//
|
//
|
||||||
this.AcceptButton = this.submitBtn;
|
this.AcceptButton = this.submitBtn;
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
this.ClientSize = new System.Drawing.Size(283, 99);
|
this.ClientSize = new System.Drawing.Size(330, 114);
|
||||||
this.Controls.Add(this.usernameLbl);
|
this.Controls.Add(this.usernameLbl);
|
||||||
this.Controls.Add(this.localeLbl);
|
this.Controls.Add(this.localeLbl);
|
||||||
this.Controls.Add(this.submitBtn);
|
this.Controls.Add(this.submitBtn);
|
||||||
this.Controls.Add(this.passwordLbl);
|
this.Controls.Add(this.passwordLbl);
|
||||||
this.Controls.Add(this.passwordTb);
|
this.Controls.Add(this.passwordTb);
|
||||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||||
|
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.MaximizeBox = false;
|
this.MaximizeBox = false;
|
||||||
this.MinimizeBox = false;
|
this.MinimizeBox = false;
|
||||||
this.Name = "AudibleLoginDialog";
|
this.Name = "LoginCallbackDialog";
|
||||||
this.ShowIcon = false;
|
this.ShowIcon = false;
|
||||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
this.Text = "Audible Login";
|
this.Text = "Audible Login";
|
||||||
+6
-9
@@ -1,28 +1,25 @@
|
|||||||
using Dinah.Core;
|
using System;
|
||||||
using InternalUtilities;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using Dinah.Core;
|
||||||
|
using InternalUtilities;
|
||||||
|
|
||||||
namespace LibationWinForms.Dialogs.Login
|
namespace LibationWinForms.Dialogs.Login
|
||||||
{
|
{
|
||||||
public partial class AudibleLoginDialog : Form
|
public partial class LoginCallbackDialog : Form
|
||||||
{
|
{
|
||||||
private string locale { get; }
|
|
||||||
private string accountId { get; }
|
private string accountId { get; }
|
||||||
|
|
||||||
public string Email { get; private set; }
|
public string Email { get; private set; }
|
||||||
public string Password { get; private set; }
|
public string Password { get; private set; }
|
||||||
|
|
||||||
public AudibleLoginDialog(Account account)
|
public LoginCallbackDialog(Account account)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
locale = account.Locale.Name;
|
|
||||||
accountId = account.AccountId;
|
accountId = account.AccountId;
|
||||||
|
|
||||||
// do not allow user to change login id here. if they do then jsonpath will fail
|
// do not allow user to change login id here. if they do then jsonpath will fail
|
||||||
this.localeLbl.Text = string.Format(this.localeLbl.Text, locale);
|
this.localeLbl.Text = string.Format(this.localeLbl.Text, account.Locale.Name);
|
||||||
this.usernameLbl.Text = string.Format(this.usernameLbl.Text, accountId);
|
this.usernameLbl.Text = string.Format(this.usernameLbl.Text, accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
+1
-2
@@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<root>
|
||||||
<root>
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
<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:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
namespace LibationWinForms.Dialogs.Login
|
||||||
|
{
|
||||||
|
partial class LoginChoiceEagerDialog
|
||||||
|
{
|
||||||
|
/// <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.passwordLbl = new System.Windows.Forms.Label();
|
||||||
|
this.passwordTb = new System.Windows.Forms.TextBox();
|
||||||
|
this.submitBtn = new System.Windows.Forms.Button();
|
||||||
|
this.localeLbl = new System.Windows.Forms.Label();
|
||||||
|
this.usernameLbl = new System.Windows.Forms.Label();
|
||||||
|
this.externalLoginLink = new System.Windows.Forms.LinkLabel();
|
||||||
|
this.externalLoginLbl2 = new System.Windows.Forms.Label();
|
||||||
|
this.externalLoginLbl1 = new System.Windows.Forms.Label();
|
||||||
|
this.SuspendLayout();
|
||||||
|
//
|
||||||
|
// passwordLbl
|
||||||
|
//
|
||||||
|
this.passwordLbl.AutoSize = true;
|
||||||
|
this.passwordLbl.Location = new System.Drawing.Point(14, 47);
|
||||||
|
this.passwordLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
|
this.passwordLbl.Name = "passwordLbl";
|
||||||
|
this.passwordLbl.Size = new System.Drawing.Size(57, 15);
|
||||||
|
this.passwordLbl.TabIndex = 2;
|
||||||
|
this.passwordLbl.Text = "Password";
|
||||||
|
//
|
||||||
|
// passwordTb
|
||||||
|
//
|
||||||
|
this.passwordTb.Location = new System.Drawing.Point(83, 44);
|
||||||
|
this.passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
|
this.passwordTb.Name = "passwordTb";
|
||||||
|
this.passwordTb.PasswordChar = '*';
|
||||||
|
this.passwordTb.Size = new System.Drawing.Size(233, 23);
|
||||||
|
this.passwordTb.TabIndex = 3;
|
||||||
|
//
|
||||||
|
// submitBtn
|
||||||
|
//
|
||||||
|
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.submitBtn.Location = new System.Drawing.Point(293, 176);
|
||||||
|
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
|
this.submitBtn.Name = "submitBtn";
|
||||||
|
this.submitBtn.Size = new System.Drawing.Size(88, 27);
|
||||||
|
this.submitBtn.TabIndex = 7;
|
||||||
|
this.submitBtn.Text = "Submit";
|
||||||
|
this.submitBtn.UseVisualStyleBackColor = true;
|
||||||
|
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
|
||||||
|
//
|
||||||
|
// localeLbl
|
||||||
|
//
|
||||||
|
this.localeLbl.AutoSize = true;
|
||||||
|
this.localeLbl.Location = new System.Drawing.Point(14, 10);
|
||||||
|
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
|
this.localeLbl.Name = "localeLbl";
|
||||||
|
this.localeLbl.Size = new System.Drawing.Size(61, 15);
|
||||||
|
this.localeLbl.TabIndex = 0;
|
||||||
|
this.localeLbl.Text = "Locale: {0}";
|
||||||
|
//
|
||||||
|
// usernameLbl
|
||||||
|
//
|
||||||
|
this.usernameLbl.AutoSize = true;
|
||||||
|
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
|
||||||
|
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
|
this.usernameLbl.Name = "usernameLbl";
|
||||||
|
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
|
||||||
|
this.usernameLbl.TabIndex = 1;
|
||||||
|
this.usernameLbl.Text = "Username: {0}";
|
||||||
|
//
|
||||||
|
// externalLoginLink
|
||||||
|
//
|
||||||
|
this.externalLoginLink.AutoSize = true;
|
||||||
|
this.externalLoginLink.Location = new System.Drawing.Point(14, 93);
|
||||||
|
this.externalLoginLink.Name = "externalLoginLink";
|
||||||
|
this.externalLoginLink.Size = new System.Drawing.Size(107, 15);
|
||||||
|
this.externalLoginLink.TabIndex = 4;
|
||||||
|
this.externalLoginLink.TabStop = true;
|
||||||
|
this.externalLoginLink.Text = "Or click here to log";
|
||||||
|
this.externalLoginLink.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.externalLoginLink_LinkClicked);
|
||||||
|
//
|
||||||
|
// externalLoginLbl2
|
||||||
|
//
|
||||||
|
this.externalLoginLbl2.AutoSize = true;
|
||||||
|
this.externalLoginLbl2.Location = new System.Drawing.Point(14, 108);
|
||||||
|
this.externalLoginLbl2.Name = "externalLoginLbl2";
|
||||||
|
this.externalLoginLbl2.Size = new System.Drawing.Size(352, 45);
|
||||||
|
this.externalLoginLbl2.TabIndex = 6;
|
||||||
|
this.externalLoginLbl2.Text = "This more advanced login is recommended if you\'re experiencing\r\nerrors logging in" +
|
||||||
|
" the conventional way above or if you\'re not\r\ncomfortable typing your password h" +
|
||||||
|
"ere.";
|
||||||
|
//
|
||||||
|
// externalLoginLbl1
|
||||||
|
//
|
||||||
|
this.externalLoginLbl1.AutoSize = true;
|
||||||
|
this.externalLoginLbl1.Location = new System.Drawing.Point(83, 93);
|
||||||
|
this.externalLoginLbl1.Name = "externalLoginLbl1";
|
||||||
|
this.externalLoginLbl1.Size = new System.Drawing.Size(158, 15);
|
||||||
|
this.externalLoginLbl1.TabIndex = 5;
|
||||||
|
this.externalLoginLbl1.Text = "to log in using your browser.";
|
||||||
|
//
|
||||||
|
// LoginChoiceEagerDialog
|
||||||
|
//
|
||||||
|
this.AcceptButton = this.submitBtn;
|
||||||
|
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
|
this.ClientSize = new System.Drawing.Size(394, 216);
|
||||||
|
this.Controls.Add(this.externalLoginLbl2);
|
||||||
|
this.Controls.Add(this.externalLoginLbl1);
|
||||||
|
this.Controls.Add(this.externalLoginLink);
|
||||||
|
this.Controls.Add(this.usernameLbl);
|
||||||
|
this.Controls.Add(this.localeLbl);
|
||||||
|
this.Controls.Add(this.submitBtn);
|
||||||
|
this.Controls.Add(this.passwordLbl);
|
||||||
|
this.Controls.Add(this.passwordTb);
|
||||||
|
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||||
|
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
|
this.MaximizeBox = false;
|
||||||
|
this.MinimizeBox = false;
|
||||||
|
this.Name = "LoginChoiceEagerDialog";
|
||||||
|
this.ShowIcon = false;
|
||||||
|
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
|
this.Text = "Audible Login";
|
||||||
|
this.ResumeLayout(false);
|
||||||
|
this.PerformLayout();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private System.Windows.Forms.Label passwordLbl;
|
||||||
|
private System.Windows.Forms.TextBox passwordTb;
|
||||||
|
private System.Windows.Forms.Button submitBtn;
|
||||||
|
private System.Windows.Forms.Label localeLbl;
|
||||||
|
private System.Windows.Forms.Label usernameLbl;
|
||||||
|
private System.Windows.Forms.LinkLabel externalLoginLink;
|
||||||
|
private System.Windows.Forms.Label externalLoginLbl2;
|
||||||
|
private System.Windows.Forms.Label externalLoginLbl1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Dinah.Core;
|
||||||
|
using InternalUtilities;
|
||||||
|
|
||||||
|
namespace LibationWinForms.Dialogs.Login
|
||||||
|
{
|
||||||
|
public partial class LoginChoiceEagerDialog : Form
|
||||||
|
{
|
||||||
|
private string accountId { get; }
|
||||||
|
|
||||||
|
public AudibleApi.LoginMethod LoginMethod { get; private set; }
|
||||||
|
|
||||||
|
public string Email { get; private set; }
|
||||||
|
public string Password { get; private set; }
|
||||||
|
|
||||||
|
public LoginChoiceEagerDialog(Account account)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
accountId = account.AccountId;
|
||||||
|
|
||||||
|
// do not allow user to change login id here. if they do then jsonpath will fail
|
||||||
|
this.localeLbl.Text = string.Format(this.localeLbl.Text, account.Locale.Name);
|
||||||
|
this.usernameLbl.Text = string.Format(this.usernameLbl.Text, accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void externalLoginLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||||
|
{
|
||||||
|
LoginMethod = AudibleApi.LoginMethod.External;
|
||||||
|
DialogResult = DialogResult.OK;
|
||||||
|
this.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submitBtn_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Email = accountId;
|
||||||
|
Password = this.passwordTb.Text;
|
||||||
|
|
||||||
|
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Email?.ToMask(), passwordLength = Password.Length });
|
||||||
|
|
||||||
|
LoginMethod = AudibleApi.LoginMethod.Api;
|
||||||
|
DialogResult = DialogResult.OK;
|
||||||
|
// Close() not needed for AcceptButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
namespace LibationWinForms.Dialogs.Login
|
||||||
|
{
|
||||||
|
partial class LoginExternalDialog
|
||||||
|
{
|
||||||
|
/// <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()
|
||||||
|
{
|
||||||
|
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(LoginExternalDialog));
|
||||||
|
this.submitBtn = new System.Windows.Forms.Button();
|
||||||
|
this.localeLbl = new System.Windows.Forms.Label();
|
||||||
|
this.usernameLbl = new System.Windows.Forms.Label();
|
||||||
|
this.loginUrlLbl = new System.Windows.Forms.Label();
|
||||||
|
this.loginUrlTb = new System.Windows.Forms.TextBox();
|
||||||
|
this.copyBtn = new System.Windows.Forms.Button();
|
||||||
|
this.launchBrowserBtn = new System.Windows.Forms.Button();
|
||||||
|
this.instructionsLbl = new System.Windows.Forms.Label();
|
||||||
|
this.responseUrlTb = new System.Windows.Forms.TextBox();
|
||||||
|
this.SuspendLayout();
|
||||||
|
//
|
||||||
|
// submitBtn
|
||||||
|
//
|
||||||
|
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.submitBtn.Location = new System.Drawing.Point(665, 400);
|
||||||
|
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
|
this.submitBtn.Name = "submitBtn";
|
||||||
|
this.submitBtn.Size = new System.Drawing.Size(88, 27);
|
||||||
|
this.submitBtn.TabIndex = 8;
|
||||||
|
this.submitBtn.Text = "Submit";
|
||||||
|
this.submitBtn.UseVisualStyleBackColor = true;
|
||||||
|
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
|
||||||
|
//
|
||||||
|
// localeLbl
|
||||||
|
//
|
||||||
|
this.localeLbl.AutoSize = true;
|
||||||
|
this.localeLbl.Location = new System.Drawing.Point(14, 10);
|
||||||
|
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
|
this.localeLbl.Name = "localeLbl";
|
||||||
|
this.localeLbl.Size = new System.Drawing.Size(61, 15);
|
||||||
|
this.localeLbl.TabIndex = 0;
|
||||||
|
this.localeLbl.Text = "Locale: {0}";
|
||||||
|
//
|
||||||
|
// usernameLbl
|
||||||
|
//
|
||||||
|
this.usernameLbl.AutoSize = true;
|
||||||
|
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
|
||||||
|
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
|
this.usernameLbl.Name = "usernameLbl";
|
||||||
|
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
|
||||||
|
this.usernameLbl.TabIndex = 1;
|
||||||
|
this.usernameLbl.Text = "Username: {0}";
|
||||||
|
//
|
||||||
|
// loginUrlLbl
|
||||||
|
//
|
||||||
|
this.loginUrlLbl.AutoSize = true;
|
||||||
|
this.loginUrlLbl.Location = new System.Drawing.Point(14, 61);
|
||||||
|
this.loginUrlLbl.Name = "loginUrlLbl";
|
||||||
|
this.loginUrlLbl.Size = new System.Drawing.Size(180, 15);
|
||||||
|
this.loginUrlLbl.TabIndex = 2;
|
||||||
|
this.loginUrlLbl.Text = "Paste this URL into your browser:";
|
||||||
|
//
|
||||||
|
// loginUrlTb
|
||||||
|
//
|
||||||
|
this.loginUrlTb.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.loginUrlTb.Location = new System.Drawing.Point(14, 79);
|
||||||
|
this.loginUrlTb.Multiline = true;
|
||||||
|
this.loginUrlTb.Name = "loginUrlTb";
|
||||||
|
this.loginUrlTb.ReadOnly = true;
|
||||||
|
this.loginUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||||
|
this.loginUrlTb.Size = new System.Drawing.Size(739, 92);
|
||||||
|
this.loginUrlTb.TabIndex = 3;
|
||||||
|
//
|
||||||
|
// copyBtn
|
||||||
|
//
|
||||||
|
this.copyBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||||
|
this.copyBtn.Location = new System.Drawing.Point(14, 177);
|
||||||
|
this.copyBtn.Name = "copyBtn";
|
||||||
|
this.copyBtn.Size = new System.Drawing.Size(165, 23);
|
||||||
|
this.copyBtn.TabIndex = 4;
|
||||||
|
this.copyBtn.Text = "Copy URL to clipboard";
|
||||||
|
this.copyBtn.UseVisualStyleBackColor = true;
|
||||||
|
this.copyBtn.Click += new System.EventHandler(this.copyBtn_Click);
|
||||||
|
//
|
||||||
|
// launchBrowserBtn
|
||||||
|
//
|
||||||
|
this.launchBrowserBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.launchBrowserBtn.Location = new System.Drawing.Point(588, 177);
|
||||||
|
this.launchBrowserBtn.Name = "launchBrowserBtn";
|
||||||
|
this.launchBrowserBtn.Size = new System.Drawing.Size(165, 23);
|
||||||
|
this.launchBrowserBtn.TabIndex = 5;
|
||||||
|
this.launchBrowserBtn.Text = "Launch in browser";
|
||||||
|
this.launchBrowserBtn.UseVisualStyleBackColor = true;
|
||||||
|
this.launchBrowserBtn.Click += new System.EventHandler(this.launchBrowserBtn_Click);
|
||||||
|
//
|
||||||
|
// instructionsLbl
|
||||||
|
//
|
||||||
|
this.instructionsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||||
|
this.instructionsLbl.AutoSize = true;
|
||||||
|
this.instructionsLbl.Location = new System.Drawing.Point(14, 203);
|
||||||
|
this.instructionsLbl.Name = "instructionsLbl";
|
||||||
|
this.instructionsLbl.Size = new System.Drawing.Size(436, 90);
|
||||||
|
this.instructionsLbl.TabIndex = 6;
|
||||||
|
this.instructionsLbl.Text = resources.GetString("instructionsLbl.Text");
|
||||||
|
//
|
||||||
|
// responseUrlTb
|
||||||
|
//
|
||||||
|
this.responseUrlTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||||
|
| System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.responseUrlTb.Location = new System.Drawing.Point(14, 296);
|
||||||
|
this.responseUrlTb.Multiline = true;
|
||||||
|
this.responseUrlTb.Name = "responseUrlTb";
|
||||||
|
this.responseUrlTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||||
|
this.responseUrlTb.Size = new System.Drawing.Size(739, 98);
|
||||||
|
this.responseUrlTb.TabIndex = 7;
|
||||||
|
//
|
||||||
|
// LoginExternalDialog
|
||||||
|
//
|
||||||
|
this.AcceptButton = this.submitBtn;
|
||||||
|
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
|
this.ClientSize = new System.Drawing.Size(766, 440);
|
||||||
|
this.Controls.Add(this.responseUrlTb);
|
||||||
|
this.Controls.Add(this.instructionsLbl);
|
||||||
|
this.Controls.Add(this.launchBrowserBtn);
|
||||||
|
this.Controls.Add(this.copyBtn);
|
||||||
|
this.Controls.Add(this.loginUrlTb);
|
||||||
|
this.Controls.Add(this.loginUrlLbl);
|
||||||
|
this.Controls.Add(this.usernameLbl);
|
||||||
|
this.Controls.Add(this.localeLbl);
|
||||||
|
this.Controls.Add(this.submitBtn);
|
||||||
|
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||||
|
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
|
this.MaximizeBox = false;
|
||||||
|
this.MinimizeBox = false;
|
||||||
|
this.Name = "LoginExternalDialog";
|
||||||
|
this.ShowIcon = false;
|
||||||
|
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
|
this.Text = "Audible External Login";
|
||||||
|
this.ResumeLayout(false);
|
||||||
|
this.PerformLayout();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
private System.Windows.Forms.Button submitBtn;
|
||||||
|
private System.Windows.Forms.Label localeLbl;
|
||||||
|
private System.Windows.Forms.Label usernameLbl;
|
||||||
|
private System.Windows.Forms.Label loginUrlLbl;
|
||||||
|
private System.Windows.Forms.TextBox loginUrlTb;
|
||||||
|
private System.Windows.Forms.Button copyBtn;
|
||||||
|
private System.Windows.Forms.Button launchBrowserBtn;
|
||||||
|
private System.Windows.Forms.Label instructionsLbl;
|
||||||
|
private System.Windows.Forms.TextBox responseUrlTb;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Dinah.Core;
|
||||||
|
using InternalUtilities;
|
||||||
|
|
||||||
|
namespace LibationWinForms.Dialogs.Login
|
||||||
|
{
|
||||||
|
public partial class LoginExternalDialog : Form
|
||||||
|
{
|
||||||
|
public string ResponseUrl { get; private set; }
|
||||||
|
|
||||||
|
public LoginExternalDialog(Account account, string loginUrl)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
// do not allow user to change login id here. if they do then jsonpath will fail
|
||||||
|
this.localeLbl.Text = string.Format(this.localeLbl.Text, account.Locale.Name);
|
||||||
|
this.usernameLbl.Text = string.Format(this.usernameLbl.Text, account.AccountId);
|
||||||
|
|
||||||
|
this.loginUrlTb.Text = loginUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyBtn_Click(object sender, EventArgs e) => Clipboard.SetText(this.loginUrlTb.Text);
|
||||||
|
|
||||||
|
private void launchBrowserBtn_Click(object sender, EventArgs e) => Go.To.Url(this.loginUrlTb.Text);
|
||||||
|
|
||||||
|
private void submitBtn_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
ResponseUrl = this.responseUrlTb.Text?.Trim();
|
||||||
|
|
||||||
|
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { ResponseUrl });
|
||||||
|
if (!Uri.TryCreate(ResponseUrl, UriKind.Absolute, out var result))
|
||||||
|
{
|
||||||
|
MessageBox.Show("Invalid response URL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogResult = DialogResult.OK;
|
||||||
|
// Close() not needed for AcceptButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<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>
|
||||||
|
<data name="instructionsLbl.Text" xml:space="preserve">
|
||||||
|
<value>Login with your Amazon/Audible credentials.
|
||||||
|
After login is complete, your browser will show you an error page similar to:
|
||||||
|
Looking for Something?
|
||||||
|
We're sorry. The Web address you entered is not a functioning page on our site
|
||||||
|
Don't worry -- this is ACTUALLY A SUCCESSFUL LOGIN.
|
||||||
|
Copy the current url from your browser's address bar and paste it here:</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LibationWinForms.Dialogs.Login
|
||||||
|
{
|
||||||
|
public abstract class WinformLoginBase
|
||||||
|
{
|
||||||
|
/// <returns>True if ShowDialog's DialogResult == OK</returns>
|
||||||
|
protected static bool ShowDialog(System.Windows.Forms.Form dialog)
|
||||||
|
{
|
||||||
|
var result = dialog.ShowDialog();
|
||||||
|
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
|
||||||
|
return result == System.Windows.Forms.DialogResult.OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-19
@@ -1,16 +1,15 @@
|
|||||||
using AudibleApi;
|
using System;
|
||||||
|
using AudibleApi;
|
||||||
using InternalUtilities;
|
using InternalUtilities;
|
||||||
using LibationWinForms.Dialogs.Login;
|
using LibationWinForms.Dialogs.Login;
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace LibationWinForms.Login
|
namespace LibationWinForms.Login
|
||||||
{
|
{
|
||||||
public class WinformResponder : ILoginCallback
|
public class WinformLoginCallback : WinformLoginBase, ILoginCallback
|
||||||
{
|
{
|
||||||
private Account _account { get; }
|
private Account _account { get; }
|
||||||
|
|
||||||
public WinformResponder(Account account)
|
public WinformLoginCallback(Account account)
|
||||||
{
|
{
|
||||||
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||||
}
|
}
|
||||||
@@ -18,7 +17,7 @@ namespace LibationWinForms.Login
|
|||||||
public string Get2faCode()
|
public string Get2faCode()
|
||||||
{
|
{
|
||||||
using var dialog = new _2faCodeDialog();
|
using var dialog = new _2faCodeDialog();
|
||||||
if (showDialog(dialog))
|
if (ShowDialog(dialog))
|
||||||
return dialog.Code;
|
return dialog.Code;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -26,7 +25,7 @@ namespace LibationWinForms.Login
|
|||||||
public string GetCaptchaAnswer(byte[] captchaImage)
|
public string GetCaptchaAnswer(byte[] captchaImage)
|
||||||
{
|
{
|
||||||
using var dialog = new CaptchaDialog(captchaImage);
|
using var dialog = new CaptchaDialog(captchaImage);
|
||||||
if (showDialog(dialog))
|
if (ShowDialog(dialog))
|
||||||
return dialog.Answer;
|
return dialog.Answer;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -34,15 +33,15 @@ namespace LibationWinForms.Login
|
|||||||
public (string name, string value) GetMfaChoice(MfaConfig mfaConfig)
|
public (string name, string value) GetMfaChoice(MfaConfig mfaConfig)
|
||||||
{
|
{
|
||||||
using var dialog = new MfaDialog(mfaConfig);
|
using var dialog = new MfaDialog(mfaConfig);
|
||||||
if (showDialog(dialog))
|
if (ShowDialog(dialog))
|
||||||
return (dialog.SelectedName, dialog.SelectedValue);
|
return (dialog.SelectedName, dialog.SelectedValue);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public (string email, string password) GetLogin()
|
public (string email, string password) GetLogin()
|
||||||
{
|
{
|
||||||
using var dialog = new AudibleLoginDialog(_account);
|
using var dialog = new LoginCallbackDialog(_account);
|
||||||
if (showDialog(dialog))
|
if (ShowDialog(dialog))
|
||||||
return (dialog.Email, dialog.Password);
|
return (dialog.Email, dialog.Password);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
@@ -50,15 +49,7 @@ namespace LibationWinForms.Login
|
|||||||
public void ShowApprovalNeeded()
|
public void ShowApprovalNeeded()
|
||||||
{
|
{
|
||||||
using var dialog = new ApprovalNeededDialog();
|
using var dialog = new ApprovalNeededDialog();
|
||||||
showDialog(dialog);
|
ShowDialog(dialog);
|
||||||
}
|
|
||||||
|
|
||||||
/// <returns>True if ShowDialog's DialogResult == OK</returns>
|
|
||||||
private static bool showDialog(System.Windows.Forms.Form dialog)
|
|
||||||
{
|
|
||||||
var result = dialog.ShowDialog();
|
|
||||||
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
|
|
||||||
return result == System.Windows.Forms.DialogResult.OK;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using AudibleApi;
|
||||||
|
using InternalUtilities;
|
||||||
|
using LibationWinForms.Dialogs.Login;
|
||||||
|
|
||||||
|
namespace LibationWinForms.Login
|
||||||
|
{
|
||||||
|
public class WinformLoginChoiceEager : WinformLoginBase, ILoginChoiceEager
|
||||||
|
{
|
||||||
|
public ILoginCallback LoginCallback { get; private set; }
|
||||||
|
|
||||||
|
private Account _account { get; }
|
||||||
|
|
||||||
|
public WinformLoginChoiceEager(Account account)
|
||||||
|
{
|
||||||
|
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||||
|
LoginCallback = new WinformLoginCallback(_account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChoiceOut Start(ChoiceIn choiceIn)
|
||||||
|
{
|
||||||
|
using var dialog = new LoginChoiceEagerDialog(_account);
|
||||||
|
|
||||||
|
if (!ShowDialog(dialog))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
switch (dialog.LoginMethod)
|
||||||
|
{
|
||||||
|
case LoginMethod.Api:
|
||||||
|
return ChoiceOut.WithApi(dialog.Email, dialog.Password);
|
||||||
|
case LoginMethod.External:
|
||||||
|
{
|
||||||
|
using var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
|
||||||
|
return ShowDialog(externalDialog)
|
||||||
|
? ChoiceOut.External(externalDialog.ResponseUrl)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Exception($"Unknown {nameof(LoginMethod)} value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,10 +33,37 @@ namespace LibationWinForms.Dialogs
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void githubLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
private void githubLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||||
=> Go.To.Url("https://github.com/rmcrackan/Libation/issues");
|
{
|
||||||
|
var url = "https://github.com/rmcrackan/Libation/issues";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Go.To.Url(url);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void logsLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
private void logsLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||||
=> Go.To.Folder(FileManager.Configuration.Instance.LibationFiles);
|
{
|
||||||
|
string dir = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dir = FileManager.Configuration.Instance.LibationFiles;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Go.To.Folder(dir);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void okBtn_Click(object sender, EventArgs e)
|
private void okBtn_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-2
@@ -38,7 +38,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
this.authorsDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.authorsDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.miscDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
this.miscDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||||
this.purchaseDateGridViewTextBoxColumn = 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.btnRemoveBooks = new System.Windows.Forms.Button();
|
||||||
this.label1 = new System.Windows.Forms.Label();
|
this.label1 = new System.Windows.Forms.Label();
|
||||||
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).BeginInit();
|
||||||
@@ -176,7 +176,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private System.Windows.Forms.DataGridView _dataGridView;
|
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.Button btnRemoveBooks;
|
||||||
private System.Windows.Forms.Label label1;
|
private System.Windows.Forms.Label label1;
|
||||||
private System.Windows.Forms.DataGridViewCheckBoxColumn removeDataGridViewCheckBoxColumn;
|
private System.Windows.Forms.DataGridViewCheckBoxColumn removeDataGridViewCheckBoxColumn;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
return;
|
return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var removedBooks = await LibraryCommands.FindInactiveBooks((account) => new WinformResponder(account), _libraryBooks, _accounts);
|
var removedBooks = await LibraryCommands.FindInactiveBooks((account) => ApiExtended.CreateAsync(account, new WinformLoginChoiceEager(account)), _libraryBooks, _accounts);
|
||||||
|
|
||||||
var removable = _removableGridEntries.Where(rge => removedBooks.Any(rb => rb.Book.AudibleProductId == rge.AudibleProductId)).ToList();
|
var removable = _removableGridEntries.Where(rge => removedBooks.Any(rb => rb.Book.AudibleProductId == rge.AudibleProductId)).ToList();
|
||||||
|
|
||||||
@@ -91,16 +91,22 @@ namespace LibationWinForms.Dialogs
|
|||||||
{
|
{
|
||||||
var selectedBooks = SelectedEntries.ToList();
|
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 thisThese = selectedBooks.Count > 1 ? "these" : "this";
|
||||||
string bookBooks = selectedBooks.Count > 1 ? "books" : "book";
|
string bookBooks = selectedBooks.Count > 1 ? "books" : "book";
|
||||||
|
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
this,
|
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?",
|
"Remove books from Libation?",
|
||||||
MessageBoxButtons.YesNo,
|
MessageBoxButtons.YesNo,
|
||||||
MessageBoxIcon.Question,
|
MessageBoxIcon.Question,
|
||||||
|
|||||||
+160
-47
@@ -33,16 +33,26 @@
|
|||||||
this.saveBtn = new System.Windows.Forms.Button();
|
this.saveBtn = new System.Windows.Forms.Button();
|
||||||
this.cancelBtn = new System.Windows.Forms.Button();
|
this.cancelBtn = new System.Windows.Forms.Button();
|
||||||
this.advancedSettingsGb = new System.Windows.Forms.GroupBox();
|
this.advancedSettingsGb = new System.Windows.Forms.GroupBox();
|
||||||
|
this.importEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||||
|
this.downloadEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||||
|
this.badBookGb = new System.Windows.Forms.GroupBox();
|
||||||
|
this.badBookIgnoreRb = new System.Windows.Forms.RadioButton();
|
||||||
|
this.badBookRetryRb = new System.Windows.Forms.RadioButton();
|
||||||
|
this.badBookAbortRb = new System.Windows.Forms.RadioButton();
|
||||||
|
this.badBookAskRb = new System.Windows.Forms.RadioButton();
|
||||||
|
this.decryptAndConvertGb = new System.Windows.Forms.GroupBox();
|
||||||
|
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||||
this.convertLossyRb = new System.Windows.Forms.RadioButton();
|
this.convertLossyRb = new System.Windows.Forms.RadioButton();
|
||||||
this.convertLosslessRb = new System.Windows.Forms.RadioButton();
|
this.convertLosslessRb = new System.Windows.Forms.RadioButton();
|
||||||
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
|
this.inProgressSelectControl = new LibationWinForms.Dialogs.DirectorySelectControl();
|
||||||
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
|
||||||
this.logsBtn = new System.Windows.Forms.Button();
|
this.logsBtn = new System.Windows.Forms.Button();
|
||||||
this.booksSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
|
this.booksSelectControl = new LibationWinForms.Dialogs.DirectoryOrCustomSelectControl();
|
||||||
this.booksGb = new System.Windows.Forms.GroupBox();
|
this.booksGb = new System.Windows.Forms.GroupBox();
|
||||||
this.loggingLevelLbl = new System.Windows.Forms.Label();
|
this.loggingLevelLbl = new System.Windows.Forms.Label();
|
||||||
this.loggingLevelCb = new System.Windows.Forms.ComboBox();
|
this.loggingLevelCb = new System.Windows.Forms.ComboBox();
|
||||||
this.advancedSettingsGb.SuspendLayout();
|
this.advancedSettingsGb.SuspendLayout();
|
||||||
|
this.badBookGb.SuspendLayout();
|
||||||
|
this.decryptAndConvertGb.SuspendLayout();
|
||||||
this.booksGb.SuspendLayout();
|
this.booksGb.SuspendLayout();
|
||||||
this.SuspendLayout();
|
this.SuspendLayout();
|
||||||
//
|
//
|
||||||
@@ -53,27 +63,27 @@
|
|||||||
this.booksLocationDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
this.booksLocationDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
this.booksLocationDescLbl.Name = "booksLocationDescLbl";
|
this.booksLocationDescLbl.Name = "booksLocationDescLbl";
|
||||||
this.booksLocationDescLbl.Size = new System.Drawing.Size(69, 15);
|
this.booksLocationDescLbl.Size = new System.Drawing.Size(69, 15);
|
||||||
this.booksLocationDescLbl.TabIndex = 2;
|
this.booksLocationDescLbl.TabIndex = 1;
|
||||||
this.booksLocationDescLbl.Text = "[book desc]";
|
this.booksLocationDescLbl.Text = "[book desc]";
|
||||||
//
|
//
|
||||||
// inProgressDescLbl
|
// inProgressDescLbl
|
||||||
//
|
//
|
||||||
this.inProgressDescLbl.AutoSize = true;
|
this.inProgressDescLbl.AutoSize = true;
|
||||||
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 127);
|
this.inProgressDescLbl.Location = new System.Drawing.Point(8, 199);
|
||||||
this.inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
this.inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
this.inProgressDescLbl.Name = "inProgressDescLbl";
|
this.inProgressDescLbl.Name = "inProgressDescLbl";
|
||||||
this.inProgressDescLbl.Size = new System.Drawing.Size(43, 45);
|
this.inProgressDescLbl.Size = new System.Drawing.Size(43, 45);
|
||||||
this.inProgressDescLbl.TabIndex = 1;
|
this.inProgressDescLbl.TabIndex = 18;
|
||||||
this.inProgressDescLbl.Text = "[desc]\r\n[line 2]\r\n[line 3]";
|
this.inProgressDescLbl.Text = "[desc]\r\n[line 2]\r\n[line 3]";
|
||||||
//
|
//
|
||||||
// saveBtn
|
// saveBtn
|
||||||
//
|
//
|
||||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
this.saveBtn.Location = new System.Drawing.Point(714, 419);
|
this.saveBtn.Location = new System.Drawing.Point(714, 496);
|
||||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.saveBtn.Name = "saveBtn";
|
this.saveBtn.Name = "saveBtn";
|
||||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||||
this.saveBtn.TabIndex = 4;
|
this.saveBtn.TabIndex = 98;
|
||||||
this.saveBtn.Text = "Save";
|
this.saveBtn.Text = "Save";
|
||||||
this.saveBtn.UseVisualStyleBackColor = true;
|
this.saveBtn.UseVisualStyleBackColor = true;
|
||||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||||
@@ -82,83 +92,174 @@
|
|||||||
//
|
//
|
||||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||||
this.cancelBtn.Location = new System.Drawing.Point(832, 419);
|
this.cancelBtn.Location = new System.Drawing.Point(832, 496);
|
||||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.cancelBtn.Name = "cancelBtn";
|
this.cancelBtn.Name = "cancelBtn";
|
||||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||||
this.cancelBtn.TabIndex = 5;
|
this.cancelBtn.TabIndex = 99;
|
||||||
this.cancelBtn.Text = "Cancel";
|
this.cancelBtn.Text = "Cancel";
|
||||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||||
//
|
//
|
||||||
// advancedSettingsGb
|
// advancedSettingsGb
|
||||||
//
|
//
|
||||||
this.advancedSettingsGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
this.advancedSettingsGb.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)));
|
| System.Windows.Forms.AnchorStyles.Right)));
|
||||||
this.advancedSettingsGb.Controls.Add(this.convertLossyRb);
|
this.advancedSettingsGb.Controls.Add(this.importEpisodesCb);
|
||||||
this.advancedSettingsGb.Controls.Add(this.convertLosslessRb);
|
this.advancedSettingsGb.Controls.Add(this.downloadEpisodesCb);
|
||||||
|
this.advancedSettingsGb.Controls.Add(this.badBookGb);
|
||||||
|
this.advancedSettingsGb.Controls.Add(this.decryptAndConvertGb);
|
||||||
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
|
this.advancedSettingsGb.Controls.Add(this.inProgressSelectControl);
|
||||||
this.advancedSettingsGb.Controls.Add(this.allowLibationFixupCbox);
|
|
||||||
this.advancedSettingsGb.Controls.Add(this.inProgressDescLbl);
|
this.advancedSettingsGb.Controls.Add(this.inProgressDescLbl);
|
||||||
this.advancedSettingsGb.Location = new System.Drawing.Point(12, 176);
|
this.advancedSettingsGb.Location = new System.Drawing.Point(12, 176);
|
||||||
this.advancedSettingsGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
this.advancedSettingsGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.advancedSettingsGb.Name = "advancedSettingsGb";
|
this.advancedSettingsGb.Name = "advancedSettingsGb";
|
||||||
this.advancedSettingsGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
this.advancedSettingsGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 232);
|
this.advancedSettingsGb.Size = new System.Drawing.Size(908, 309);
|
||||||
this.advancedSettingsGb.TabIndex = 5;
|
this.advancedSettingsGb.TabIndex = 6;
|
||||||
this.advancedSettingsGb.TabStop = false;
|
this.advancedSettingsGb.TabStop = false;
|
||||||
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
|
this.advancedSettingsGb.Text = "Advanced settings for control freaks";
|
||||||
//
|
//
|
||||||
// convertLossyRb
|
// importEpisodesCb
|
||||||
//
|
//
|
||||||
this.convertLossyRb.AutoSize = true;
|
this.importEpisodesCb.AutoSize = true;
|
||||||
this.convertLossyRb.Location = new System.Drawing.Point(7, 88);
|
this.importEpisodesCb.Location = new System.Drawing.Point(7, 22);
|
||||||
this.convertLossyRb.Name = "convertLossyRb";
|
this.importEpisodesCb.Name = "importEpisodesCb";
|
||||||
this.convertLossyRb.Size = new System.Drawing.Size(242, 19);
|
this.importEpisodesCb.Size = new System.Drawing.Size(146, 19);
|
||||||
this.convertLossyRb.TabIndex = 0;
|
this.importEpisodesCb.TabIndex = 7;
|
||||||
this.convertLossyRb.Text = "Download my books as .MP3 files (Lossy)";
|
this.importEpisodesCb.Text = "[import episodes desc]";
|
||||||
this.convertLossyRb.UseVisualStyleBackColor = true;
|
this.importEpisodesCb.UseVisualStyleBackColor = true;
|
||||||
//
|
//
|
||||||
// convertLosslessRb
|
// downloadEpisodesCb
|
||||||
//
|
//
|
||||||
this.convertLosslessRb.AutoSize = true;
|
this.downloadEpisodesCb.AutoSize = true;
|
||||||
this.convertLosslessRb.Checked = true;
|
this.downloadEpisodesCb.Location = new System.Drawing.Point(7, 47);
|
||||||
this.convertLosslessRb.Location = new System.Drawing.Point(7, 63);
|
this.downloadEpisodesCb.Name = "downloadEpisodesCb";
|
||||||
this.convertLosslessRb.Name = "convertLosslessRb";
|
this.downloadEpisodesCb.Size = new System.Drawing.Size(163, 19);
|
||||||
this.convertLosslessRb.Size = new System.Drawing.Size(327, 19);
|
this.downloadEpisodesCb.TabIndex = 8;
|
||||||
this.convertLosslessRb.TabIndex = 0;
|
this.downloadEpisodesCb.Text = "[download episodes desc]";
|
||||||
this.convertLosslessRb.TabStop = true;
|
this.downloadEpisodesCb.UseVisualStyleBackColor = true;
|
||||||
this.convertLosslessRb.Text = "Download my books as .M4B files (Lossless Mp4a format)";
|
|
||||||
this.convertLosslessRb.UseVisualStyleBackColor = true;
|
|
||||||
//
|
//
|
||||||
// inProgressSelectControl
|
// badBookGb
|
||||||
//
|
//
|
||||||
this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
this.badBookGb.Controls.Add(this.badBookIgnoreRb);
|
||||||
| System.Windows.Forms.AnchorStyles.Right)));
|
this.badBookGb.Controls.Add(this.badBookRetryRb);
|
||||||
this.inProgressSelectControl.Location = new System.Drawing.Point(10, 175);
|
this.badBookGb.Controls.Add(this.badBookAbortRb);
|
||||||
this.inProgressSelectControl.Name = "inProgressSelectControl";
|
this.badBookGb.Controls.Add(this.badBookAskRb);
|
||||||
this.inProgressSelectControl.Size = new System.Drawing.Size(552, 52);
|
this.badBookGb.Location = new System.Drawing.Point(372, 72);
|
||||||
this.inProgressSelectControl.TabIndex = 2;
|
this.badBookGb.Name = "badBookGb";
|
||||||
|
this.badBookGb.Size = new System.Drawing.Size(529, 124);
|
||||||
|
this.badBookGb.TabIndex = 13;
|
||||||
|
this.badBookGb.TabStop = false;
|
||||||
|
this.badBookGb.Text = "[bad book desc]";
|
||||||
|
//
|
||||||
|
// badBookIgnoreRb
|
||||||
|
//
|
||||||
|
this.badBookIgnoreRb.AutoSize = true;
|
||||||
|
this.badBookIgnoreRb.Location = new System.Drawing.Point(6, 97);
|
||||||
|
this.badBookIgnoreRb.Name = "badBookIgnoreRb";
|
||||||
|
this.badBookIgnoreRb.Size = new System.Drawing.Size(94, 19);
|
||||||
|
this.badBookIgnoreRb.TabIndex = 17;
|
||||||
|
this.badBookIgnoreRb.TabStop = true;
|
||||||
|
this.badBookIgnoreRb.Text = "[ignore desc]";
|
||||||
|
this.badBookIgnoreRb.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// badBookRetryRb
|
||||||
|
//
|
||||||
|
this.badBookRetryRb.AutoSize = true;
|
||||||
|
this.badBookRetryRb.Location = new System.Drawing.Point(6, 72);
|
||||||
|
this.badBookRetryRb.Name = "badBookRetryRb";
|
||||||
|
this.badBookRetryRb.Size = new System.Drawing.Size(84, 19);
|
||||||
|
this.badBookRetryRb.TabIndex = 16;
|
||||||
|
this.badBookRetryRb.TabStop = true;
|
||||||
|
this.badBookRetryRb.Text = "[retry desc]";
|
||||||
|
this.badBookRetryRb.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// badBookAbortRb
|
||||||
|
//
|
||||||
|
this.badBookAbortRb.AutoSize = true;
|
||||||
|
this.badBookAbortRb.Location = new System.Drawing.Point(6, 47);
|
||||||
|
this.badBookAbortRb.Name = "badBookAbortRb";
|
||||||
|
this.badBookAbortRb.Size = new System.Drawing.Size(88, 19);
|
||||||
|
this.badBookAbortRb.TabIndex = 15;
|
||||||
|
this.badBookAbortRb.TabStop = true;
|
||||||
|
this.badBookAbortRb.Text = "[abort desc]";
|
||||||
|
this.badBookAbortRb.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// badBookAskRb
|
||||||
|
//
|
||||||
|
this.badBookAskRb.AutoSize = true;
|
||||||
|
this.badBookAskRb.Location = new System.Drawing.Point(6, 22);
|
||||||
|
this.badBookAskRb.Name = "badBookAskRb";
|
||||||
|
this.badBookAskRb.Size = new System.Drawing.Size(77, 19);
|
||||||
|
this.badBookAskRb.TabIndex = 14;
|
||||||
|
this.badBookAskRb.TabStop = true;
|
||||||
|
this.badBookAskRb.Text = "[ask desc]";
|
||||||
|
this.badBookAskRb.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// decryptAndConvertGb
|
||||||
|
//
|
||||||
|
this.decryptAndConvertGb.Controls.Add(this.allowLibationFixupCbox);
|
||||||
|
this.decryptAndConvertGb.Controls.Add(this.convertLossyRb);
|
||||||
|
this.decryptAndConvertGb.Controls.Add(this.convertLosslessRb);
|
||||||
|
this.decryptAndConvertGb.Location = new System.Drawing.Point(7, 72);
|
||||||
|
this.decryptAndConvertGb.Name = "decryptAndConvertGb";
|
||||||
|
this.decryptAndConvertGb.Size = new System.Drawing.Size(359, 124);
|
||||||
|
this.decryptAndConvertGb.TabIndex = 9;
|
||||||
|
this.decryptAndConvertGb.TabStop = false;
|
||||||
|
this.decryptAndConvertGb.Text = "Decrypt and convert";
|
||||||
//
|
//
|
||||||
// allowLibationFixupCbox
|
// allowLibationFixupCbox
|
||||||
//
|
//
|
||||||
this.allowLibationFixupCbox.AutoSize = true;
|
this.allowLibationFixupCbox.AutoSize = true;
|
||||||
this.allowLibationFixupCbox.Checked = true;
|
this.allowLibationFixupCbox.Checked = true;
|
||||||
this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
|
this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||||
this.allowLibationFixupCbox.Location = new System.Drawing.Point(7, 22);
|
this.allowLibationFixupCbox.Location = new System.Drawing.Point(6, 22);
|
||||||
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
|
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
|
||||||
this.allowLibationFixupCbox.Size = new System.Drawing.Size(262, 19);
|
this.allowLibationFixupCbox.Size = new System.Drawing.Size(262, 19);
|
||||||
this.allowLibationFixupCbox.TabIndex = 0;
|
this.allowLibationFixupCbox.TabIndex = 10;
|
||||||
this.allowLibationFixupCbox.Text = "Allow Libation to fix up audiobook metadata";
|
this.allowLibationFixupCbox.Text = "Allow Libation to fix up audiobook metadata";
|
||||||
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
||||||
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
this.allowLibationFixupCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
||||||
//
|
//
|
||||||
|
// convertLossyRb
|
||||||
|
//
|
||||||
|
this.convertLossyRb.AutoSize = true;
|
||||||
|
this.convertLossyRb.Location = new System.Drawing.Point(6, 81);
|
||||||
|
this.convertLossyRb.Name = "convertLossyRb";
|
||||||
|
this.convertLossyRb.Size = new System.Drawing.Size(329, 19);
|
||||||
|
this.convertLossyRb.TabIndex = 12;
|
||||||
|
this.convertLossyRb.Text = "Download my books as .MP3 files (transcode if necessary)";
|
||||||
|
this.convertLossyRb.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// convertLosslessRb
|
||||||
|
//
|
||||||
|
this.convertLosslessRb.AutoSize = true;
|
||||||
|
this.convertLosslessRb.Checked = true;
|
||||||
|
this.convertLosslessRb.Location = new System.Drawing.Point(6, 56);
|
||||||
|
this.convertLosslessRb.Name = "convertLosslessRb";
|
||||||
|
this.convertLosslessRb.Size = new System.Drawing.Size(335, 19);
|
||||||
|
this.convertLosslessRb.TabIndex = 11;
|
||||||
|
this.convertLosslessRb.TabStop = true;
|
||||||
|
this.convertLosslessRb.Text = "Download my books in the original audio format (Lossless)";
|
||||||
|
this.convertLosslessRb.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// inProgressSelectControl
|
||||||
|
//
|
||||||
|
this.inProgressSelectControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||||
|
| System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.inProgressSelectControl.Location = new System.Drawing.Point(7, 247);
|
||||||
|
this.inProgressSelectControl.Name = "inProgressSelectControl";
|
||||||
|
this.inProgressSelectControl.Size = new System.Drawing.Size(552, 52);
|
||||||
|
this.inProgressSelectControl.TabIndex = 19;
|
||||||
|
//
|
||||||
// logsBtn
|
// logsBtn
|
||||||
//
|
//
|
||||||
this.logsBtn.Location = new System.Drawing.Point(262, 147);
|
this.logsBtn.Location = new System.Drawing.Point(262, 147);
|
||||||
this.logsBtn.Name = "logsBtn";
|
this.logsBtn.Name = "logsBtn";
|
||||||
this.logsBtn.Size = new System.Drawing.Size(132, 23);
|
this.logsBtn.Size = new System.Drawing.Size(132, 23);
|
||||||
this.logsBtn.TabIndex = 4;
|
this.logsBtn.TabIndex = 5;
|
||||||
this.logsBtn.Text = "Open log folder";
|
this.logsBtn.Text = "Open log folder";
|
||||||
this.logsBtn.UseVisualStyleBackColor = true;
|
this.logsBtn.UseVisualStyleBackColor = true;
|
||||||
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
|
this.logsBtn.Click += new System.EventHandler(this.logsBtn_Click);
|
||||||
@@ -170,7 +271,7 @@
|
|||||||
this.booksSelectControl.Location = new System.Drawing.Point(7, 37);
|
this.booksSelectControl.Location = new System.Drawing.Point(7, 37);
|
||||||
this.booksSelectControl.Name = "booksSelectControl";
|
this.booksSelectControl.Name = "booksSelectControl";
|
||||||
this.booksSelectControl.Size = new System.Drawing.Size(895, 87);
|
this.booksSelectControl.Size = new System.Drawing.Size(895, 87);
|
||||||
this.booksSelectControl.TabIndex = 1;
|
this.booksSelectControl.TabIndex = 2;
|
||||||
//
|
//
|
||||||
// booksGb
|
// booksGb
|
||||||
//
|
//
|
||||||
@@ -181,7 +282,7 @@
|
|||||||
this.booksGb.Location = new System.Drawing.Point(12, 12);
|
this.booksGb.Location = new System.Drawing.Point(12, 12);
|
||||||
this.booksGb.Name = "booksGb";
|
this.booksGb.Name = "booksGb";
|
||||||
this.booksGb.Size = new System.Drawing.Size(908, 129);
|
this.booksGb.Size = new System.Drawing.Size(908, 129);
|
||||||
this.booksGb.TabIndex = 1;
|
this.booksGb.TabIndex = 0;
|
||||||
this.booksGb.TabStop = false;
|
this.booksGb.TabStop = false;
|
||||||
this.booksGb.Text = "Books location";
|
this.booksGb.Text = "Books location";
|
||||||
//
|
//
|
||||||
@@ -191,7 +292,7 @@
|
|||||||
this.loggingLevelLbl.Location = new System.Drawing.Point(12, 150);
|
this.loggingLevelLbl.Location = new System.Drawing.Point(12, 150);
|
||||||
this.loggingLevelLbl.Name = "loggingLevelLbl";
|
this.loggingLevelLbl.Name = "loggingLevelLbl";
|
||||||
this.loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
|
this.loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
|
||||||
this.loggingLevelLbl.TabIndex = 2;
|
this.loggingLevelLbl.TabIndex = 3;
|
||||||
this.loggingLevelLbl.Text = "Logging level";
|
this.loggingLevelLbl.Text = "Logging level";
|
||||||
//
|
//
|
||||||
// loggingLevelCb
|
// loggingLevelCb
|
||||||
@@ -201,7 +302,7 @@
|
|||||||
this.loggingLevelCb.Location = new System.Drawing.Point(96, 147);
|
this.loggingLevelCb.Location = new System.Drawing.Point(96, 147);
|
||||||
this.loggingLevelCb.Name = "loggingLevelCb";
|
this.loggingLevelCb.Name = "loggingLevelCb";
|
||||||
this.loggingLevelCb.Size = new System.Drawing.Size(129, 23);
|
this.loggingLevelCb.Size = new System.Drawing.Size(129, 23);
|
||||||
this.loggingLevelCb.TabIndex = 3;
|
this.loggingLevelCb.TabIndex = 4;
|
||||||
//
|
//
|
||||||
// SettingsDialog
|
// SettingsDialog
|
||||||
//
|
//
|
||||||
@@ -209,7 +310,7 @@
|
|||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
this.CancelButton = this.cancelBtn;
|
this.CancelButton = this.cancelBtn;
|
||||||
this.ClientSize = new System.Drawing.Size(933, 462);
|
this.ClientSize = new System.Drawing.Size(933, 539);
|
||||||
this.Controls.Add(this.logsBtn);
|
this.Controls.Add(this.logsBtn);
|
||||||
this.Controls.Add(this.loggingLevelCb);
|
this.Controls.Add(this.loggingLevelCb);
|
||||||
this.Controls.Add(this.loggingLevelLbl);
|
this.Controls.Add(this.loggingLevelLbl);
|
||||||
@@ -225,6 +326,10 @@
|
|||||||
this.Load += new System.EventHandler(this.SettingsDialog_Load);
|
this.Load += new System.EventHandler(this.SettingsDialog_Load);
|
||||||
this.advancedSettingsGb.ResumeLayout(false);
|
this.advancedSettingsGb.ResumeLayout(false);
|
||||||
this.advancedSettingsGb.PerformLayout();
|
this.advancedSettingsGb.PerformLayout();
|
||||||
|
this.badBookGb.ResumeLayout(false);
|
||||||
|
this.badBookGb.PerformLayout();
|
||||||
|
this.decryptAndConvertGb.ResumeLayout(false);
|
||||||
|
this.decryptAndConvertGb.PerformLayout();
|
||||||
this.booksGb.ResumeLayout(false);
|
this.booksGb.ResumeLayout(false);
|
||||||
this.booksGb.PerformLayout();
|
this.booksGb.PerformLayout();
|
||||||
this.ResumeLayout(false);
|
this.ResumeLayout(false);
|
||||||
@@ -247,5 +352,13 @@
|
|||||||
private System.Windows.Forms.Button logsBtn;
|
private System.Windows.Forms.Button logsBtn;
|
||||||
private System.Windows.Forms.Label loggingLevelLbl;
|
private System.Windows.Forms.Label loggingLevelLbl;
|
||||||
private System.Windows.Forms.ComboBox loggingLevelCb;
|
private System.Windows.Forms.ComboBox loggingLevelCb;
|
||||||
|
private System.Windows.Forms.GroupBox decryptAndConvertGb;
|
||||||
|
private System.Windows.Forms.GroupBox badBookGb;
|
||||||
|
private System.Windows.Forms.RadioButton badBookRetryRb;
|
||||||
|
private System.Windows.Forms.RadioButton badBookAbortRb;
|
||||||
|
private System.Windows.Forms.RadioButton badBookAskRb;
|
||||||
|
private System.Windows.Forms.RadioButton badBookIgnoreRb;
|
||||||
|
private System.Windows.Forms.CheckBox downloadEpisodesCb;
|
||||||
|
private System.Windows.Forms.CheckBox importEpisodesCb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,8 @@ namespace LibationWinForms.Dialogs
|
|||||||
loggingLevelCb.SelectedItem = config.LogLevel;
|
loggingLevelCb.SelectedItem = config.LogLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
||||||
|
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
||||||
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
||||||
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
|
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
|
||||||
|
|
||||||
@@ -41,6 +43,8 @@ namespace LibationWinForms.Dialogs
|
|||||||
"Books");
|
"Books");
|
||||||
booksSelectControl.SelectDirectory(config.Books);
|
booksSelectControl.SelectDirectory(config.Books);
|
||||||
|
|
||||||
|
importEpisodesCb.Checked = config.ImportEpisodes;
|
||||||
|
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
||||||
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
|
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
|
||||||
convertLosslessRb.Checked = !config.DecryptToLossy;
|
convertLosslessRb.Checked = !config.DecryptToLossy;
|
||||||
convertLossyRb.Checked = config.DecryptToLossy;
|
convertLossyRb.Checked = config.DecryptToLossy;
|
||||||
@@ -56,6 +60,21 @@ namespace LibationWinForms.Dialogs
|
|||||||
Configuration.KnownDirectories.LibationFiles
|
Configuration.KnownDirectories.LibationFiles
|
||||||
}, Configuration.KnownDirectories.WinTemp);
|
}, Configuration.KnownDirectories.WinTemp);
|
||||||
inProgressSelectControl.SelectDirectory(config.InProgress);
|
inProgressSelectControl.SelectDirectory(config.InProgress);
|
||||||
|
|
||||||
|
badBookGb.Text = desc(nameof(config.BadBook));
|
||||||
|
badBookAskRb.Text = Configuration.BadBookAction.Ask.GetDescription();
|
||||||
|
badBookAbortRb.Text = Configuration.BadBookAction.Abort.GetDescription();
|
||||||
|
badBookRetryRb.Text = Configuration.BadBookAction.Retry.GetDescription();
|
||||||
|
badBookIgnoreRb.Text = Configuration.BadBookAction.Ignore.GetDescription();
|
||||||
|
var rb = config.BadBook switch
|
||||||
|
{
|
||||||
|
Configuration.BadBookAction.Ask => this.badBookAskRb,
|
||||||
|
Configuration.BadBookAction.Abort => this.badBookAbortRb,
|
||||||
|
Configuration.BadBookAction.Retry => this.badBookRetryRb,
|
||||||
|
Configuration.BadBookAction.Ignore => this.badBookIgnoreRb,
|
||||||
|
_ => this.badBookAskRb
|
||||||
|
};
|
||||||
|
rb.Checked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
|
private void allowLibationFixupCbox_CheckedChanged(object sender, EventArgs e)
|
||||||
@@ -106,11 +125,20 @@ namespace LibationWinForms.Dialogs
|
|||||||
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.ImportEpisodes = importEpisodesCb.Checked;
|
||||||
|
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
||||||
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
||||||
config.DecryptToLossy = convertLossyRb.Checked;
|
config.DecryptToLossy = convertLossyRb.Checked;
|
||||||
|
|
||||||
config.InProgress = inProgressSelectControl.SelectedDirectory;
|
config.InProgress = inProgressSelectControl.SelectedDirectory;
|
||||||
|
|
||||||
|
config.BadBook
|
||||||
|
= badBookAskRb.Checked ? Configuration.BadBookAction.Ask
|
||||||
|
: badBookAbortRb.Checked ? Configuration.BadBookAction.Abort
|
||||||
|
: badBookRetryRb.Checked ? Configuration.BadBookAction.Retry
|
||||||
|
: badBookIgnoreRb.Checked ? Configuration.BadBookAction.Ignore
|
||||||
|
: Configuration.BadBookAction.Ask;
|
||||||
|
|
||||||
this.DialogResult = DialogResult.OK;
|
this.DialogResult = DialogResult.OK;
|
||||||
this.Close();
|
this.Close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<root>
|
||||||
<root>
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
<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:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ namespace LibationWinForms
|
|||||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||||
LibraryCommands.LibrarySizeChanged += reloadGridAndUpdateBottomNumbers;
|
LibraryCommands.LibrarySizeChanged += reloadGridAndUpdateBottomNumbers;
|
||||||
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
||||||
|
// used by async migrations to update ui when complete
|
||||||
|
DataLayer.UserDefinedItem.Batch_ItemChanged += reloadGridAndUpdateBottomNumbers;
|
||||||
|
|
||||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||||
|
|||||||
@@ -27,17 +27,16 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public event EventHandler Committed;
|
||||||
|
|
||||||
private Book Book => LibraryBook.Book;
|
private Book Book => LibraryBook.Book;
|
||||||
private Image _cover;
|
private Image _cover;
|
||||||
private Action Refilter { get; }
|
|
||||||
|
|
||||||
public GridEntry(LibraryBook libraryBook, Action refilterOnChanged = null)
|
public GridEntry(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
LibraryBook = libraryBook;
|
LibraryBook = libraryBook;
|
||||||
Refilter = refilterOnChanged;
|
|
||||||
_memberValues = CreateMemberValueDictionary();
|
_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) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
|
(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;
|
Book.UserDefinedItem.BookStatus = displayStatus;
|
||||||
|
|
||||||
Refilter?.Invoke();
|
Committed?.Invoke(this, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -253,7 +252,7 @@ namespace LibationWinForms
|
|||||||
private static string GetDescriptionDisplay(Book book)
|
private static string GetDescriptionDisplay(Book book)
|
||||||
{
|
{
|
||||||
var doc = new HtmlAgilityPack.HtmlDocument();
|
var doc = new HtmlAgilityPack.HtmlDocument();
|
||||||
doc.LoadHtml(book.Description);
|
doc.LoadHtml(book?.Description ?? "");
|
||||||
var noHtml = doc.DocumentNode.InnerText;
|
var noHtml = doc.DocumentNode.InnerText;
|
||||||
return
|
return
|
||||||
noHtml.Length < 63 ?
|
noHtml.Length < 63 ?
|
||||||
|
|||||||
@@ -18,8 +18,18 @@
|
|||||||
<!-- Version is now in AppScaffolding.csproj -->
|
<!-- Version is now in AppScaffolding.csproj -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<!--
|
||||||
|
HACK FOR COMPILER BUG 2021-09-14. Hopefully will be fixed in future versions
|
||||||
|
- Not using SatelliteResourceLanguages will load all language packs: works
|
||||||
|
- Specifying 'en' semicolon 1 more should load 1 language pack: works
|
||||||
|
- Specifying only 'en' should load no language packs: broken, still loads all
|
||||||
|
-->
|
||||||
|
<SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="1.1.0.2" />
|
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="1.1.1.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using LibationWinForms.Dialogs;
|
using System;
|
||||||
using System;
|
using LibationWinForms.Dialogs;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace LibationWinForms
|
namespace LibationWinForms
|
||||||
{
|
{
|
||||||
@@ -15,7 +14,11 @@ namespace LibationWinForms
|
|||||||
/// <returns>One of the System.Windows.Forms.DialogResult values.</returns>
|
/// <returns>One of the System.Windows.Forms.DialogResult values.</returns>
|
||||||
public static System.Windows.Forms.DialogResult Show(string text, string caption, Exception exception)
|
public static System.Windows.Forms.DialogResult Show(string text, string caption, Exception exception)
|
||||||
{
|
{
|
||||||
Serilog.Log.Logger.Error(exception, "Alert admin error: {@DebugText}", new { text, caption });
|
try
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error(exception, "Alert admin error: {@DebugText}", new { text, caption });
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
using var form = new MessageBoxAlertAdminDialog(text, caption, exception);
|
using var form = new MessageBoxAlertAdminDialog(text, caption, exception);
|
||||||
return form.ShowDialog();
|
return form.ShowDialog();
|
||||||
|
|||||||
+2
-2
@@ -30,7 +30,7 @@
|
|||||||
{
|
{
|
||||||
this.components = new System.ComponentModel.Container();
|
this.components = new System.ComponentModel.Container();
|
||||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
|
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.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
|
||||||
this.dataGridViewImageButtonBoxColumn1 = new LibationWinForms.LiberateDataGridViewImageButtonColumn();
|
this.dataGridViewImageButtonBoxColumn1 = new LibationWinForms.LiberateDataGridViewImageButtonColumn();
|
||||||
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
|
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private System.Windows.Forms.BindingSource gridEntryBindingSource;
|
private LibationWinForms.SyncBindingSource gridEntryBindingSource;
|
||||||
private System.Windows.Forms.DataGridView gridEntryDataGridView;
|
private System.Windows.Forms.DataGridView gridEntryDataGridView;
|
||||||
private LiberateDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn1;
|
private LiberateDataGridViewImageButtonColumn dataGridViewImageButtonBoxColumn1;
|
||||||
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
|
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using DataLayer;
|
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Dinah.Core.DataBinding;
|
using Dinah.Core.DataBinding;
|
||||||
|
using Dinah.Core.Threading;
|
||||||
using Dinah.Core.Windows.Forms;
|
using Dinah.Core.Windows.Forms;
|
||||||
using LibationWinForms.Dialogs;
|
using LibationWinForms.Dialogs;
|
||||||
|
|
||||||
@@ -130,7 +130,12 @@ namespace LibationWinForms
|
|||||||
}
|
}
|
||||||
|
|
||||||
var orderedGridEntries = lib
|
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
|
// default load order
|
||||||
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
|
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
|
||||||
//// more advanced example: sort by author, then series, then title
|
//// more advanced example: sort by author, then series, then title
|
||||||
@@ -166,8 +171,11 @@ namespace LibationWinForms
|
|||||||
var bindingContext = BindingContext[_dataGridView.DataSource];
|
var bindingContext = BindingContext[_dataGridView.DataSource];
|
||||||
bindingContext.SuspendBinding();
|
bindingContext.SuspendBinding();
|
||||||
{
|
{
|
||||||
for (var r = _dataGridView.RowCount - 1; r >= 0; r--)
|
this.UIThreadSync(() =>
|
||||||
_dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId);
|
{
|
||||||
|
for (var r = _dataGridView.RowCount - 1; r >= 0; r--)
|
||||||
|
_dataGridView.Rows[r].Visible = productIds.Contains(getGridEntry(r).AudibleProductId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Causes repainting of the DataGridView
|
//Causes repainting of the DataGridView
|
||||||
|
|||||||
+56
-37
@@ -24,37 +24,55 @@ namespace LibationWinForms
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
static void Main()
|
static void Main()
|
||||||
{
|
{
|
||||||
//// Uncomment to see Console. Must be called before anything writes to Console.
|
try
|
||||||
//// Only use while debugging. Acts erratically in the wild
|
{
|
||||||
//AllocConsole();
|
//// Uncomment to see Console. Must be called before anything writes to Console.
|
||||||
|
//// Only use while debugging. Acts erratically in the wild
|
||||||
|
//AllocConsole();
|
||||||
|
|
||||||
Application.SetHighDpiMode(HighDpiMode.SystemAware);
|
Application.SetHighDpiMode(HighDpiMode.SystemAware);
|
||||||
Application.EnableVisualStyles();
|
Application.EnableVisualStyles();
|
||||||
Application.SetCompatibleTextRenderingDefault(false);
|
Application.SetCompatibleTextRenderingDefault(false);
|
||||||
|
|
||||||
//***********************************************//
|
//***********************************************//
|
||||||
// //
|
// //
|
||||||
// do not use Configuration before this line //
|
// do not use Configuration before this line //
|
||||||
// //
|
// //
|
||||||
//***********************************************//
|
//***********************************************//
|
||||||
// Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
|
// Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
|
||||||
var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations();
|
var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations();
|
||||||
|
|
||||||
RunInstaller(config);
|
// do this as soon as possible (post-config)
|
||||||
|
RunInstaller(config);
|
||||||
|
|
||||||
// most migrations go in here
|
// most migrations go in here
|
||||||
AppScaffolding.LibationScaffolding.RunPostConfigMigrations();
|
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
|
||||||
|
|
||||||
// migrations which require Forms or are long-running
|
// migrations which require Forms or are long-running
|
||||||
RunWindowsOnlyMigrations(config);
|
RunWindowsOnlyMigrations(config);
|
||||||
|
|
||||||
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
MessageBoxVerboseLoggingWarning.ShowIfTrue();
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
checkForUpdate();
|
checkForUpdate();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding();
|
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var title = "Fatal error, pre-logging";
|
||||||
|
var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MessageBoxAlertAdmin.Show(body, title, ex);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Application.Run(new Form1());
|
Application.Run(new Form1());
|
||||||
}
|
}
|
||||||
@@ -125,8 +143,6 @@ namespace LibationWinForms
|
|||||||
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
|
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
|
||||||
config.Books ??= Path.Combine(defaultLibationFilesDir, "Books");
|
config.Books ??= Path.Combine(defaultLibationFilesDir, "Books");
|
||||||
config.InProgress ??= Configuration.WinTemp;
|
config.InProgress ??= Configuration.WinTemp;
|
||||||
config.AllowLibationFixup = true;
|
|
||||||
config.DecryptToLossy = false;
|
|
||||||
|
|
||||||
if (new SettingsDialog().ShowDialog() != DialogResult.OK)
|
if (new SettingsDialog().ShowDialog() != DialogResult.OK)
|
||||||
{
|
{
|
||||||
@@ -140,6 +156,7 @@ namespace LibationWinForms
|
|||||||
CancelInstallation();
|
CancelInstallation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>migrations which require Forms or are long-running</summary>
|
||||||
private static void RunWindowsOnlyMigrations(Configuration config)
|
private static void RunWindowsOnlyMigrations(Configuration config)
|
||||||
{
|
{
|
||||||
// only supported in winforms. don't move to app scaffolding
|
// only supported in winforms. don't move to app scaffolding
|
||||||
@@ -152,9 +169,6 @@ namespace LibationWinForms
|
|||||||
#region migrate to v5.0.0 re-register device if device info not in settings
|
#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)
|
private static void migrate_to_v5_0_0(Configuration config)
|
||||||
{
|
{
|
||||||
if (!config.Exists(nameof(config.AllowLibationFixup)))
|
|
||||||
config.AllowLibationFixup = true;
|
|
||||||
|
|
||||||
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))
|
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -183,7 +197,8 @@ namespace LibationWinForms
|
|||||||
authorize.DeregisterAsync(identity.ExistingAccessToken, identity.Cookies.ToKeyValuePair()).GetAwaiter().GetResult();
|
authorize.DeregisterAsync(identity.ExistingAccessToken, identity.Cookies.ToKeyValuePair()).GetAwaiter().GetResult();
|
||||||
identity.Invalidate();
|
identity.Invalidate();
|
||||||
|
|
||||||
var api = AudibleApiActions.GetApiAsync(new LibationWinForms.Login.WinformResponder(account), account).GetAwaiter().GetResult();
|
// re-registers device
|
||||||
|
ApiExtended.CreateAsync(account, new Login.WinformLoginChoiceEager(account)).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -240,22 +255,26 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
// assign these strings and enums/ints unconditionally. EFCore will only update if changed
|
// assign these strings and enums/ints unconditionally. EFCore will only update if changed
|
||||||
if (fileType == FileType.PDF)
|
if (fileType == FileType.PDF)
|
||||||
book.UserDefinedItem.PdfStatus = LiberatedStatus.Liberated;
|
book.UserDefinedItem.BatchMode_UpdatePdfStatus(LiberatedStatus.Liberated);
|
||||||
|
|
||||||
if (fileType == FileType.Audio)
|
if (fileType == FileType.Audio)
|
||||||
{
|
{
|
||||||
var lhack = libhackFiles.FirstOrDefault(f => f.ContainsInsensitive(asin));
|
var lhack = libhackFiles.FirstOrDefault(f => f.ContainsInsensitive(asin));
|
||||||
if (lhack is null)
|
if (lhack is null)
|
||||||
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
|
book.UserDefinedItem.BatchMode_UpdateBookStatus(LiberatedStatus.Liberated);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
|
book.UserDefinedItem.BatchMode_UpdateBookStatus(LiberatedStatus.Error);
|
||||||
libhackFilesToDelete.Add(lhack);
|
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
|
// only do this after save changes
|
||||||
foreach (var libhackFile in libhackFilesToDelete)
|
foreach (var libhackFile in libhackFilesToDelete)
|
||||||
@@ -299,14 +318,14 @@ namespace LibationWinForms
|
|||||||
if (result != DialogResult.Yes)
|
if (result != DialogResult.Yes)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
using var fileSelector = new SaveFileDialog { FileName = zipName, Filter = "Zip Files (*.zip)|*.zip|All files (*.*)|*.*" };
|
|
||||||
if (fileSelector.ShowDialog() != DialogResult.OK)
|
|
||||||
return;
|
|
||||||
var selectedPath = fileSelector.FileName;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LibationWinForms.BookLiberation.ProcessorAutomationController.DownloadFile(zipUrl, selectedPath, true);
|
using var fileSelector = new SaveFileDialog { FileName = zipName, Filter = "Zip Files (*.zip)|*.zip|All files (*.*)|*.*" };
|
||||||
|
if (fileSelector.ShowDialog() != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
var selectedPath = fileSelector.FileName;
|
||||||
|
|
||||||
|
BookLiberation.ProcessorAutomationController.DownloadFile(zipUrl, selectedPath, true);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
- [Files and folders](#files-and-folders)
|
- [Files and folders](#files-and-folders)
|
||||||
- [Linux and Mac (unofficial)](#linux-and-mac)
|
- [Linux and Mac (unofficial)](#linux-and-mac)
|
||||||
- [Settings](#settings)
|
- [Settings](#settings)
|
||||||
|
- [Command Line Interface](#command-line-interface)
|
||||||
|
|
||||||
## Audible audiobook manager
|
## Audible audiobook manager
|
||||||
|
|
||||||
@@ -241,3 +242,49 @@ Although Libation only currently officially supports Windows, [some users](https
|
|||||||
### Settings
|
### Settings
|
||||||
|
|
||||||
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
|
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
|
||||||
|
|
||||||
|
### Command Line Interface
|
||||||
|
|
||||||
|
Libationcli.exe allows limited access to Libation's functionalities as a CLI.
|
||||||
|
|
||||||
|
Warnings about relying solely on on the CLI:
|
||||||
|
* CLI will not perform any upgrades.
|
||||||
|
* It will show that there is an upgrade, but that will likely scroll by too fast to notice.
|
||||||
|
* It will not perform all post-upgrade migrations. Some migrations are only be possible by launching GUI.
|
||||||
|
|
||||||
|
```
|
||||||
|
help
|
||||||
|
libationcli --help
|
||||||
|
|
||||||
|
verb-specific help
|
||||||
|
libationcli scan --help
|
||||||
|
|
||||||
|
scan all libraries
|
||||||
|
libationcli scan
|
||||||
|
scan only libraries for specific accounts
|
||||||
|
libationcli scan nickname1 nickname2
|
||||||
|
|
||||||
|
convert all m4b files to mp3
|
||||||
|
libationcli convert
|
||||||
|
|
||||||
|
liberate all books and pdfs
|
||||||
|
libationcli liberate
|
||||||
|
liberate pdfs only
|
||||||
|
libationcli liberate --pdf
|
||||||
|
libationcli liberate -p
|
||||||
|
|
||||||
|
export library to file
|
||||||
|
libationcli export --path "C:\foo\bar\my.json" --json
|
||||||
|
libationcli export -p "C:\foo\bar\my.json" -j
|
||||||
|
```
|
||||||
|
|
||||||
|
Currently logs are written to Console and to file. This means they'll be printed in the CLI. To disable, find this in Settings.json and delete the 3 lines after `"WriteTo": [`
|
||||||
|
|
||||||
|
```
|
||||||
|
"Serilog": {
|
||||||
|
"MinimumLevel": "Information",
|
||||||
|
"WriteTo": [
|
||||||
|
{
|
||||||
|
"Name": "Console"
|
||||||
|
},
|
||||||
|
```
|
||||||
Reference in new issue
Block a user