Merge pull request #32 from Mbucari/master

Changes we discussed, plus a few more.
This commit is contained in:
rmcrackan 2021-07-01 08:45:37 -04:00 committed by GitHub
commit 2146ebff29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 413 additions and 276 deletions

View File

@ -5,9 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="taglib-sharp"> <PackageReference Include="TagLibSharp" Version="2.2.0" />
<HintPath>lib\taglib-sharp.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -42,9 +40,6 @@
<None Update="DecryptLib\swscale-5.dll"> <None Update="DecryptLib\swscale-5.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="DecryptLib\taglib-sharp.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -10,51 +10,54 @@ using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
public interface ISimpleAaxToM4bConverter public interface ISimpleAaxcToM4bConverter
{ {
event EventHandler<AaxcTagLibFile> RetrievedTags;
event EventHandler<byte[]> RetrievedCoverArt;
event EventHandler<TimeSpan> DecryptTimeRemaining;
event EventHandler<int> DecryptProgressUpdate; event EventHandler<int> DecryptProgressUpdate;
bool Run(); bool Run();
string AppName { get; set; } string AppName { get; set; }
string outDir { get; } string outDir { get; }
string outputFileName { get; } string outputFileName { get; }
ChapterInfo chapters { get; } ChapterInfo chapters { get; }
AaxcTagLibFile aaxcTagLib { get; }
byte[] coverArt { get; }
void SetCoverArt(byte[] coverArt);
void SetOutputFilename(string outFileName); void SetOutputFilename(string outFileName);
string Title { get; }
string Author { get; }
string Narrator { get; }
byte[] CoverArt { get; }
} }
public interface IAdvancedAaxcToM4bConverter : ISimpleAaxToM4bConverter public interface IAdvancedAaxcToM4bConverter : ISimpleAaxcToM4bConverter
{ {
void Cancel(); void Cancel();
bool Step1_CreateDir(); bool Step1_CreateDir();
bool Step2_DownloadAndCombine(); bool Step2_GetMetadata();
bool Step3_RestoreMetadata(); bool Step3_DownloadAndCombine();
bool Step4_CreateCue(); bool Step4_RestoreMetadata();
bool Step5_CreateNfo(); bool Step5_CreateCue();
bool Step6_CreateNfo();
} }
public class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter public class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter
{ {
public event EventHandler<AaxcTagLibFile> RetrievedTags;
public event EventHandler<byte[]> RetrievedCoverArt;
public event EventHandler<int> DecryptProgressUpdate; public event EventHandler<int> DecryptProgressUpdate;
public event EventHandler<TimeSpan> DecryptTimeRemaining; public event EventHandler<TimeSpan> DecryptTimeRemaining;
public string AppName { get; set; } = nameof(AaxcDownloadConverter); public string AppName { get; set; } = nameof(AaxcDownloadConverter);
public string outDir { get; private set; } public string outDir { get; private set; }
public string outputFileName { get; private set; } public string outputFileName { get; private set; }
public ChapterInfo chapters { get; private set; } public ChapterInfo chapters { get; private set; }
public string Title => aaxcTagLib.Tag.Title.Replace(" (Unabridged)", ""); public AaxcTagLibFile aaxcTagLib { get; private set; }
public string Author => aaxcTagLib.Tag.FirstPerformer ?? "[unknown]"; public byte[] coverArt { get; private set; }
public string Narrator => aaxcTagLib.GetTag(TagLib.TagTypes.Apple).Narrator;
public byte[] CoverArt => aaxcTagLib.Tag.Pictures.Length > 0 ? aaxcTagLib.Tag.Pictures[0].Data.Data : default;
private TagLib.Mpeg4.File aaxcTagLib { get; set; }
private StepSequence steps { get; } private StepSequence steps { get; }
private DownloadLicense downloadLicense { get; set; } private DownloadLicense downloadLicense { get; set; }
private FFMpegAaxcProcesser aaxcProcesser; private FFMpegAaxcProcesser aaxcProcesser;
private bool isCanceled { get; set; }
public static async Task<AaxcDownloadConverter> CreateAsync(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters = null) public static AaxcDownloadConverter Create(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters = null)
{ {
var converter = new AaxcDownloadConverter(outDirectory, dlLic, chapters); var converter = new AaxcDownloadConverter(outDirectory, dlLic, chapters);
await converter.prelimProcessing(); converter.SetOutputFilename(Path.GetTempFileName());
return converter; return converter;
} }
@ -72,35 +75,20 @@ namespace AaxDecrypter
Name = "Convert Aax To M4b", Name = "Convert Aax To M4b",
["Step 1: Create Dir"] = Step1_CreateDir, ["Step 1: Create Dir"] = Step1_CreateDir,
["Step 2: Download and Combine Audiobook"] = Step2_DownloadAndCombine, ["Step 2: Get Aaxc Metadata"] = Step2_GetMetadata,
["Step 3: Restore Aaxc Metadata"] = Step3_RestoreMetadata, ["Step 3: Download Decrypted Audiobook"] = Step3_DownloadAndCombine,
["Step 4: Create Cue"] = Step4_CreateCue, ["Step 4: Restore Aaxc Metadata"] = Step4_RestoreMetadata,
["Step 5: Create Nfo"] = Step5_CreateNfo, ["Step 5: Create Cue"] = Step5_CreateCue,
["Step 6: Create Nfo"] = Step6_CreateNfo,
}; };
aaxcProcesser = new FFMpegAaxcProcesser(dlLic);
aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate;
downloadLicense = dlLic; downloadLicense = dlLic;
this.chapters = chapters; this.chapters = chapters;
} }
private async Task prelimProcessing()
{
//Get metadata from the file over http
var client = new System.Net.Http.HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", downloadLicense.UserAgent);
var networkFile = await NetworkFileAbstraction.CreateAsync(client, new Uri(downloadLicense.DownloadUrl));
aaxcTagLib = await Task.Run(() => TagLib.File.Create(networkFile, "audio/mp4", TagLib.ReadStyle.Average) as TagLib.Mpeg4.File);
var defaultFilename = Path.Combine(
outDir,
PathLib.ToPathSafeString(aaxcTagLib.Tag.FirstPerformer??"[unknown]"),
PathLib.ToPathSafeString(aaxcTagLib.Tag.Title.Replace(" (Unabridged)", "")) + ".m4b"
);
SetOutputFilename(defaultFilename);
}
public void SetOutputFilename(string outFileName) public void SetOutputFilename(string outFileName)
{ {
outputFileName = PathLib.ReplaceExtension(outFileName, ".m4b"); outputFileName = PathLib.ReplaceExtension(outFileName, ".m4b");
@ -110,6 +98,14 @@ namespace AaxDecrypter
File.Delete(outputFileName); File.Delete(outputFileName);
} }
public void SetCoverArt(byte[] coverArt)
{
if (coverArt is null) return;
this.coverArt = coverArt;
RetrievedCoverArt?.Invoke(this, coverArt);
}
public bool Run() public bool Run()
{ {
var (IsSuccess, Elapsed) = steps.Run(); var (IsSuccess, Elapsed) = steps.Run();
@ -131,14 +127,32 @@ namespace AaxDecrypter
ProcessRunner.WorkingDir = outDir; ProcessRunner.WorkingDir = outDir;
Directory.CreateDirectory(outDir); Directory.CreateDirectory(outDir);
return true; return !isCanceled;
} }
public bool Step2_DownloadAndCombine() public bool Step2_GetMetadata()
{ {
aaxcProcesser = new FFMpegAaxcProcesser(downloadLicense); //Get metadata from the file over http
aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate; var client = new System.Net.Http.HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", downloadLicense.UserAgent);
var networkFile = NetworkFileAbstraction.CreateAsync(client, new Uri(downloadLicense.DownloadUrl)).GetAwaiter().GetResult();
aaxcTagLib = new AaxcTagLibFile(networkFile);
if (coverArt is null && aaxcTagLib.AppleTags.Pictures.Length > 0)
{
coverArt = aaxcTagLib.AppleTags.Pictures[0].Data.Data;
}
RetrievedTags?.Invoke(this, aaxcTagLib);
RetrievedCoverArt?.Invoke(this, coverArt);
return !isCanceled;
}
public bool Step3_DownloadAndCombine()
{
DecryptProgressUpdate?.Invoke(this, int.MaxValue);
bool userSuppliedChapters = chapters != null; bool userSuppliedChapters = chapters != null;
string metadataPath = null; string metadataPath = null;
@ -165,115 +179,56 @@ namespace AaxDecrypter
DecryptProgressUpdate?.Invoke(this, 0); DecryptProgressUpdate?.Invoke(this, 0);
return aaxcProcesser.Succeeded; return aaxcProcesser.Succeeded && !isCanceled;
} }
private void AaxcProcesser_ProgressUpdate(object sender, TimeSpan e) private void AaxcProcesser_ProgressUpdate(object sender, AaxcProcessUpdate e)
{ {
double averageRate = getAverageProcessRate(e); double remainingSecsToProcess = (aaxcTagLib.Properties.Duration - e.ProcessPosition).TotalSeconds;
double remainingSecsToProcess = (aaxcTagLib.Properties.Duration - e).TotalSeconds; double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
double estTimeRemaining = remainingSecsToProcess / averageRate;
if (double.IsNormal(estTimeRemaining)) if (double.IsNormal(estTimeRemaining))
DecryptTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining)); DecryptTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
double progressPercent = 100 * e.TotalSeconds / aaxcTagLib.Properties.Duration.TotalSeconds; double progressPercent = 100 * e.ProcessPosition.TotalSeconds / aaxcTagLib.Properties.Duration.TotalSeconds;
DecryptProgressUpdate?.Invoke(this, (int)progressPercent); DecryptProgressUpdate?.Invoke(this, (int)progressPercent);
} }
/// <summary>
/// Calculates the average processing rate based on the last 2 to <see cref="MAX_NUM_AVERAGE"/> samples.
/// </summary>
/// <param name="lastProcessedPosition">Position in the audio file last processed</param>
/// <returns>The average processing rate, in book_duration_seconds / second.</returns>
private double getAverageProcessRate(TimeSpan lastProcessedPosition)
{
streamPositions.Enqueue(new StreamPosition
{
ProcessPosition = lastProcessedPosition,
EventTime = DateTime.Now,
});
if (streamPositions.Count < 2)
return double.PositiveInfinity;
//Calculate the harmonic mean of the last 2 to MAX_NUM_AVERAGE progress updates
//Units are Book_Duration_Seconds / second
var lastPos = streamPositions.Count > MAX_NUM_AVERAGE ? streamPositions.Dequeue() : null;
double harmonicDenominator = 0;
int harmonicNumerator = 0;
foreach (var pos in streamPositions)
{
if (lastPos is null)
{
lastPos = pos;
continue;
}
double dP = (pos.ProcessPosition - lastPos.ProcessPosition).TotalSeconds;
double dT = (pos.EventTime - lastPos.EventTime).TotalSeconds;
harmonicDenominator += dT / dP;
harmonicNumerator++;
lastPos = pos;
}
double harmonicMean = harmonicNumerator / harmonicDenominator;
return harmonicMean;
}
private const int MAX_NUM_AVERAGE = 15;
private class StreamPosition
{
public TimeSpan ProcessPosition { get; set; }
public DateTime EventTime { get; set; }
}
private Queue<StreamPosition> streamPositions = new Queue<StreamPosition>();
/// <summary> /// <summary>
/// Copy all aacx metadata to m4b file, including cover art. /// Copy all aacx metadata to m4b file, including cover art.
/// </summary> /// </summary>
public bool Step3_RestoreMetadata() public bool Step4_RestoreMetadata()
{ {
var outFile = new TagLib.Mpeg4.File(outputFileName, TagLib.ReadStyle.Average); var outFile = new AaxcTagLibFile(outputFileName);
outFile.CopyTagsFrom(aaxcTagLib);
var destTags = outFile.GetTag(TagLib.TagTypes.Apple) as TagLib.Mpeg4.AppleTag; if (outFile.AppleTags.Pictures.Length == 0 && coverArt is not null)
destTags.Clear();
var sourceTag = aaxcTagLib.GetTag(TagLib.TagTypes.Apple) as TagLib.Mpeg4.AppleTag;
//copy all metadata fields in the source file, even those that TagLib doesn't
//recognize, to the output file.
//NOTE: Chapters aren't stored in MPEG-4 metadata. They are encoded as a Timed
//Text Stream (MPEG-4 Part 17), so taglib doesn't read or write them.
foreach (var stag in sourceTag)
{ {
destTags.SetData(stag.BoxType, stag.Children.Cast<TagLib.Mpeg4.AppleDataBox>().ToArray()); outFile.AddPicture(coverArt);
} }
outFile.Save(); outFile.Save();
return true; return !isCanceled;
} }
public bool Step4_CreateCue() public bool Step5_CreateCue()
{ {
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), chapters)); File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), chapters));
return true; return !isCanceled;
} }
public bool Step5_CreateNfo() public bool Step6_CreateNfo()
{ {
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxcTagLib, chapters)); File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxcTagLib, chapters));
return true; return !isCanceled;
} }
public void Cancel() public void Cancel()
{ {
aaxcProcesser?.Cancel(); isCanceled = true;
aaxcProcesser.Cancel();
} }
} }
} }

