Compare commits

..
42 Commits
Author SHA1 Message Date
Robert McRackan 19369a21ef * New feature: setting to not import episodes ( #125 ) 2021-09-25 14:02:27 -04:00
Robert McRackan 611fb4d6d8 increm ver 2021-09-24 20:11:52 -04:00
Robert McRackan c77ec54035 bug fix: DownloadEpisodes logic needs parans 2021-09-24 19:59:57 -04:00
Robert McRackan c9c28c7826 oops. again 2021-09-24 16:44:49 -04:00
Robert McRackan 30e2caaff5 New feature: setting to not download episodes 2021-09-24 16:44:28 -04:00
Robert McRackan fd56017af5 dedicated lock objects for safety 2021-09-24 11:46:37 -04:00
Robert McRackan d2eaf26117 Removing books
* message box is too big when removing huge amounts of books. This scenario is typical when removing podcasts
* removing books should remove user defined entries. eg: tags and is-liberated
2021-09-24 10:00:52 -04:00
Robert McRackan 7c38e18435 Add support for unencrypted mp3 audiobooks 2021-09-24 08:26:23 -04:00
Robert McRackan bfb1dbc69a Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-09-24 08:25:16 -04:00
Robert McRackan d2ff19e309 null description causes errors 2021-09-24 08:24:16 -04:00
rmcrackan aa3a7dce06 Merge pull request #123 from Mbucari/master
Add support for unencrypted mp3 audiobooks.
2021-09-24 08:23:14 -04:00
Mbucari 71075838eb Moved event logging to LibationBaseForm 2021-09-23 19:29:25 -06:00
Michael Bucari-Tovo 803a0b7ccf Comment typo. 2021-09-23 18:14:29 -06:00
Michael Bucari-Tovo d9f3fa825c Renaming and comments. 2021-09-23 18:13:43 -06:00
Michael Bucari-Tovo df42ba584e Better naming. 2021-09-23 18:06:59 -06:00
Michael Bucari-Tovo 9f09a62a1e Ensure chapter info is downloaded for unencrypted MP3s 2021-09-23 18:04:12 -06:00
Michael Bucari-Tovo e714179c30 Add support for unencrypted mp3 audiobooks. 2021-09-23 18:01:39 -06:00
Robert McRackan db84c9a7d9 unencrypted podcast downloads (incomplete) 2021-09-23 16:50:59 -04:00
Robert McRackan 937bd56fcc debug code to incl episodes 2021-09-23 16:20:08 -04:00
Robert McRackan f29968f379 conditional re-index 2021-09-23 14:44:11 -04:00
Robert McRackan 14e14ba9bd batch book status updates
bug fixes, esp. threading
2021-09-23 14:33:04 -04:00
Robert McRackan 613c97524a User get getting "bindingsource cannot be its own data source" error. Can't repro locally. Adding SyncBindingSource for datagrid views to attempt to solve it 2021-09-23 10:41:33 -04:00
Robert McRackan 4fd16f04e0 More robust error handling, especially before logging can be initialized 2021-09-23 09:19:35 -04:00
Robert McRackan 61385f0f0b more explicit book series order 2021-09-22 11:51:06 -04:00
Robert McRackan 7647882344 update audible api dependency 2021-09-22 09:48:28 -04:00
Robert McRackan 96ffa619ec Bug fix: podcast episodes with no title cause library import failure 2021-09-21 15:06:01 -04:00
Robert McRackan de1147ac1b External login completed 2021-09-20 15:31:07 -04:00
Robert McRackan 926a7a1148 * include new external login interfaces in ApiExtended
* new ext method to simplify api access in file liberator
2021-09-17 16:09:49 -04:00
Robert McRackan 51020ef99e * upgrade api. lay the groundwork for supporting external logins. When complete, this will be v6.0
* replace AudibleApiActions with ApiExtended
2021-09-17 15:30:06 -04:00
Robert McRackan 5a1303c33a update dependency 2021-09-17 14:02:26 -04:00
Robert McRackan a0e2d78b9b * upgraded AAXClean addresses issues #104 , #110
* update dependencies
2021-09-16 07:53:34 -04:00
rmcrackan 6b711190c3 Merge pull request #116 from Mbucari/master
Correctly write mp3 Xing header.
2021-09-16 07:46:03 -04:00
Michael Bucari-Tovo b4a6342513 Revert accidental change. 2021-09-15 15:32:34 -06:00
Michael Bucari-Tovo 988b137d67 Updated AAXClean version. 2021-09-15 15:26:40 -06:00
Michael Bucari-Tovo dae9c9c9b6 Add read permission to file stream. 2021-09-15 15:16:08 -06:00
Robert McRackan 420b7529c6 Bug fix: issues #113 , #114
Forcing a new build order. Seems to fix the issue. Since the issue was already intermittent (race condition?) it's difficult to be sure
2021-09-14 09:05:56 -04:00
Robert McRackan 4cf999c84d null book description throws 2021-09-13 11:05:44 -04:00
Robert McRackan 8fe3896d76 build error bug fix 2021-09-13 10:35:41 -04:00
Robert McRackan adcba34560 'bad book' setting labels 2021-09-11 08:01:58 -04:00
Robert McRackan 8e09d7e617 Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-09-10 22:59:02 -04:00
Robert McRackan 197b50e3ac issue #111 -- setting for bad book abort/retry/ignore. default: ask 2021-09-10 22:58:34 -04:00
rmcrackan ac2114e270 Add CLI documentation 2021-09-10 17:20:13 -04:00
79 changed files with 2240 additions and 742 deletions

No files matched your search

+2 -2
View File
@@ -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>
+39 -147
View File
@@ -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);
}
} }
+165
View File
@@ -0,0 +1,165 @@
using Dinah.Core;
using Dinah.Core.IO;
using Dinah.Core.Net.Http;
using Dinah.Core.StepRunner;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AaxDecrypter
{
public enum OutputFormat { M4b, Mp3 }
public abstract class AudiobookDownloadBase
{
public event EventHandler<string> RetrievedTitle;
public event EventHandler<string> RetrievedAuthors;
public event EventHandler<string> RetrievedNarrators;
public event EventHandler<byte[]> RetrievedCoverArt;
public event EventHandler<DownloadProgress> DecryptProgressUpdate;
public event EventHandler<TimeSpan> DecryptTimeRemaining;
public string AppName { get; set; }
protected bool isCanceled { get; set; }
protected string outputFileName { get; }
protected string cacheDir { get; }
protected DownloadLicense downloadLicense { get; }
protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream;
protected abstract StepSequence steps { get; }
private NetworkFileStreamPersister nfsPersister;
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".tmp");
public AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
{
AppName = GetType().Name;
ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
outputFileName = outFileName;
var outDir = Path.GetDirectoryName(outputFileName);
if (!Directory.Exists(outDir))
throw new ArgumentNullException(nameof(outDir), "Directory does not exist");
if (File.Exists(outputFileName))
File.Delete(outputFileName);
if (!Directory.Exists(cacheDirectory))
throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist");
cacheDir = cacheDirectory;
downloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
}
public abstract void Cancel();
protected abstract int GetSpeedup(TimeSpan elapsed);
protected abstract bool Step2_DownloadAudiobook();
protected abstract bool Step1_GetMetadata();
public virtual void SetCoverArt(byte[] coverArt)
{
if (coverArt is null) return;
OnRetrievedCoverArt(coverArt);
}
public bool Run()
{
var (IsSuccess, Elapsed) = steps.Run();
if (!IsSuccess)
{
Console.WriteLine("WARNING-Conversion failed");
return false;
}
Serilog.Log.Logger.Information($"Speedup is {GetSpeedup(Elapsed)}x realtime.");
return true;
}
protected void OnRetrievedTitle(string title)
=> RetrievedTitle?.Invoke(this, title);
protected void OnRetrievedAuthors(string authors)
=> RetrievedAuthors?.Invoke(this, authors);
protected void OnRetrievedNarrators(string narrators)
=> RetrievedNarrators?.Invoke(this, narrators);
protected void OnRetrievedCoverArt(byte[] coverArt)
=> RetrievedCoverArt?.Invoke(this, coverArt);
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
=> DecryptTimeRemaining?.Invoke(this, timeRemaining);
protected void CloseInputFileStream()
{
nfsPersister?.NetworkFileStream?.Close();
nfsPersister?.Dispose();
}
protected bool Step3_CreateCue()
{
// not a critical step. its failure should not prevent future steps from running
try
{
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), downloadLicense.ChapterInfo));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"{nameof(Step3_CreateCue)}. FAILED");
}
return !isCanceled;
}
protected bool Step4_Cleanup()
{
FileExt.SafeDelete(jsonDownloadState);
FileExt.SafeDelete(tempFile);
return !isCanceled;
}
private NetworkFileStreamPersister OpenNetworkFileStream()
{
NetworkFileStreamPersister nfsp;
if (File.Exists(jsonDownloadState))
{
try
{
nfsp = new NetworkFileStreamPersister(jsonDownloadState);
//If More than ~1 hour has elapsed since getting the download url, it will expire.
//The new url will be to the same file.
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl));
}
catch
{
FileExt.SafeDelete(jsonDownloadState);
FileExt.SafeDelete(tempFile);
nfsp = NewNetworkFilePersister();
}
}
else
{
nfsp = NewNetworkFilePersister();
}
return nfsp;
}
private NetworkFileStreamPersister NewNetworkFilePersister()
{
var headers = new System.Net.WebHeaderCollection
{
{ "User-Agent", downloadLicense.UserAgent }
};
var networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
}
}
+3 -6
View File
@@ -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;
} }
} }
} }
-54
View File
@@ -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;
}
}
}
+5 -5
View File
@@ -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;
}
}
}
+1 -1
View File
@@ -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>
+22 -6
View File
@@ -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;
}
} }
} }
+20 -2
View File
@@ -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);
+12 -16
View File
@@ -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)
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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>
+5 -5
View File
@@ -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
-19
View File
@@ -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 -12
View File
@@ -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}";
+38 -5
View File
@@ -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");
+1 -12
View File
@@ -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
{ {
-15
View File
@@ -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");
+23 -48
View File
@@ -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")
-6
View File
@@ -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();
+44 -23
View File
@@ -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)
-66
View File
@@ -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);
}
}
}
}
+4 -7
View File
@@ -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;
}
} }
} }
+7 -5
View File
@@ -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)
+15 -17
View File
@@ -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)
{ {
+39 -11
View File
@@ -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";
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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 "";
+4 -3
View File
@@ -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()
{ {
+6 -4
View File
@@ -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;
} }
+1 -1
View File
@@ -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>
+3
View File
@@ -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
+8
View File
@@ -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>
+1 -3
View File
@@ -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}");
+1 -1
View File
@@ -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 ";
+2 -2
View File
@@ -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();
+1 -1
View File
@@ -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)
{ {
@@ -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";
@@ -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,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;
}
}
}
@@ -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
View File
@@ -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;
+10 -4
View File
@@ -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
View File
@@ -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 -2
View File
@@ -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">
+2
View File
@@ -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));
+5 -6
View File
@@ -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 ?
+11 -1
View File
@@ -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>
+7 -4
View File
@@ -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
View File
@@ -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;
+12 -4
View File
@@ -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
View File
@@ -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)
{ {
+28
View File
@@ -0,0 +1,28 @@
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
// https://stackoverflow.com/a/32886415
namespace LibationWinForms
{
public class SyncBindingSource : BindingSource
{
private SynchronizationContext syncContext { get; }
public SyncBindingSource() : base()
=> syncContext = SynchronizationContext.Current;
public SyncBindingSource(IContainer container) : base(container)
=> syncContext = SynchronizationContext.Current;
public SyncBindingSource(object dataSource, string dataMember) : base(dataSource, dataMember)
=> syncContext = SynchronizationContext.Current;
protected override void OnListChanged(ListChangedEventArgs e)
{
if (syncContext != null)
syncContext.Send(_ => base.OnListChanged(e), null);
else
base.OnListChanged(e);
}
}
}
+47
View File
@@ -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"
},
```