View File

@ -0,0 +1,165 @@
using System.Linq;
using System.Text.RegularExpressions;
using TagLib;
using TagLib.Mpeg4;
namespace AaxDecrypter
{
public class AaxcTagLibFile : TagLib.Mpeg4.File
{
public AppleTag AppleTags => GetTag(TagTypes.Apple) as AppleTag;
private static ReadOnlyByteVector narratorType = new ReadOnlyByteVector(0xa9, (byte)'n', (byte)'r', (byte)'t');
private static ReadOnlyByteVector descriptionType = new ReadOnlyByteVector(0xa9, (byte)'d', (byte)'e', (byte)'s');
private static ReadOnlyByteVector publisherType = new ReadOnlyByteVector(0xa9, (byte)'p', (byte)'u', (byte)'b');
public string AsciiTitleSansUnabridged => TitleSansUnabridged is not null? unicodeToAscii(TitleSansUnabridged) : default;
public string AsciiFirstAuthor => FirstAuthor is not null? unicodeToAscii(FirstAuthor) : default;
public string AsciiNarrator => Narrator is not null ? unicodeToAscii(Narrator) : default;
public string AsciiComment => Comment is not null ? unicodeToAscii(Comment) : default;
public string AsciiLongDescription => LongDescription is not null ? unicodeToAscii(LongDescription) : default;
public string Comment => AppleTags.Comment;
public string[] Authors => AppleTags.Performers;
public string FirstAuthor => Authors?.Length > 0 ? Authors[0] : default;
public string TitleSansUnabridged => AppleTags.Title?.Replace(" (Unabridged)", "");
public string BookCopyright => _copyright is not null && _copyright.Length > 0 ? _copyright[0] : default;
public string RecordingCopyright => _copyright is not null && _copyright.Length > 1 ? _copyright[1] : default;
public string Narrator
{
get
{
string[] text = AppleTags.GetText(narratorType);
return text.Length == 0 ? default : text[0];
}
}
public string LongDescription
{
get
{
string[] text = AppleTags.GetText(descriptionType);
return text.Length == 0 ? default : text[0];
}
}
public string ReleaseDate
{
get
{
string[] text = AppleTags.GetText("rldt");
return text.Length == 0 ? default : text[0];
}
}
public string Publisher
{
get
{
string[] text = AppleTags.GetText(publisherType);
return text.Length == 0 ? default : text[0];
}
}
private string[] _copyright => AppleTags.Copyright?.Replace("&#169;", string.Empty)?.Replace("(P)", string.Empty)?.Split(';');
public AaxcTagLibFile(IFileAbstraction abstraction)
: base(abstraction, ReadStyle.Average)
{
}
public AaxcTagLibFile(string path)
: this(new LocalFileAbstraction(path))
{
}
/// <summary>
/// Copy all metadata fields in the source file, even those that TagLib doesn't
/// recognize, to the output file.
/// NOTE: Chapters aren't stored in MPEG-4 metadata. They are encoded as a Timed
/// Text Stream (MPEG-4 Part 17), so taglib doesn't read or write them.
/// </summary>
/// <param name="sourceFile">File from which tags will be coppied.</param>
public void CopyTagsFrom(AaxcTagLibFile sourceFile)
{
AppleTags.Clear();
foreach (var stag in sourceFile.AppleTags)
{
AppleTags.SetData(stag.BoxType, stag.Children.Cast<AppleDataBox>().ToArray());
}
}
public void AddPicture(byte[] coverArt)
{
AppleTags.SetData("covr", coverArt, 0);
}
/// <summary>
/// Attempts to convert unicode characters to an approximately equal ASCII character.
/// </summary>
private string unicodeToAscii(string unicodeStr)
{
//Accents
unicodeStr = Regex.Replace(unicodeStr, "[éèëêð]", "e");
unicodeStr = Regex.Replace(unicodeStr, "[ÉÈËÊ]", "E");
unicodeStr = Regex.Replace(unicodeStr, "[àâä]", "a");
unicodeStr = Regex.Replace(unicodeStr, "[ÀÁÂÃÄÅ]", "A");
unicodeStr = Regex.Replace(unicodeStr, "[àáâãäå]", "a");
unicodeStr = Regex.Replace(unicodeStr, "[ÙÚÛÜ]", "U");
unicodeStr = Regex.Replace(unicodeStr, "[ùúûüµ]", "u");
unicodeStr = Regex.Replace(unicodeStr, "[òóôõöø]", "o");
unicodeStr = Regex.Replace(unicodeStr, "[ÒÓÔÕÖØ]", "O");
unicodeStr = Regex.Replace(unicodeStr, "[ìíîï]", "i");
unicodeStr = Regex.Replace(unicodeStr, "[ÌÍÎÏ]", "I");
unicodeStr = Regex.Replace(unicodeStr, "[š]", "s");
unicodeStr = Regex.Replace(unicodeStr, "[Š]", "S");
unicodeStr = Regex.Replace(unicodeStr, "[ñ]", "n");
unicodeStr = Regex.Replace(unicodeStr, "[Ñ]", "N");
unicodeStr = Regex.Replace(unicodeStr, "[ç]", "c");
unicodeStr = Regex.Replace(unicodeStr, "[Ç]", "C");
unicodeStr = Regex.Replace(unicodeStr, "[ÿ]", "y");
unicodeStr = Regex.Replace(unicodeStr, "[Ÿ]", "Y");
unicodeStr = Regex.Replace(unicodeStr, "[ž]", "z");
unicodeStr = Regex.Replace(unicodeStr, "[Ž]", "Z");
unicodeStr = Regex.Replace(unicodeStr, "[Ð]", "D");
//Ligatures
unicodeStr = Regex.Replace(unicodeStr, "[œ]", "oe");
unicodeStr = Regex.Replace(unicodeStr, "[Œ]", "Oe");
unicodeStr = Regex.Replace(unicodeStr, "[ꜳ]", "aa");
unicodeStr = Regex.Replace(unicodeStr, "[Ꜳ]", "AA");
unicodeStr = Regex.Replace(unicodeStr, "[æ]", "ae");
unicodeStr = Regex.Replace(unicodeStr, "[Æ]", "AE");
unicodeStr = Regex.Replace(unicodeStr, "[ꜵ]", "ao");
unicodeStr = Regex.Replace(unicodeStr, "[Ꜵ]", "AO");
unicodeStr = Regex.Replace(unicodeStr, "[ꜷ]", "au");
unicodeStr = Regex.Replace(unicodeStr, "[Ꜷ]", "AU");
unicodeStr = Regex.Replace(unicodeStr, "[«»ꜹꜻ]", "av");
unicodeStr = Regex.Replace(unicodeStr, "[«»ꜸꜺ]", "AV");
unicodeStr = Regex.Replace(unicodeStr, "[🙰]", "et");
unicodeStr = Regex.Replace(unicodeStr, "[ff]", "ff");
unicodeStr = Regex.Replace(unicodeStr, "[ffi]", "ffi");
unicodeStr = Regex.Replace(unicodeStr, "[ffl]", "ffl");
unicodeStr = Regex.Replace(unicodeStr, "[fi]", "fi");
unicodeStr = Regex.Replace(unicodeStr, "[fl]", "fl");
unicodeStr = Regex.Replace(unicodeStr, "[ƕ]", "hv");
unicodeStr = Regex.Replace(unicodeStr, "[Ƕ]", "Hv");
unicodeStr = Regex.Replace(unicodeStr, "[℔]", "lb");
unicodeStr = Regex.Replace(unicodeStr, "[ꝏ]", "oo");
unicodeStr = Regex.Replace(unicodeStr, "[Ꝏ]", "OO");
unicodeStr = Regex.Replace(unicodeStr, "[st]", "st");
unicodeStr = Regex.Replace(unicodeStr, "[ꜩ]", "tz");
unicodeStr = Regex.Replace(unicodeStr, "[Ꜩ]", "TZ");
unicodeStr = Regex.Replace(unicodeStr, "[ᵫ]", "ue");
unicodeStr = Regex.Replace(unicodeStr, "[ꭣ]", "uo");
//Punctuation
unicodeStr = Regex.Replace(unicodeStr, "[«»\u2018\u2019\u201A\u201B\u2032\u2035]", "\'");
unicodeStr = Regex.Replace(unicodeStr, "[«»\u201C\u201D\u201E\u201F\u2033\u2036]", "\"");
unicodeStr = Regex.Replace(unicodeStr, "[\u2026]", "...");
unicodeStr = Regex.Replace(unicodeStr, "[\u1680]", "-");
//Spaces
unicodeStr = Regex.Replace(unicodeStr, "[«»\u00A0\u2000\u2002\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u200F\u205F\u3000]", " ");
unicodeStr = Regex.Replace(unicodeStr, "[«»\u2001\u2003]", " ");
unicodeStr = Regex.Replace(unicodeStr, "[«»\u180E\u200B\uFEFF]", "");
return unicodeStr;
}
}
}

View File

@ -7,13 +7,25 @@ using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
class AaxcProcessUpdate
{
public AaxcProcessUpdate(TimeSpan position, double speed)
{
ProcessPosition = position;
ProcessSpeed = speed;
EventTime = DateTime.Now;
}
public TimeSpan ProcessPosition { get; }
public double ProcessSpeed { get; }
public DateTime EventTime { get; }
}
/// <summary> /// <summary>
/// Download audible aaxc, decrypt, and remux with chapters. /// Download audible aaxc, decrypt, and remux with chapters.
/// </summary> /// </summary>
class FFMpegAaxcProcesser class FFMpegAaxcProcesser
{ {
public event EventHandler<TimeSpan> ProgressUpdate; public event EventHandler<AaxcProcessUpdate> ProgressUpdate;
public string FFMpegPath { get; } public string FFMpegPath { get; }
public DownloadLicense DownloadLicense { get; } public DownloadLicense DownloadLicense { get; }
public bool IsRunning { get; private set; } public bool IsRunning { get; private set; }
@ -24,9 +36,10 @@ namespace AaxDecrypter
private StringBuilder remuxerError = new StringBuilder(); private StringBuilder remuxerError = new StringBuilder();
private StringBuilder downloaderError = new StringBuilder(); private StringBuilder downloaderError = new StringBuilder();
private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}.*speed=\\s{0,1}([0-9]*[.]?[0-9]+)(?:e\\+([0-9]+)){0,1}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private Process downloader; private Process downloader;
private Process remuxer; private Process remuxer;
private bool isCanceled = false;
public FFMpegAaxcProcesser( DownloadLicense downloadLicense) public FFMpegAaxcProcesser( DownloadLicense downloadLicense)
{ {
@ -60,6 +73,9 @@ namespace AaxDecrypter
remuxer.Start(); remuxer.Start();
remuxer.BeginErrorReadLine(); remuxer.BeginErrorReadLine();
//Thic check needs to be placed after remuxer has started
if (isCanceled) return;
var pipedOutput = downloader.StandardOutput.BaseStream; var pipedOutput = downloader.StandardOutput.BaseStream;
var pipedInput = remuxer.StandardInput.BaseStream; var pipedInput = remuxer.StandardInput.BaseStream;
@ -94,8 +110,12 @@ namespace AaxDecrypter
} }
public void Cancel() public void Cancel()
{ {
isCanceled = true;
if (IsRunning && !remuxer.HasExited) if (IsRunning && !remuxer.HasExited)
remuxer.Kill(); remuxer.Kill();
if (IsRunning && !downloader.HasExited)
downloader.Kill();
} }
private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e) private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{ {
@ -115,6 +135,7 @@ namespace AaxDecrypter
if (processedTimeRegex.IsMatch(e.Data)) if (processedTimeRegex.IsMatch(e.Data))
{ {
//get timestamp of of last processed audio stream position //get timestamp of of last processed audio stream position
//and processing speed
var match = processedTimeRegex.Match(e.Data); var match = processedTimeRegex.Match(e.Data);
int hours = int.Parse(match.Groups[1].Value); int hours = int.Parse(match.Groups[1].Value);
@ -123,7 +144,11 @@ namespace AaxDecrypter
var position = new TimeSpan(hours, minutes, seconds); var position = new TimeSpan(hours, minutes, seconds);
ProgressUpdate?.Invoke(sender, position); double speed = double.Parse(match.Groups[4].Value);
int exp = match.Groups[5].Success ? int.Parse(match.Groups[5].Value) : 0;
speed *= Math.Pow(10, exp);
ProgressUpdate?.Invoke(this, new AaxcProcessUpdate(position, speed));
} }
if (e.Data.Contains("aac bitstream error")) if (e.Data.Contains("aac bitstream error"))

View File

@ -3,57 +3,50 @@ namespace AaxDecrypter
{ {
public static class NFO public static class NFO
{ {
public static string CreateContents(string ripper, TagLib.File aaxcTagLib, ChapterInfo chapters) public static string CreateContents(string ripper, AaxcTagLibFile aaxcTagLib, ChapterInfo chapters)
{ {
var tag = aaxcTagLib.GetTag(TagLib.TagTypes.Apple);
string narator = string.IsNullOrWhiteSpace(aaxcTagLib.Tag.FirstComposer) ? tag.Narrator : aaxcTagLib.Tag.FirstComposer;
var _hours = (int)aaxcTagLib.Properties.Duration.TotalHours; var _hours = (int)aaxcTagLib.Properties.Duration.TotalHours;
var myDuration var myDuration
= (_hours > 0 ? _hours + " hours, " : "") = (_hours > 0 ? _hours + " hours, " : string.Empty)
+ aaxcTagLib.Properties.Duration.Minutes + " minutes, " + aaxcTagLib.Properties.Duration.Minutes + " minutes, "
+ aaxcTagLib.Properties.Duration.Seconds + " seconds"; + aaxcTagLib.Properties.Duration.Seconds + " seconds";
var header var nfoString
= "General Information\r\n" = "General Information\r\n"
+ "===================\r\n" + "======================\r\n"
+ $" Title: {aaxcTagLib.Tag.Title.Replace(" (Unabridged)", "")}\r\n" + $" Title: {aaxcTagLib.AsciiTitleSansUnabridged ?? "[unknown]"}\r\n"
+ $" Author: {aaxcTagLib.Tag.FirstPerformer ?? "[unknown]"}\r\n" + $" Author: {aaxcTagLib.AsciiFirstAuthor ?? "[unknown]"}\r\n"
+ $" Read By: {aaxcTagLib.GetTag(TagLib.TagTypes.Apple).Narrator??"[unknown]"}\r\n" + $" Read By: {aaxcTagLib.AsciiNarrator ?? "[unknown]"}\r\n"
+ $" Copyright: {aaxcTagLib.Tag.Year}\r\n" + $" Release Date: {aaxcTagLib.ReleaseDate ?? "[unknown]"}\r\n"
+ $" Audiobook Copyright: {aaxcTagLib.Tag.Year}\r\n"; + $" Book Copyright: {aaxcTagLib.BookCopyright ?? "[unknown]"}\r\n"
if (!string.IsNullOrEmpty(aaxcTagLib.Tag.FirstGenre)) + $" Recording Copyright: {aaxcTagLib.RecordingCopyright ?? "[unknown]"}\r\n"
header += $" Genre: {aaxcTagLib.Tag.FirstGenre}\r\n"; + $" Genre: {aaxcTagLib.AppleTags.FirstGenre ?? "[unknown]"}\r\n"
+ $" Publisher: {aaxcTagLib.Publisher ?? "[unknown]"}\r\n"
var s
= header
+ $" Publisher: {tag.Publisher ?? ""}\r\n"
+ $" Duration: {myDuration}\r\n" + $" Duration: {myDuration}\r\n"
+ $" Chapters: {chapters.Count}\r\n" + $" Chapters: {chapters.Count}\r\n"
+ "\r\n" + "\r\n"
+ "\r\n" + "\r\n"
+ "Media Information\r\n" + "Media Information\r\n"
+ "=================\r\n" + "======================\r\n"
+ " Source Format: Audible AAX\r\n" + " Source Format: Audible AAX\r\n"
+ $" Source Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n" + $" Source Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
+ $" Source Channels: {aaxcTagLib.Properties.AudioChannels}\r\n" + $" Source Channels: {aaxcTagLib.Properties.AudioChannels}\r\n"
+ $" Source Bitrate: {aaxcTagLib.Properties.AudioBitrate} kbits\r\n" + $" Source Bitrate: {aaxcTagLib.Properties.AudioBitrate} Kbps\r\n"
+ "\r\n" + "\r\n"
+ " Lossless Encode: Yes\r\n" + " Lossless Encode: Yes\r\n"
+ " Encoded Codec: AAC / M4B\r\n" + " Encoded Codec: AAC / M4B\r\n"
+ $" Encoded Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n" + $" Encoded Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
+ $" Encoded Channels: {aaxcTagLib.Properties.AudioChannels}\r\n" + $" Encoded Channels: {aaxcTagLib.Properties.AudioChannels}\r\n"
+ $" Encoded Bitrate: {aaxcTagLib.Properties.AudioBitrate} kbits\r\n" + $" Encoded Bitrate: {aaxcTagLib.Properties.AudioBitrate} Kbps\r\n"
+ "\r\n" + "\r\n"
+ $" Ripper: {ripper}\r\n" + $" Ripper: {ripper}\r\n"
+ "\r\n" + "\r\n"
+ "\r\n" + "\r\n"
+ "Book Description\r\n" + "Book Description\r\n"
+ "================\r\n" + "================\r\n"
+ (!string.IsNullOrWhiteSpace(tag.LongDescription) ? tag.LongDescription : tag.Description); + (!string.IsNullOrWhiteSpace(aaxcTagLib.LongDescription) ? aaxcTagLib.AsciiLongDescription : aaxcTagLib.AsciiComment);
return s; return nfoString;
} }
} }
} }

View File

@ -1,16 +0,0 @@
using DataLayer;
using Dinah.Core.ErrorHandling;
using System.Threading.Tasks;
namespace FileLiberator.AaxcDownloadDecrypt
{
public class DownloadBookDummy : DownloadableBase
{
public override Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook) => Task.FromResult(new StatusHandler());
public override bool Validate(LibraryBook libraryBook)
{
return true;
}
}
}

View File

@ -3,7 +3,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using DataLayer; using DataLayer;
using Dinah.Core.ErrorHandling; using Dinah.Core.ErrorHandling;
using FileLiberator.AaxcDownloadDecrypt; using FileLiberator;
using FileManager; using FileManager;
namespace FileLiberator namespace FileLiberator
@ -22,8 +22,6 @@ namespace FileLiberator
public event EventHandler<string> StatusUpdate; public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> Completed; public event EventHandler<LibraryBook> Completed;
public DownloadBookDummy DownloadBook { get; } = new DownloadBookDummy();
public DownloadDecryptBook DecryptBook { get; } = new DownloadDecryptBook(); public DownloadDecryptBook DecryptBook { get; } = new DownloadDecryptBook();
public DownloadPdf DownloadPdf { get; } = new DownloadPdf(); public DownloadPdf DownloadPdf { get; } = new DownloadPdf();
@ -38,12 +36,6 @@ namespace FileLiberator
try try
{ {
{
var statusHandler = await DownloadBook.TryProcessAsync(libraryBook);
if (statusHandler.HasErrors)
return statusHandler;
}
{ {
var statusHandler = await DecryptBook.TryProcessAsync(libraryBook); var statusHandler = await DecryptBook.TryProcessAsync(libraryBook);
if (statusHandler.HasErrors) if (statusHandler.HasErrors)

View File

@ -11,10 +11,11 @@ using System.Threading.Tasks;
using AaxDecrypter; using AaxDecrypter;
using AudibleApi; using AudibleApi;
namespace FileLiberator.AaxcDownloadDecrypt namespace FileLiberator
{ {
public class DownloadDecryptBook : IDecryptable public class DownloadDecryptBook : IDecryptable
{ {
public event EventHandler<Action<byte[]>> RequestCoverArt;
public event EventHandler<LibraryBook> Begin; public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> DecryptBegin; public event EventHandler<string> DecryptBegin;
public event EventHandler<string> TitleDiscovered; public event EventHandler<string> TitleDiscovered;
@ -34,7 +35,6 @@ namespace FileLiberator.AaxcDownloadDecrypt
try try
{ {
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)) if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
@ -57,7 +57,6 @@ namespace FileLiberator.AaxcDownloadDecrypt
{ {
Completed?.Invoke(this, libraryBook); Completed?.Invoke(this, libraryBook);
} }
} }
private async Task<string> aaxToM4bConverterDecryptAsync(string destinationDir, LibraryBook libraryBook) private async Task<string> aaxToM4bConverterDecryptAsync(string destinationDir, LibraryBook libraryBook)
@ -74,38 +73,30 @@ namespace FileLiberator.AaxcDownloadDecrypt
var aaxcDecryptDlLic = new DownloadLicense(dlLic.DownloadUrl, dlLic.AudibleKey, dlLic.AudibleIV, Resources.UserAgent); var aaxcDecryptDlLic = new DownloadLicense(dlLic.DownloadUrl, dlLic.AudibleKey, dlLic.AudibleIV, Resources.UserAgent);
var destinationDirectory = Path.GetDirectoryName(destinationDir); if (Configuration.Instance.AllowLibationFixup)
if (Configuration.Instance.DownloadChapters)
{ {
var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId); var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId);
var aaxcDecryptChapters = new ChapterInfo(); var aaxcDecryptChapters = new ChapterInfo();
foreach (var chap in contentMetadata?.ChapterInfo?.Chapters) foreach (var chap in contentMetadata?.ChapterInfo?.Chapters)
aaxcDecryptChapters.AddChapter(new Chapter(chap.Title, chap.StartOffsetMs, chap.LengthMs)); aaxcDecryptChapters.AddChapter(new Chapter(chap.Title, chap.StartOffsetMs, chap.LengthMs));
aaxcDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic, aaxcDecryptChapters); aaxcDownloader = AaxcDownloadConverter.Create(destinationDir, aaxcDecryptDlLic, aaxcDecryptChapters);
} }
else else
{ {
aaxcDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic); aaxcDownloader = AaxcDownloadConverter.Create(destinationDir, aaxcDecryptDlLic);
} }
aaxcDownloader.AppName = "Libation"; aaxcDownloader.AppName = "Libation";
TitleDiscovered?.Invoke(this, aaxcDownloader.Title);
AuthorsDiscovered?.Invoke(this, aaxcDownloader.Author);
NarratorsDiscovered?.Invoke(this, aaxcDownloader.Narrator);
if (aaxcDownloader.CoverArt is not null)
CoverImageFilepathDiscovered?.Invoke(this, aaxcDownloader.CoverArt);
// override default which was set in CreateAsync // override default which was set in CreateAsync
var proposedOutputFile = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].m4b"); var proposedOutputFile = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].m4b");
aaxcDownloader.SetOutputFilename(proposedOutputFile); aaxcDownloader.SetOutputFilename(proposedOutputFile);
aaxcDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress); aaxcDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
aaxcDownloader.DecryptTimeRemaining += (s, remaining) => UpdateRemainingTime?.Invoke(this, remaining); aaxcDownloader.DecryptTimeRemaining += (s, remaining) => UpdateRemainingTime?.Invoke(this, remaining);
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());
@ -122,6 +113,26 @@ namespace FileLiberator.AaxcDownloadDecrypt
} }
} }
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
{
if (e is null && Configuration.Instance.AllowLibationFixup)
{
RequestCoverArt?.Invoke(this, aaxcDownloader.SetCoverArt);
}
if (e is not null)
{
CoverImageFilepathDiscovered?.Invoke(this, e);
}
}
private void aaxcDownloader_RetrievedTags(object sender, AaxcTagLibFile e)
{
TitleDiscovered?.Invoke(this, e.TitleSansUnabridged);
AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]");
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
}
private static string moveFilesToBooksDir(Book product, string outputAudioFilename) private static string 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

View File

@ -6,6 +6,7 @@ namespace FileLiberator
{ {
event EventHandler<string> DecryptBegin; event EventHandler<string> DecryptBegin;
event EventHandler<Action<byte[]>> RequestCoverArt;
event EventHandler<string> TitleDiscovered; event EventHandler<string> TitleDiscovered;
event EventHandler<string> AuthorsDiscovered; event EventHandler<string> AuthorsDiscovered;
event EventHandler<string> NarratorsDiscovered; event EventHandler<string> NarratorsDiscovered;

View File

@ -83,11 +83,11 @@ namespace FileManager
set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value); set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value);
} }
[Description("Download chapter titles from Audible?")] [Description("Allow Libation for fix up audiobook metadata?")]
public bool DownloadChapters public bool AllowLibationFixup
{ {
get => persistentDictionary.Get<bool>(nameof(DownloadChapters)); get => persistentDictionary.Get<bool>(nameof(AllowLibationFixup));
set => persistentDictionary.Set(nameof(DownloadChapters), value); set => persistentDictionary.Set(nameof(AllowLibationFixup), value);
} }
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app // note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app

View File

@ -60,7 +60,7 @@ namespace LibationLauncher
config.DownloadsInProgressEnum ??= "WinTemp"; config.DownloadsInProgressEnum ??= "WinTemp";
config.DecryptInProgressEnum ??= "WinTemp"; config.DecryptInProgressEnum ??= "WinTemp";
config.Books ??= Configuration.AppDir; config.Books ??= Configuration.AppDir;
config.DownloadChapters = true; config.AllowLibationFixup = true;
}; };
// setupDialog.BasicBtn_Click += (_, __) => // no action needed // setupDialog.BasicBtn_Click += (_, __) => // no action needed
setupDialog.AdvancedBtn_Click += (_, __) => isAdvanced = true; setupDialog.AdvancedBtn_Click += (_, __) => isAdvanced = true;
@ -235,9 +235,9 @@ namespace LibationLauncher
{ {
var persistentDictionary = new PersistentDictionary(Configuration.Instance.SettingsFilePath); var persistentDictionary = new PersistentDictionary(Configuration.Instance.SettingsFilePath);
if (persistentDictionary.GetString("DownloadChapters") is null) if (persistentDictionary.GetString("AllowLibationFixup") is null)
{ {
persistentDictionary.Set("DownloadChapters", true); persistentDictionary.Set("AllowLibationFixup", true);
} }
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile)) if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))

View File

@ -56,15 +56,22 @@ namespace LibationWinForms.BookLiberation
private void updateBookInfo() private void updateBookInfo()
=> bookInfoLbl.UIThread(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}"); => bookInfoLbl.UIThread(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
public void SetCoverImage(byte[] coverBytes) public void SetCoverImage(System.Drawing.Image coverImage)
=> pictureBox1.UIThread(() => pictureBox1.Image = ImageReader.ToImage(coverBytes)); => pictureBox1.UIThread(() => pictureBox1.Image = coverImage);
public void UpdateProgress(int percentage) public void UpdateProgress(int percentage)
{ {
if (percentage == 0) if (percentage == 0)
remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = "ETA:\r\n0 sec"); remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = "ETA:\r\n0 sec");
progressBar1.UIThread(() => progressBar1.Value = percentage); if (percentage == int.MaxValue)
progressBar1.UIThread(() => progressBar1.Style = ProgressBarStyle.Marquee);
else
progressBar1.UIThread(() =>
{
progressBar1.Value = percentage;
progressBar1.Style = ProgressBarStyle.Blocks;
});
} }
public void UpdateRemainingTime(TimeSpan remaining) public void UpdateRemainingTime(TimeSpan remaining)

View File

@ -70,8 +70,7 @@ namespace LibationWinForms.BookLiberation
{ {
var backupBook = new BackupBook(); var backupBook = new BackupBook();
backupBook.DownloadBook.Begin += (_, __) => wireUpEvents(backupBook.DownloadBook); backupBook.DecryptBook.Begin += (_, l) => wireUpEvents(backupBook.DecryptBook, l);
backupBook.DecryptBook.Begin += (_, __) => wireUpEvents(backupBook.DecryptBook);
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf); backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
// must occur before completedAction. A common use case is: // must occur before completedAction. A common use case is:
@ -80,13 +79,11 @@ namespace LibationWinForms.BookLiberation
// completedAction is to refresh grid // completedAction is to refresh grid
// - want to see that book disappear from grid // - want to see that book disappear from grid
// also for this to work, updateIsLiberated can NOT be async // also for this to work, updateIsLiberated can NOT be async
backupBook.DownloadBook.Completed += updateIsLiberated;
backupBook.DecryptBook.Completed += updateIsLiberated; backupBook.DecryptBook.Completed += updateIsLiberated;
backupBook.DownloadPdf.Completed += updateIsLiberated; backupBook.DownloadPdf.Completed += updateIsLiberated;
if (completedAction != null) if (completedAction != null)
{ {
backupBook.DownloadBook.Completed += completedAction;
backupBook.DecryptBook.Completed += completedAction; backupBook.DecryptBook.Completed += completedAction;
backupBook.DownloadPdf.Completed += completedAction; backupBook.DownloadPdf.Completed += completedAction;
} }
@ -104,9 +101,7 @@ namespace LibationWinForms.BookLiberation
#endregion #endregion
#region define how model actions will affect form behavior #region define how model actions will affect form behavior
void downloadBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Download Step, Begin: {libraryBook.Book}");
void statusUpdate(object _, string str) => logMe.Info("- " + str); void statusUpdate(object _, string str) => logMe.Info("- " + str);
void downloadBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Download Step, Completed: {libraryBook.Book}");
void decryptBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Begin: {libraryBook.Book}"); void decryptBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Begin: {libraryBook.Book}");
// extra line after book is completely finished // extra line after book is completely finished
void decryptBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}"); void decryptBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
@ -116,9 +111,6 @@ namespace LibationWinForms.BookLiberation
#endregion #endregion
#region subscribe new form to model's events #region subscribe new form to model's events
backupBook.DownloadBook.Begin += downloadBookBegin;
backupBook.DownloadBook.StatusUpdate += statusUpdate;
backupBook.DownloadBook.Completed += downloadBookCompleted;
backupBook.DecryptBook.Begin += decryptBookBegin; backupBook.DecryptBook.Begin += decryptBookBegin;
backupBook.DecryptBook.StatusUpdate += statusUpdate; backupBook.DecryptBook.StatusUpdate += statusUpdate;
backupBook.DecryptBook.Completed += decryptBookCompleted; backupBook.DecryptBook.Completed += decryptBookCompleted;
@ -131,9 +123,6 @@ namespace LibationWinForms.BookLiberation
// unsubscribe so disposed forms aren't still trying to receive notifications // unsubscribe so disposed forms aren't still trying to receive notifications
automatedBackupsForm.FormClosing += (_, __) => automatedBackupsForm.FormClosing += (_, __) =>
{ {
backupBook.DownloadBook.Begin -= downloadBookBegin;
backupBook.DownloadBook.StatusUpdate -= statusUpdate;
backupBook.DownloadBook.Completed -= downloadBookCompleted;
backupBook.DecryptBook.Begin -= decryptBookBegin; backupBook.DecryptBook.Begin -= decryptBookBegin;
backupBook.DecryptBook.StatusUpdate -= statusUpdate; backupBook.DecryptBook.StatusUpdate -= statusUpdate;
backupBook.DecryptBook.Completed -= decryptBookCompleted; backupBook.DecryptBook.Completed -= decryptBookCompleted;
@ -250,23 +239,51 @@ namespace LibationWinForms.BookLiberation
} }
// subscribed to Begin event because a new form should be created+processed+closed on each iteration // subscribed to Begin event because a new form should be created+processed+closed on each iteration
private static void wireUpEvents(IDecryptable decryptBook) private static void wireUpEvents(IDecryptable decryptBook, LibraryBook libraryBook)
{ {
#region create form #region create form
var decryptDialog = new DecryptForm(); var decryptDialog = new DecryptForm();
#endregion #endregion
#region Set initially displayed book properties from library info.
decryptDialog.SetTitle(libraryBook.Book.Title);
decryptDialog.SetAuthorNames(string.Join(", ", libraryBook.Book.Authors));
decryptDialog.SetNarratorNames(string.Join(", ", libraryBook.Book.NarratorNames));
decryptDialog.SetCoverImage(
WindowsDesktopUtilities.WinAudibleImageServer.GetImage(
libraryBook.Book.PictureId,
FileManager.PictureSize._80x80
));
#endregion
#region define how model actions will affect form behavior #region define how model actions will affect form behavior
void decryptBegin(object _, string __) => decryptDialog.Show(); void decryptBegin(object _, string __) => decryptDialog.Show();
void titleDiscovered(object _, string title) => decryptDialog.SetTitle(title); void titleDiscovered(object _, string title) => decryptDialog.SetTitle(title);
void authorsDiscovered(object _, string authors) => decryptDialog.SetAuthorNames(authors); void authorsDiscovered(object _, string authors) => decryptDialog.SetAuthorNames(authors);
void narratorsDiscovered(object _, string narrators) => decryptDialog.SetNarratorNames(narrators); void narratorsDiscovered(object _, string narrators) => decryptDialog.SetNarratorNames(narrators);
void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(coverBytes); void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(Dinah.Core.Drawing.ImageReader.ToImage(coverBytes));
void updateProgress(object _, int percentage) => decryptDialog.UpdateProgress(percentage); void updateProgress(object _, int percentage) => decryptDialog.UpdateProgress(percentage);
void updateRemainingTime(object _, TimeSpan remaining) => decryptDialog.UpdateRemainingTime(remaining); void updateRemainingTime(object _, TimeSpan remaining) => decryptDialog.UpdateRemainingTime(remaining);
void decryptCompleted(object _, string __) => decryptDialog.Close(); void decryptCompleted(object _, string __) => decryptDialog.Close();
void requestCoverArt(object _, Action<byte[]> setArt)
{
var picDef = new FileManager.PictureDefinition(libraryBook.Book.PictureId, FileManager.PictureSize._500x500);
(bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(picDef);
if (isDefault)
{
void pictureCached(object _, string pictureId) => onPictureCached(libraryBook, pictureId, setArt, pictureCached);
FileManager.PictureStorage.PictureCached += pictureCached;
}
else
setArt(picture);
}
#endregion #endregion
#region subscribe new form to model's events #region subscribe new form to model's events
@ -278,6 +295,7 @@ namespace LibationWinForms.BookLiberation
decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered; decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered;
decryptBook.UpdateProgress += updateProgress; decryptBook.UpdateProgress += updateProgress;
decryptBook.UpdateRemainingTime += updateRemainingTime; decryptBook.UpdateRemainingTime += updateRemainingTime;
decryptBook.RequestCoverArt += requestCoverArt;
decryptBook.DecryptCompleted += decryptCompleted; decryptBook.DecryptCompleted += decryptCompleted;
#endregion #endregion
@ -294,13 +312,25 @@ namespace LibationWinForms.BookLiberation
decryptBook.CoverImageFilepathDiscovered -= coverImageFilepathDiscovered; decryptBook.CoverImageFilepathDiscovered -= coverImageFilepathDiscovered;
decryptBook.UpdateProgress -= updateProgress; decryptBook.UpdateProgress -= updateProgress;
decryptBook.UpdateRemainingTime -= updateRemainingTime; decryptBook.UpdateRemainingTime -= updateRemainingTime;
decryptBook.RequestCoverArt -= requestCoverArt;
decryptBook.DecryptCompleted -= decryptCompleted; decryptBook.DecryptCompleted -= decryptCompleted;
decryptBook.Cancel(); decryptBook.Cancel();
}; };
#endregion #endregion
} }
private static void onPictureCached(LibraryBook libraryBook, string picId, Action<byte[]> setArt, EventHandler<string> pictureCacheDelegate)
{
if (picId == libraryBook.Book.PictureId)
{
FileManager.PictureStorage.PictureCached -= pictureCacheDelegate;
var picDef = new FileManager.PictureDefinition(libraryBook.Book.PictureId, FileManager.PictureSize._500x500);
(_, byte[] picture) = FileManager.PictureStorage.GetPicture(picDef);
setArt(picture);
}
}
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(IDownloadableProcessable downloadable) private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(IDownloadableProcessable downloadable)
{ {
#region create form and logger #region create form and logger

View File

@ -43,7 +43,7 @@
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.groupBox1 = new System.Windows.Forms.GroupBox(); this.groupBox1 = new System.Windows.Forms.GroupBox();
this.downloadChaptersCbox = new System.Windows.Forms.CheckBox(); this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
this.downloadsInProgressGb.SuspendLayout(); this.downloadsInProgressGb.SuspendLayout();
this.decryptInProgressGb.SuspendLayout(); this.decryptInProgressGb.SuspendLayout();
this.groupBox1.SuspendLayout(); this.groupBox1.SuspendLayout();
@ -215,7 +215,7 @@
// //
// groupBox1 // groupBox1
// //
this.groupBox1.Controls.Add(this.downloadChaptersCbox); this.groupBox1.Controls.Add(this.allowLibationFixupCbox);
this.groupBox1.Controls.Add(this.downloadsInProgressGb); this.groupBox1.Controls.Add(this.downloadsInProgressGb);
this.groupBox1.Controls.Add(this.decryptInProgressGb); this.groupBox1.Controls.Add(this.decryptInProgressGb);
this.groupBox1.Location = new System.Drawing.Point(18, 61); this.groupBox1.Location = new System.Drawing.Point(18, 61);
@ -229,13 +229,13 @@
// //
// downloadChaptersCbox // downloadChaptersCbox
// //
this.downloadChaptersCbox.AutoSize = true; this.allowLibationFixupCbox.AutoSize = true;
this.downloadChaptersCbox.Location = new System.Drawing.Point(10, 24); this.allowLibationFixupCbox.Location = new System.Drawing.Point(10, 24);
this.downloadChaptersCbox.Name = "downloadChaptersCbox"; this.allowLibationFixupCbox.Name = "downloadChaptersCbox";
this.downloadChaptersCbox.Size = new System.Drawing.Size(224, 19); this.allowLibationFixupCbox.Size = new System.Drawing.Size(262, 19);
this.downloadChaptersCbox.TabIndex = 6; this.allowLibationFixupCbox.TabIndex = 6;
this.downloadChaptersCbox.Text = "Download chapter titles from Audible"; this.allowLibationFixupCbox.Text = "Allow Libation to fix up audiobook metadata";
this.downloadChaptersCbox.UseVisualStyleBackColor = true; this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
// //
// SettingsDialog // SettingsDialog
// //
@ -284,6 +284,6 @@
private System.Windows.Forms.Button saveBtn; private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.Button cancelBtn; private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.GroupBox groupBox1; private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.CheckBox downloadChaptersCbox; private System.Windows.Forms.CheckBox allowLibationFixupCbox;
} }
} }

View File

@ -36,7 +36,7 @@ namespace LibationWinForms.Dialogs
? config.Books ? config.Books
: Path.GetDirectoryName(Exe.FileLocationOnDisk); : Path.GetDirectoryName(Exe.FileLocationOnDisk);
downloadChaptersCbox.Checked = config.DownloadChapters; allowLibationFixupCbox.Checked = config.AllowLibationFixup;
switch (config.DownloadsInProgressEnum) switch (config.DownloadsInProgressEnum)
{ {
@ -73,7 +73,7 @@ namespace LibationWinForms.Dialogs
private void saveBtn_Click(object sender, EventArgs e) private void saveBtn_Click(object sender, EventArgs e)
{ {
config.DownloadChapters = downloadChaptersCbox.Checked; config.AllowLibationFixup = allowLibationFixupCbox.Checked;
config.DownloadsInProgressEnum = downloadsInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp"; config.DownloadsInProgressEnum = downloadsInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp";
config.DecryptInProgressEnum = decryptInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp"; config.DecryptInProgressEnum = decryptInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp";

View File

@ -81,15 +81,6 @@
<metadata name="downloadsInProgressDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="downloadsInProgressDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
<metadata name="downloadsInProgressLibationFilesRb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="downloadsInProgressWinTempRb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="downloadsInProgressDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="decryptInProgressGb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="decryptInProgressGb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
@ -102,15 +93,6 @@
<metadata name="decryptInProgressDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="decryptInProgressDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
<metadata name="decryptInProgressLibationFilesRb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="decryptInProgressWinTempRb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="decryptInProgressDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="saveBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="saveBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
@ -123,9 +105,6 @@
<metadata name="downloadChaptersCbox.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="downloadChaptersCbox.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
<metadata name="downloadChaptersCbox.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="$this.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="$this.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>